Compare commits

..

9 Commits

Author SHA1 Message Date
Constantin Graf
0a28be83a1 Add more tests; Add filter in filament resource; Added options for user create command 2025-02-06 12:15:49 -05:00
Constantin Graf
4eb716d2cc Fixed bugs causing incorrect computed attributes in imported data 2025-02-04 19:51:54 -05:00
Constantin Graf
55323fa4b4 Add missing serve option to local filesystem disk 2025-02-04 19:51:23 -05:00
Constantin Graf
6df20ed1e5 Updated composer dependencies 2025-02-04 19:50:34 -05:00
Constantin Graf
bc7c564eb2 Added estimated time to clockify project import 2025-02-04 13:45:10 -05:00
Constantin Graf
5423b03201 Fixed timezones in unit tests 2024-12-20 19:47:12 -05:00
Constantin Graf
0e910ba565 Updated composer dependencies 2024-12-20 19:28:18 -05:00
Constantin Graf
bad1cd1343 Fixed reports in deletion service 2024-12-20 19:28:10 -05:00
Constantin Graf
dd312b396b Deactivated registration 2024-12-20 19:05:21 -05:00
149 changed files with 3373 additions and 3646 deletions

13
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,13 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
extends: ['plugin:vue/vue3-essential', '@vue/eslint-config-typescript/recommended', '@vue/eslint-config-prettier'],
rules: {
'vue/multi-word-component-names': 'off',
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
},
plugins: ['unused-imports'],
}

View File

@@ -14,7 +14,7 @@ on:
name: Build - Public
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
packages: write
contents: read

View File

@@ -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.3.1
uses: codecov/codecov-action@v4.5.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: solidtime-io/solidtime

View File

@@ -10,6 +10,6 @@ jobs:
uses: actions/checkout@v4
- name: "Check code style"
uses: aglipanci/laravel-pint-action@2.5
uses: aglipanci/laravel-pint-action@2.4
with:
configPath: "pint.json"

View File

@@ -57,7 +57,7 @@ class UserCreateCommand extends Command
}
$user = null;
DB::transaction(function () use (&$user, $name, $email, $password, $verifyEmail): void {
DB::transaction(function () use (&$user, $name, $email, $password): void {
$user = app(UserService::class)->createUser(
$name,
$email,
@@ -65,7 +65,6 @@ class UserCreateCommand extends Command
'UTC',
Weekday::Monday,
'EUR',
$verifyEmail
);
});
/** @var Organization|null $organization */
@@ -74,6 +73,10 @@ class UserCreateCommand extends Command
throw new LogicException('User does not have an organization');
}
if ($verifyEmail) {
$user->markEmailAsVerified();
}
$this->info('Created user "'.$name.'" ("'.$email.'")');
$this->line('ID: '.$user->getKey());
$this->line('Name: '.$name);

View File

@@ -25,7 +25,6 @@ use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use STS\FilamentImpersonate\Tables\Actions\Impersonate;
class UserResource extends Resource
@@ -40,8 +39,6 @@ class UserResource extends Resource
public static function form(Form $form): Form
{
/** @var User|null $record */
$record = $form->getRecord();
return $form
->columns(1)
->schema([
@@ -58,13 +55,6 @@ class UserResource extends Resource
Forms\Components\TextInput::make('email')
->label('Email')
->required()
->rules($record?->is_placeholder ? [] : [
UniqueEloquent::make(User::class, 'email')
->ignore($record?->getKey()),
])
->rule([
'email',
])
->maxLength(255),
Forms\Components\Toggle::make('is_placeholder')
->label('Is Placeholder?')
@@ -72,11 +62,7 @@ class UserResource extends Resource
->disabledOn(['edit']),
Forms\Components\DateTimePicker::make('email_verified_at')
->label('Email Verified At')
->hiddenOn(['create'])
->nullable(),
Forms\Components\Toggle::make('is_email_verified')
->label('Email Verified?')
->visibleOn(['create']),
Forms\Components\Select::make('timezone')
->label('Timezone')
->options(fn (): array => app(TimezoneService::class)->getSelectOptions())
@@ -88,16 +74,8 @@ class UserResource extends Resource
->required(),
TextInput::make('password')
->password()
->label('Password')
->dehydrateStateUsing(fn ($state) => Hash::make($state))
->dehydrated(fn ($state) => filled($state))
->hiddenOn(['create'])
->required(fn (string $context): bool => $context === 'create')
->maxLength(255),
TextInput::make('password_create')
->password()
->label('Password')
->visibleOn(['create'])
->required(fn (string $context): bool => $context === 'create')
->maxLength(255),
Forms\Components\Select::make('currency')

View File

@@ -20,11 +20,10 @@ class CreateUser extends CreateRecord
$user = $userService->createUser(
$data['name'],
$data['email'],
$data['password_create'],
$data['password'],
$data['timezone'],
Weekday::from($data['week_start']),
$data['currency'],
(bool) $data['is_email_verified']
);
return $user;

View File

@@ -34,7 +34,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
* @property string $id
* @property string $name
* @property string $email
* @property Carbon|null $email_verified_at
* @property string|null $email_verified_at
* @property string|null $password
* @property string|null $two_factor_secret
* @property string $timezone

View File

@@ -12,12 +12,11 @@ use App\Models\Organization;
use App\Models\ProjectMember;
use App\Models\TimeEntry;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
class UserService
{
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency, bool $verifyEmail = false): User
public function createUser(string $name, string $email, string $password, string $timezone, Weekday $weekStart, string $currency): User
{
$user = new User;
$user->name = $name;
@@ -25,9 +24,6 @@ class UserService
$user->password = Hash::make($password);
$user->timezone = $timezone;
$user->week_start = $weekStart;
if ($verifyEmail) {
$user->email_verified_at = Carbon::now();
}
$user->save();
$organization = new Organization;

116
composer.lock generated
View File

@@ -128,16 +128,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.339.7",
"version": "3.339.6",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643"
"reference": "cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7b7e48ce7970c0416c5fda045df7b93948fbf643",
"reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31",
"reference": "cc0b21de3b1eaabb7d0a1ed4f3f067bad4be5f31",
"shasum": ""
},
"require": {
@@ -220,9 +220,9 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.339.7"
"source": "https://github.com/aws/aws-sdk-php/tree/3.339.6"
},
"time": "2025-02-05T19:06:15+00:00"
"time": "2025-02-04T19:03:40+00:00"
},
{
"name": "bacon/bacon-qr-code",
@@ -692,16 +692,16 @@
},
{
"name": "composer/class-map-generator",
"version": "1.6.0",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/composer/class-map-generator.git",
"reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9"
"reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/ffe442c5974c44a9343e37a0abcb1cc37319f5b9",
"reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9",
"url": "https://api.github.com/repos/composer/class-map-generator/zipball/4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915",
"reference": "4b0a223cf5be7c9ee7e0ef1bc7db42b4a97c9915",
"shasum": ""
},
"require": {
@@ -745,7 +745,7 @@
],
"support": {
"issues": "https://github.com/composer/class-map-generator/issues",
"source": "https://github.com/composer/class-map-generator/tree/1.6.0"
"source": "https://github.com/composer/class-map-generator/tree/1.5.0"
},
"funding": [
{
@@ -761,7 +761,7 @@
"type": "tidelift"
}
],
"time": "2025-02-05T10:05:34+00:00"
"time": "2024-11-25T16:11:06+00:00"
},
{
"name": "composer/composer",
@@ -1519,16 +1519,16 @@
},
{
"name": "dedoc/scramble",
"version": "v0.12.5",
"version": "v0.12.2",
"source": {
"type": "git",
"url": "https://github.com/dedoc/scramble.git",
"reference": "de055584b61338dd6de6cc1cc20d79da6a442230"
"reference": "9482d3c6285ebb16a067bd28b01e94899e1fddd8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/de055584b61338dd6de6cc1cc20d79da6a442230",
"reference": "de055584b61338dd6de6cc1cc20d79da6a442230",
"url": "https://api.github.com/repos/dedoc/scramble/zipball/9482d3c6285ebb16a067bd28b01e94899e1fddd8",
"reference": "9482d3c6285ebb16a067bd28b01e94899e1fddd8",
"shasum": ""
},
"require": {
@@ -1583,7 +1583,7 @@
],
"support": {
"issues": "https://github.com/dedoc/scramble/issues",
"source": "https://github.com/dedoc/scramble/tree/v0.12.5"
"source": "https://github.com/dedoc/scramble/tree/v0.12.2"
},
"funding": [
{
@@ -1591,7 +1591,7 @@
"type": "github"
}
],
"time": "2025-02-05T11:22:52+00:00"
"time": "2025-02-03T12:23:33+00:00"
},
{
"name": "defuse/php-encryption",
@@ -2249,16 +2249,16 @@
},
{
"name": "filament/actions",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/actions.git",
"reference": "0eee8f8eeea4422c279c2622442b2a9a101d558c"
"reference": "cdefacc18993050cdd37e8e980ec66ca4109ae9a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/0eee8f8eeea4422c279c2622442b2a9a101d558c",
"reference": "0eee8f8eeea4422c279c2622442b2a9a101d558c",
"url": "https://api.github.com/repos/filamentphp/actions/zipball/cdefacc18993050cdd37e8e980ec66ca4109ae9a",
"reference": "cdefacc18993050cdd37e8e980ec66ca4109ae9a",
"shasum": ""
},
"require": {
@@ -2298,20 +2298,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-02-06T11:47:20+00:00"
"time": "2025-01-31T11:08:24+00:00"
},
{
"name": "filament/filament",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/panels.git",
"reference": "407b52ee57feed6f67706af2a18bc81d32aea3f1"
"reference": "c92daf4b6e4b478be5d32d5e1b404ba92bb45414"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/407b52ee57feed6f67706af2a18bc81d32aea3f1",
"reference": "407b52ee57feed6f67706af2a18bc81d32aea3f1",
"url": "https://api.github.com/repos/filamentphp/panels/zipball/c92daf4b6e4b478be5d32d5e1b404ba92bb45414",
"reference": "c92daf4b6e4b478be5d32d5e1b404ba92bb45414",
"shasum": ""
},
"require": {
@@ -2363,20 +2363,20 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-02-06T11:47:25+00:00"
"time": "2025-01-31T11:08:29+00:00"
},
{
"name": "filament/forms",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/forms.git",
"reference": "6ab0419fd3599aacdd7b2a8f69641cab6f158674"
"reference": "8856b3b3714a0efae65d9f817fcc1934b6f34fda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/6ab0419fd3599aacdd7b2a8f69641cab6f158674",
"reference": "6ab0419fd3599aacdd7b2a8f69641cab6f158674",
"url": "https://api.github.com/repos/filamentphp/forms/zipball/8856b3b3714a0efae65d9f817fcc1934b6f34fda",
"reference": "8856b3b3714a0efae65d9f817fcc1934b6f34fda",
"shasum": ""
},
"require": {
@@ -2419,11 +2419,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-02-06T11:47:21+00:00"
"time": "2025-01-31T11:08:23+00:00"
},
{
"name": "filament/infolists",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/infolists.git",
@@ -2474,7 +2474,7 @@
},
{
"name": "filament/notifications",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/notifications.git",
@@ -2526,7 +2526,7 @@
},
{
"name": "filament/support",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/support.git",
@@ -2585,16 +2585,16 @@
},
{
"name": "filament/tables",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/tables.git",
"reference": "d420b0b0cbe605c712ab054a43d51baf5095dc87"
"reference": "1b1d9c3b837c11408ad28240dc9e3e33340d870b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/d420b0b0cbe605c712ab054a43d51baf5095dc87",
"reference": "d420b0b0cbe605c712ab054a43d51baf5095dc87",
"url": "https://api.github.com/repos/filamentphp/tables/zipball/1b1d9c3b837c11408ad28240dc9e3e33340d870b",
"reference": "1b1d9c3b837c11408ad28240dc9e3e33340d870b",
"shasum": ""
},
"require": {
@@ -2633,11 +2633,11 @@
"issues": "https://github.com/filamentphp/filament/issues",
"source": "https://github.com/filamentphp/filament"
},
"time": "2025-02-06T11:47:34+00:00"
"time": "2025-01-31T11:08:58+00:00"
},
{
"name": "filament/widgets",
"version": "v3.2.137",
"version": "v3.2.136",
"source": {
"type": "git",
"url": "https://github.com/filamentphp/widgets.git",
@@ -9112,27 +9112,27 @@
},
{
"name": "spatie/laravel-package-tools",
"version": "1.19.0",
"version": "1.18.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
"reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa"
"reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
"reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78",
"reference": "ba67eee37d86ed775dab7dad58a7cbaf9a6cfe78",
"shasum": ""
},
"require": {
"illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
"illuminate/contracts": "^9.28|^10.0|^11.0",
"php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.23|^2.1|^3.1",
"phpunit/phpunit": "^9.5.24|^10.5|^11.5",
"orchestra/testbench": "^7.7|^8.0|^9.0",
"pestphp/pest": "^1.22|^2",
"phpunit/phpunit": "^9.5.24|^10.5",
"spatie/pest-plugin-test-time": "^1.1|^2.2"
},
"type": "library",
@@ -9160,7 +9160,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
"source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0"
"source": "https://github.com/spatie/laravel-package-tools/tree/1.18.3"
},
"funding": [
{
@@ -9168,7 +9168,7 @@
"type": "github"
}
],
"time": "2025-02-06T14:58:20+00:00"
"time": "2025-01-22T08:51:18+00:00"
},
{
"name": "spatie/regex",
@@ -14188,16 +14188,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.5.7",
"version": "11.5.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d"
"reference": "3c3ae14c90f244cdda95028c3e469028e8d1c02c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e1cb706f019e2547039ca2c839898cd5f557ee5d",
"reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3c3ae14c90f244cdda95028c3e469028e8d1c02c",
"reference": "3c3ae14c90f244cdda95028c3e469028e8d1c02c",
"shasum": ""
},
"require": {
@@ -14269,7 +14269,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.7"
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.6"
},
"funding": [
{
@@ -14285,7 +14285,7 @@
"type": "tidelift"
}
],
"time": "2025-02-06T16:10:05+00:00"
"time": "2025-01-31T07:03:30+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@@ -109,7 +109,7 @@ services:
- sail
- reverse-proxy
playwright:
image: mcr.microsoft.com/playwright:v1.50.0-jammy
image: mcr.microsoft.com/playwright:v1.46.1-jammy
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
working_dir: /src
extra_hosts:

View File

@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
'time_entry_range_selector'
);
await timeEntryRangeElement.click();
await page.getByTestId('time_entry_range_start').first().fill('1');
await page.getByTestId('time_picker_input').first().fill('1');
await Promise.all([
page.waitForResponse(async (response) => {
return (
@@ -204,7 +204,10 @@ test('test that updating a the start of an existing time entry in the overview w
(await response.json()).data.end !== null
);
}),
page.getByTestId('time_entry_range_end').press('Enter'),
page
.getByTestId('time_entry_range_end')
.getByTestId('time_picker_input')
.press('Enter'),
]);
});

View File

@@ -152,7 +152,7 @@ test('test that starting and updating the time while running works', async ({
JSON.stringify([])
);
}),
page.getByTestId('time_entry_time').press('Enter'),
page.getByTestId('time_entry_time').press('Tab'),
]);
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);

View File

@@ -1,36 +0,0 @@
import eslint from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import eslintPluginVue from 'eslint-plugin-vue';
import globals from 'globals';
import typescriptEslint from 'typescript-eslint';
import unusedImports from "eslint-plugin-unused-imports";
export default typescriptEslint.config(
{ ignores: ['*.d.ts', '**/coverage', '**/dist'] },
{
extends: [
eslint.configs.recommended,
...typescriptEslint.configs.recommended,
...eslintPluginVue.configs['flat/recommended'],
],
files: ['**/*.{ts,vue,js}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: globals.browser,
parserOptions: {
parser: typescriptEslint.parser,
},
},
plugins: {
"unused-imports": unusedImports,
},
rules: {
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": "error",
},
},
eslintConfigPrettier
);

View File

@@ -5,20 +5,18 @@ declare(strict_types=1);
return [
'clockify_time_entries' => [
'name' => 'Clockify Time Entries',
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br>'.
'2. In the same preferences page change the language of Clockfiy to English.<br>'.
'3. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
'4. Now select the date range that you want to export in the right top. '.
'description' => '1. First make sure that you set the Date format to "MM/DD/YYYY" and the Time format to "12-hour" in the user settings.<br> '.
'2. Go to REPORTS -> TIME -> Detailed in the navigation on the left. <br>'.
'3. Now select the date range that you want to export in the right top. '.
'It is currently not possible to select more than one year. You can export each year separately and import them one after another .'.
'<br> 4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table left of the printer symbol. '.
'<br><br>Before you import make sure that the Timezone settings in Clockify are the same as in solidtime.',
],
'clockify_projects' => [
'name' => 'Clockify Projects',
'description' => '1. Make sure to set the language of Clockify to English in "Preferences -> General".<br>'.
'2. Go to PROJECTS in the navigation on the left.<br> '.
'3. Now click on the three dots on the right of the project that you want to export and select Export.<br> '.
'4. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.',
'description' => '1. Go to PROJECTS in the navigation on the left.<br> '.
'2. Now click on the three dots on the right of the project that you want to export and select Export.<br> '.
'3. Now click Export -> Save as CSV. The Export dropdown is in the header of the export table in the top right corner.',
],
'toggl_data_importer' => [
'name' => 'Toggl Data Importer',

3414
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,60 +4,53 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint resources/js",
"lint:fix": "eslint --fix resources/js",
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
"type-check": "vue-tsc --noEmit",
"test:e2e": "rm -rf test-results/.auth && npx playwright test",
"zod:generate": "npx openapi-zod-client http://localhost:80/docs/api.json --output resources/js/packages/api/src/openapi.json.client.ts --base-url /api"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@inertiajs/vue3": "^1.0.0",
"@playwright/test": "^1.41.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.10.10",
"@vitejs/plugin-vue": "^5.2.1",
"@types/node": "^20.11.5",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.20",
"axios": "^1.6.4",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-unused-imports": "^3.1.0",
"laravel-vite-plugin": "^1.0.0",
"openapi-zod-client": "^1.16.2",
"postcss": "^8.4.47",
"postcss-nesting": "^12.1.5",
"tailwindcss": "^3.4.13",
"typescript": "^5.7.3",
"vite": "^6.0.11",
"vite-plugin-checker": "^0.8.0",
"vue": "^3.5.0",
"vue-tsc": "^2.2.0"
"typescript": "^5.3.3",
"vite": "^5.0.0",
"vite-plugin-checker": "^0.7.2",
"vue": "^3.4.0",
"vue-tsc": "^2.0.28"
},
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/vue": "^1.0.6",
"@heroicons/vue": "^2.1.1",
"@rushstack/eslint-patch": "^1.10.5",
"@rushstack/eslint-patch": "^1.7.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/vue-query": "^5.56.2",
"@tanstack/vue-query-devtools": "^5.58.0",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.3.0",
"@vueuse/core": "^12.5.0",
"@vueuse/integrations": "^12.5.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vueuse/core": "^10.11.0",
"@vueuse/integrations": "^11.1.0",
"dayjs": "^1.11.11",
"echarts": "^5.5.0",
"focus-trap": "^7.6.0",
"parse-duration": "^2.0.1",
"parse-duration": "^1.1.0",
"pinia": "^2.1.7",
"radix-vue": "^1.9.6",
"tailwind-merge": "^2.2.1",
"vue-echarts": "^7.0.3"
},
"overrides": {
"vite-plugin-checker": {
"vue-tsc": "$vue-tsc"
}
"vue-echarts": "^6.7.2"
}
}

View File

@@ -112,7 +112,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideBlackFridayBanner = true">
<button @click="hideBlackFridayBanner = true" class="p-1">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
@@ -142,7 +142,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideTrialBanner = true">
<button @click="hideTrialBanner = true" class="p-1">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
@@ -174,7 +174,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideBlockedBanner = true">
<button @click="hideBlockedBanner = true" class="p-1">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>
@@ -206,7 +206,7 @@ const showBlackFridayBanner = computed(() => {
<span>Upgrade now</span>
</div>
</Link>
<button class="p-1" @click="hideFreeUpgradeBanner = true">
<button @click="hideFreeUpgradeBanner = true" class="p-1">
<XMarkIcon
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
</button>

View File

@@ -45,10 +45,10 @@ useFocus(clientNameInput, { initialValue: true });
v-model="client.name"
type="text"
placeholder="Client Name"
@keydown.enter="submit"
class="mt-1 block w-full"
required
autocomplete="clientName"
@keydown.enter="submit" />
autocomplete="clientName" />
</div>
</div>
</template>

View File

@@ -46,10 +46,10 @@ useFocus(clientNameInput, { initialValue: true });
v-model="clientBody.name"
type="text"
placeholder="Client Name"
@keydown.enter="submit"
class="mt-1 block w-full"
required
autocomplete="clientName"
@keydown.enter="submit" />
autocomplete="clientName" />
</div>
</div>
</template>

View File

@@ -23,28 +23,28 @@ const props = defineProps<{
<div class="min-w-[150px]">
<button
v-if="canUpdateClients()"
@click="emit('edit')"
:aria-label="'Edit Client ' + props.client.name"
data-testid="client_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click.prevent="emit('archive')"
v-if="canUpdateClients()"
:aria-label="'Archive Client ' + props.client.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('archive')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
</button>
<button
v-if="canDeleteClients()"
@click="emit('delete')"
:aria-label="'Delete Client ' + props.client.name"
data-testid="client_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Client) {
<template>
<MultiselectDropdown
search-placeholder="Search for a Client..."
searchPlaceholder="Search for a Client..."
:items="clients"
:get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem">

View File

@@ -25,18 +25,18 @@ const createClient = ref(false);
style="grid-template-columns: 1fr 150px 200px 80px">
<ClientTableHeading></ClientTableHeading>
<div
v-if="clients.length === 0"
class="col-span-2 py-24 text-center">
class="col-span-2 py-24 text-center"
v-if="clients.length === 0">
<UserCircleIcon
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
<h3 class="text-white font-semibold">No clients found</h3>
<p v-if="canCreateClients()" class="pb-5">
<p class="pb-5" v-if="canCreateClients()">
Create your first client now!
</p>
<SecondaryButton
v-if="canCreateClients()"
:icon="PlusIcon as Component"
@click="createClient = true"
:icon="PlusIcon as Component"
>Create your First Client
</SecondaryButton>
</div>

View File

@@ -38,8 +38,8 @@ const showEditModal = ref(false);
<template>
<TableRow>
<ClientEditModal
v-model:show="showEditModal"
:client="client"></ClientEditModal>
:client="client"
v-model:show="showEditModal"></ClientEditModal>
<div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">
<span>

View File

@@ -10,16 +10,16 @@ const emit = defineEmits<{
<template>
<MoreOptionsDropdown label="Actions for the invitation">
<button
@click="emit('resend')"
data-testid="invitation_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('resend')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon>
<span>Resend Invitation</span>
</button>
<button
@click="emit('delete')"
data-testid="invitation_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -18,10 +18,10 @@ defineEmits<{
<template>
<BillableRateModal
@submit="$emit('submit')"
v-model:show="show"
v-model:saving="saving"
title="Update Member Billable Rate"
@submit="$emit('submit')">
title="Update Member Billable Rate">
<p class="py-1 text-center">
The billable rate of {{ memberName }} will be updated to
<strong>{{

View File

@@ -17,8 +17,8 @@ const model = defineModel<string>({
const props = withDefaults(
defineProps<{
hiddenMembers?: ProjectMember[];
disabled?: boolean;
hiddenMembers: ProjectMember[];
disabled: boolean;
}>(),
{
hiddenMembers: () => [] as ProjectMember[],
@@ -76,7 +76,7 @@ const currentValue = computed(() => {
:items="filteredMembers"
:get-key-from-item="(member) => member.id"
:get-name-for-item="(member) => member.name">
<template #trigger>
<template v-slot:trigger>
<Badge
tag="button"
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
@@ -84,7 +84,7 @@ const currentValue = computed(() => {
<div v-if="currentValue" class="flex-1 truncate">
{{ currentValue }}
</div>
<div v-else class="flex-1">Select a member...</div>
<div class="flex-1" v-else>Select a member...</div>
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
</Badge>
</template>

View File

@@ -108,11 +108,11 @@ const roleDescription = computed(() => {
v-model:saving="saving"
v-model:show="showBillableRateModal"
:member-name="member.name"
:new-billable-rate="memberBody.billable_rate"
:newBillableRate="memberBody.billable_rate"
@submit="submitBillableRate"></MemberBillableRateModal>
<MemberOwnershipTransferConfirmModal
v-model:show="showOwnershipTransferConfirmModal"
:member-name="member.name"
v-model:show="showOwnershipTransferConfirmModal"
@submit="submit"></MemberOwnershipTransferConfirmModal>
<DialogModal closeable :show="show" @close="show = false">
<template #title>
@@ -127,9 +127,9 @@ const roleDescription = computed(() => {
<div>
<InputLabel for="role" value="Role" />
<MemberRoleSelect
v-model="memberBody.role"
class="mt-2"
name="role"></MemberRoleSelect>
name="role"
v-model="memberBody.role"></MemberRoleSelect>
</div>
<div class="flex-1 text-xs flex items-center pt-6">
<p>{{ roleDescription }}</p>
@@ -140,28 +140,28 @@ const roleDescription = computed(() => {
<div>
<InputLabel for="billableType" value="Billable" />
<MemberBillableSelect
class="mt-2"
name="billableType"
v-model="
billableRateSelect
"
class="mt-2"
name="billableType"></MemberBillableSelect>
"></MemberBillableSelect>
</div>
<div
v-if="billableRateSelect === 'custom-rate'"
class="flex-1">
class="flex-1"
v-if="billableRateSelect === 'custom-rate'">
<InputLabel
for="memberBillableRate"
class="mb-2"
value="Billable Rate" />
<BillableRateInput
v-model="
memberBody.billable_rate
"
focus
class="w-full"
:currency="getOrganizationCurrencyString()"
@keydown.enter="saveWithChecks()"
name="memberBillableRate"
@keydown.enter="saveWithChecks()"></BillableRateInput>
v-model="
memberBody.billable_rate
"></BillableRateInput>
</div>
</div>
</div>

View File

@@ -112,11 +112,11 @@ useFocus(clientNameInput, { initialValue: true });
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<PrimaryButton
type="button"
class="mt-6"
v-if="
isBillingActivated() && canUpdateOrganization()
"
type="button"
class="mt-6">
">
<CreditCardIcon class="w-5 h-5 me-2" />
Go to Billing
</PrimaryButton>
@@ -128,15 +128,15 @@ useFocus(clientNameInput, { initialValue: true });
<InputLabel for="email" value="Email" />
<TextInput
id="email"
name="email"
ref="memberEmailInput"
v-model="addTeamMemberForm.email"
name="email"
type="text"
placeholder="Member Email"
@keydown.enter="submit"
class="mt-1 block w-full"
required
autocomplete="memberName"
@keydown.enter="submit" />
autocomplete="memberName" />
<InputError :message="errors.email" class="mt-2" />
</div>

View File

@@ -20,19 +20,19 @@ const props = defineProps<{
<div class="min-w-[150px]">
<button
v-if="canUpdateMembers()"
@click="emit('edit')"
:aria-label="'Edit Member ' + props.member.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
v-if="canDeleteMembers()"
@click="emit('delete')"
:aria-label="'Delete Member ' + props.member.name"
data-testid="member_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Member) {
<template>
<MultiselectDropdown
search-placeholder="Search for a Member..."
searchPlaceholder="Search for a Member..."
:items="members"
:get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem">

View File

@@ -36,9 +36,9 @@ const emit = defineEmits<{
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
<PrimaryButton
class="ms-3"
@click="emit('submit')"
:class="{ 'opacity-25': saving }"
:disabled="saving"
@click="emit('submit')">
:disabled="saving">
Confirm Transfer
</PrimaryButton>
</template>

View File

@@ -89,8 +89,8 @@ async function invitePlaceholder(id: string) {
member.is_placeholder === true &&
canInvitePlaceholderMembers()
"
size="small"
@click="invitePlaceholder(member.id)"
size="small"
>Invite</SecondaryButton
>
<MemberMoreOptionsDropdown
@@ -99,8 +99,8 @@ async function invitePlaceholder(id: string) {
@delete="removeMember"></MemberMoreOptionsDropdown>
</div>
<MemberEditModal
v-model:show="showEditMemberModal"
:member="member"></MemberEditModal>
:member="member"
v-model:show="showEditMemberModal"></MemberEditModal>
</TableRow>
</template>

View File

@@ -34,8 +34,8 @@
<div class="ml-4 flex flex-shrink-0">
<button
type="button"
class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
@click="show = false">
@click="show = false"
class="inline-flex rounded-md bg-card-background text-muted hover:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span class="sr-only">Close</span>
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
</button>

View File

@@ -17,10 +17,10 @@ defineEmits<{
<template>
<BillableRateModal
@submit="$emit('submit')"
v-model:show="show"
v-model:saving="saving"
title="Update Organization Billable Rate"
@submit="$emit('submit')">
title="Update Organization Billable Rate">
<p class="py-0.5 text-center">
The organization billable rate will be updated to
<strong>{{

View File

@@ -40,7 +40,7 @@ const shownProjects = computed(() => {
withDefaults(
defineProps<{
border?: boolean;
border: boolean;
}>(),
{
border: true,
@@ -123,17 +123,17 @@ function updateValue(project: Project) {
<template #content>
<ComboboxRoot
:open="open"
:model-value="currentProject"
:search-term="searchValue"
class="relative"
@update:model-value="updateValue"
@update:search-term="(e) => console.log(e)">
:modelValue="currentProject"
@update:modelValue="updateValue"
@update:searchTerm="(e) => console.log(e)"
:searchTerm="searchValue"
class="relative">
<ComboboxAnchor>
<ComboboxInput
@keydown.enter="addProjectIfNoneExists"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
placeholder="Search for a project..."
@keydown.enter="addProjectIfNoneExists" />
placeholder="Search for a project..." />
</ComboboxAnchor>
<ComboboxContent>
<ComboboxViewport

View File

@@ -90,8 +90,8 @@ async function submitBillableRate() {
<div class="text-center">
<InputLabel for="color" value="Color" />
<ProjectColorSelector
v-model="project.color"
class="mt-1"></ProjectColorSelector>
class="mt-1"
v-model="project.color"></ProjectColorSelector>
</div>
</div>
<div class="w-full">
@@ -102,18 +102,18 @@ async function submitBillableRate() {
v-model="project.name"
type="text"
placeholder="Project Name"
@keydown.enter="submit()"
class="mt-1 block w-full"
required
autocomplete="projectName"
@keydown.enter="submit()" />
autocomplete="projectName" />
</div>
<div class="">
<InputLabel for="client" value="Client" />
<ClientDropdown
v-model="project.client_id"
:create-client
:createClient
:clients="clients"
class="mt-1">
class="mt-1"
v-model="project.client_id">
<template #trigger>
<Badge
class="bg-input-background cursor-pointer hover:bg-tertiary"
@@ -133,18 +133,18 @@ async function submitBillableRate() {
<div class="lg:grid grid-cols-2 gap-12">
<div>
<ProjectEditBillableSection
v-model:is-billable="project.is_billable"
v-model:billable-rate="
project.billable_rate
"
@submit="submit"
:currency="getOrganizationCurrencyString()"
@submit="submit"></ProjectEditBillableSection>
v-model:isBillable="project.is_billable"
v-model:billableRate="
project.billable_rate
"></ProjectEditBillableSection>
</div>
<div>
<EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()"
v-model="project.estimated_time"
@submit="submit()"></EstimatedTimeSection>
@submit="submit()"
v-model="project.estimated_time"></EstimatedTimeSection>
</div>
</div>
</template>
@@ -163,9 +163,9 @@ async function submitBillableRate() {
<ProjectBillableRateModal
v-model:show="showBillableRateModal"
:currency="getOrganizationCurrencyString()"
@submit="submitBillableRate"
:new-billable-rate="project.billable_rate"
:project-name="project.name"
@submit="submitBillableRate"></ProjectBillableRateModal>
:project-name="project.name"></ProjectBillableRateModal>
</template>
<style scoped></style>

View File

@@ -21,29 +21,29 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
<div class="min-w-[150px]">
<button
@click.prevent="emit('edit')"
v-if="canUpdateProjects()"
:aria-label="'Edit Project ' + props.project.name"
data-testid="project_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click.prevent="emit('archive')"
v-if="canUpdateProjects()"
:aria-label="'Archive Project ' + props.project.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('archive')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
</button>
<button
v-if="canDeleteProjects()"
@click.prevent="emit('delete')"
:aria-label="'Delete Project ' + props.project.name"
data-testid="project_delete"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('delete')">
v-if="canDeleteProjects()"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Project) {
<template>
<MultiselectDropdown
search-placeholder="Search for a Project..."
searchPlaceholder="Search for a Project..."
:items="projects"
:get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem">

View File

@@ -44,12 +44,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
<template>
<ProjectCreateModal
v-model:show="showCreateProjectModal"
:create-project
:create-client
:createProject
:createClient
:currency="getOrganizationCurrencyString()"
:clients="clients"
:enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
:enableEstimatedTime="isAllowedToPerformPremiumAction"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
<div class="flow-root max-w-[100vw] overflow-x-auto">
<div class="inline-block min-w-full align-middle">
<div
@@ -57,12 +57,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
class="grid min-w-full"
:style="gridTemplate">
<ProjectTableHeading
:show-billable-rate="
:showBillableRate="
props.showBillableRate
"></ProjectTableHeading>
<div
v-if="projects.length === 0"
class="col-span-5 py-24 text-center">
class="col-span-5 py-24 text-center"
v-if="projects.length === 0">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold">
@@ -81,14 +81,14 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
</p>
<SecondaryButton
v-if="canCreateProjects()"
:icon="PlusIcon"
@click="showCreateProjectModal = true"
:icon="PlusIcon"
>Create your First Project
</SecondaryButton>
</div>
<template v-for="project in projects" :key="project.id">
<ProjectTableRow
:show-billable-rate="props.showBillableRate"
:showBillableRate="props.showBillableRate"
:project="project"></ProjectTableRow>
</template>
</div>

View File

@@ -19,8 +19,8 @@ defineProps<{
Progress
</div>
<div
v-if="showBillableRate"
class="px-3 py-1.5 text-left font-semibold text-white">
class="px-3 py-1.5 text-left font-semibold text-white"
v-if="showBillableRate">
Billable Rate
</div>
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>

View File

@@ -83,8 +83,8 @@ const showEditProjectModal = ref(false);
</div>
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
<div
v-if="project.client_id"
class="overflow-ellipsis overflow-hidden">
class="overflow-ellipsis overflow-hidden"
v-if="project.client_id">
{{ client?.name }}
</div>
<div v-else>No client</div>
@@ -106,8 +106,8 @@ const showEditProjectModal = ref(false);
<span v-else> -- </span>
</div>
<div
v-if="showBillableRate"
class="whitespace-nowrap px-3 py-4 text-sm text-muted">
class="whitespace-nowrap px-3 py-4 text-sm text-muted"
v-if="showBillableRate">
{{ billableRateInfo }}
</div>
<div

View File

@@ -18,10 +18,10 @@ defineEmits<{
<template>
<BillableRateModal
@submit="$emit('submit')"
v-model:show="show"
v-model:saving="saving"
title="Update Project Member Billable Rate"
@submit="$emit('submit')">
title="Update Project Member Billable Rate">
<p class="py-1 text-center">
The billable rate of {{ memberName }} will be updated to
<strong>{{

View File

@@ -52,16 +52,16 @@ useFocus(projectNameInput, { initialValue: true });
<div class="grid grid-cols-3 items-center space-x-4">
<div class="col-span-3 sm:col-span-2">
<MemberCombobox
v-model="projectMember.member_id"
:hidden-members="props.existingMembers"></MemberCombobox>
:hidden-members="props.existingMembers"
v-model="projectMember.member_id"></MemberCombobox>
</div>
<div class="col-span-3 sm:col-span-1 flex-1">
<BillableRateInput
name="billable_rate"
:currency="getOrganizationCurrencyString()"
v-model="
projectMember.billable_rate
"
name="billable_rate"
:currency="getOrganizationCurrencyString()"></BillableRateInput>
"></BillableRateInput>
</div>
</div>
</template>

View File

@@ -75,8 +75,8 @@ useFocus(projectNameInput, { initialValue: true });
<template #content>
<ProjectMemberBillableRateModal
v-model:show="showBillableRateModal"
:member-name="props.name"
v-model:show="showBillableRateModal"
:new-billable-rate="projectMemberBody.billable_rate"
@close="showBillableRateModal = false"
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
@@ -92,12 +92,12 @@ useFocus(projectNameInput, { initialValue: true });
class="mb-2"
value="Billable Rate"></InputLabel>
<BillableRateInput
v-model="
projectMemberBody.billable_rate
"
@keydown.enter="submit"
:currency="getOrganizationCurrencyString()"
name="billable_rate"
@keydown.enter="submit"></BillableRateInput>
v-model="
projectMemberBody.billable_rate
"></BillableRateInput>
</div>
</div>
</template>

View File

@@ -27,17 +27,17 @@ const currentMember = computed(() => {
<MoreOptionsDropdown
:label="'Actions for Project Member ' + currentMember?.name">
<button
@click.prevent="emit('edit')"
:aria-label="'Edit Project Member ' + currentMember?.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click.prevent="emit('delete')"
:aria-label="'Delete Project Member ' + currentMember?.name"
data-testid="project_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Remove from Team</span>
</button>

View File

@@ -18,9 +18,9 @@ const createProjectMember = ref(false);
<template>
<ProjectMemberCreateModal
v-model:show="createProjectMember"
:existing-members="projectMembers"
:project-id="projectId"></ProjectMemberCreateModal>
:project-id="projectId"
v-model:show="createProjectMember"></ProjectMemberCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
@@ -29,15 +29,15 @@ const createProjectMember = ref(false);
style="grid-template-columns: 1fr 150px 150px 80px">
<ProjectMemberTableHeading></ProjectMemberTableHeading>
<div
v-if="projectMembers.length === 0"
class="col-span-5 py-24 text-center">
class="col-span-5 py-24 text-center"
v-if="projectMembers.length === 0">
<UserGroupIcon
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
<h3 class="text-white font-semibold">No project members</h3>
<p class="pb-5">Add the first project member!</p>
<SecondaryButton
:icon="PlusIcon"
@click="createProjectMember = true"
:icon="PlusIcon"
>Add a new Project Member
</SecondaryButton>
</div>

View File

@@ -37,8 +37,8 @@ const showEditModal = ref(false);
<template>
<TableRow>
<ProjectMemberEditModal
v-model:show="showEditModal"
:name="member?.name"
v-model:show="showEditModal"
:project-member="projectMember"></ProjectMemberEditModal>
<div
class="whitespace-nowrap flex items-center space-x-5 3xl:pl-12 py-4 pr-3 text-sm font-medium text-white pl-4 sm:pl-6 lg:pl-8 3xl:pl-12">

View File

@@ -82,22 +82,22 @@ async function submit() {
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="report.name"
class="mt-1.5 w-full"></TextInput>
class="mt-1.5 w-full"
v-model="report.name"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
class="mt-1.5 w-full"
v-model="report.description"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-3 px-2 py-3">
<Checkbox
id="is_public"
v-model:checked="report.is_public"></Checkbox>
v-model:checked="report.is_public"
id="is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
</div>
<div

View File

@@ -96,22 +96,22 @@ async function submit() {
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="report.name"
class="mt-1.5 w-full"></TextInput>
class="mt-1.5 w-full"
v-model="report.name"></TextInput>
</div>
<div>
<InputLabel for="description" value="Description" />
<TextInput
id="description"
v-model="report.description"
class="mt-1.5 w-full"></TextInput>
class="mt-1.5 w-full"
v-model="report.description"></TextInput>
</div>
<InputLabel value="Visibility" />
<div class="flex items-center space-x-12">
<div class="flex items-center space-x-2 px-2 py-3">
<Checkbox
id="is_public"
v-model:checked="report.is_public"></Checkbox>
v-model:checked="report.is_public"
id="is_public"></Checkbox>
<InputLabel for="is_public" value="Public" />
</div>
<div

View File

@@ -17,19 +17,19 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
<div class="min-w-[150px]">
<button
@click.prevent="emit('edit')"
v-if="canUpdateReport()"
:aria-label="'Edit Report ' + props.report.name"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
v-if="canDeleteReport()"
@click.prevent="emit('delete')"
:aria-label="'Delete Report ' + props.report.name"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click.prevent="emit('delete')">
v-if="canDeleteReport()"
class="border-b border-card-background-separator flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -27,8 +27,8 @@ function onSaveReportClick() {
<template>
<ReportCreateModal
v-model:show="showCreateReportModal"
:properties="reportProperties"></ReportCreateModal>
:properties="reportProperties"
v-model:show="showCreateReportModal"></ReportCreateModal>
<UpgradeModal v-model:show="showPremiumModal">
<strong>Sharable Reports</strong> is only available in solidtime
Professional.

View File

@@ -27,19 +27,19 @@ const gridTemplate = computed(() => {
:style="gridTemplate">
<ReportTableHeading></ReportTableHeading>
<div
v-if="reports.length === 0"
class="col-span-5 py-24 text-center">
class="col-span-5 py-24 text-center"
v-if="reports.length === 0">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold">
No shared reports found
</h3>
<p v-if="canCreateProjects()" class="pb-5">
<p class="pb-5" v-if="canCreateProjects()">
Create your first project now!
</p>
<SecondaryButton
:icon="PlusIcon"
@click="router.visit(route('reporting'))"
:icon="PlusIcon"
>Go to the overview to create a report
</SecondaryButton>
</div>

View File

@@ -87,7 +87,7 @@ async function deleteReport() {
<span v-else>Copied!</span>
</SecondaryButton>
<button
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-ring"
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-white/80"
@click="openSharableLink">
<ArrowTopRightOnSquareIcon
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>

View File

@@ -157,7 +157,7 @@ const option = ref({
:autoresize="true"
class="chart"
:option="option" />
<div v-else class="chart flex flex-col items-center justify-center">
<div class="chart flex flex-col items-center justify-center" v-else>
<p class="text-lg text-white font-semibold">
No time entries found
</p>

View File

@@ -22,10 +22,10 @@ function downloadCurrentExport() {
<Modal
closeable
max-width="lg"
:show="showExportModal"
@close="showExportModal = false">
@close="showExportModal = false"
:show="showExportModal">
<button
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-white/80 transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
</button>
<div class="text-center text-text-primary py-6">

View File

@@ -21,7 +21,6 @@ const activeClass = computed(() => {
<template>
<Badge
size="large"
tag="button"
:class="
twMerge(
'cursor-pointer hover:bg-card-background transition flex',

View File

@@ -23,7 +23,7 @@ const title = computed(() => {
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
:items="groupByOptions">
<template #trigger>
<template v-slot:trigger>
<Badge
size="large"
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">

View File

@@ -35,9 +35,9 @@ const expanded = ref(false);
)
">
<GroupedItemsCountButton
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
:expanded="expanded"
@click="expanded = !expanded">
@click="expanded = !expanded"
v-if="entry.grouped_data && entry.grouped_data?.length > 0">
{{ entry.grouped_data?.length }}
</GroupedItemsCountButton>
<span>
@@ -52,13 +52,13 @@ const expanded = ref(false);
</div>
</div>
<div
v-if="expanded && entry.grouped_data"
class="col-span-3 grid bg-quaternary"
style="grid-template-columns: 1fr 150px 150px">
style="grid-template-columns: 1fr 150px 150px"
v-if="expanded && entry.grouped_data">
<ReportingRow
indent
v-for="subEntry in entry.grouped_data"
:key="subEntry.description ?? 'none'"
indent
:entry="subEntry"></ReportingRow>
</div>
</template>

View File

@@ -10,18 +10,18 @@ defineProps<{
<template>
<TabBar>
<TabBarItem
:active="active === 'reporting'"
@click="router.visit(route('reporting'))"
:active="active === 'reporting'"
>Overview</TabBarItem
>
<TabBarItem
:active="active === 'detailed'"
@click="router.visit(route('reporting.detailed'))"
:active="active === 'detailed'"
>Detailed</TabBarItem
>
<TabBarItem
:active="active === 'shared'"
@click="router.visit(route('reporting.shared'))"
:active="active === 'shared'"
>Shared</TabBarItem
>
</TabBar>

View File

@@ -14,10 +14,10 @@ const props = defineProps<{
<template>
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
<button
@click="emit('delete')"
:aria-label="'Delete Tag ' + props.tag.name"
data-testid="tag_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -19,8 +19,8 @@ const showCreateTagModal = ref(false);
<template>
<TagCreateModal
v-model:show="showCreateTagModal"
:create-tag></TagCreateModal>
:createTag
v-model:show="showCreateTagModal"></TagCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
@@ -29,18 +29,18 @@ const showCreateTagModal = ref(false);
style="grid-template-columns: 1fr 80px">
<TagTableHeading></TagTableHeading>
<div
v-if="tags.length === 0"
class="col-span-5 py-24 text-center">
class="col-span-5 py-24 text-center"
v-if="tags.length === 0">
<FolderPlusIcon
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
<h3 class="text-white font-semibold">No tags found</h3>
<p v-if="canCreateTags()" class="pb-5">
<p class="pb-5" v-if="canCreateTags()">
Create your first tag now!
</p>
<SecondaryButton
v-if="canCreateTags()"
:icon="PlusIcon"
@click="showCreateTagModal = true"
:icon="PlusIcon"
>Create your First Tag</SecondaryButton
>
</div>

View File

@@ -53,19 +53,19 @@ useFocus(taskNameInput, { initialValue: true });
v-model="taskName"
type="text"
placeholder="Task Name"
@keydown.enter="submit()"
class="mt-1 block w-full"
required
autocomplete="taskName"
@keydown.enter="submit()" />
autocomplete="taskName" />
</div>
<div class="col-span-6 sm:col-span-4">
<ProjectDropdown :model-value="projectId"></ProjectDropdown>
<ProjectDropdown :modelValue="projectId"></ProjectDropdown>
</div>
</div>
<EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()"
v-model="estimatedTime"
@submit="submit()"></EstimatedTimeSection>
@submit="submit()"
v-model="estimatedTime"></EstimatedTimeSection>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>

View File

@@ -50,16 +50,16 @@ useFocus(taskNameInput, { initialValue: true });
v-model="taskBody.name"
type="text"
placeholder="Task Name"
@keydown.enter="submit()"
class="mt-1 block w-full"
required
autocomplete="taskName"
@keydown.enter="submit()" />
autocomplete="taskName" />
</div>
</div>
<EstimatedTimeSection
v-if="isAllowedToPerformPremiumAction()"
v-model="taskBody.estimated_time"
@submit="submit()"></EstimatedTimeSection>
@submit="submit()"
v-model="taskBody.estimated_time"></EstimatedTimeSection>
</template>
<template #footer>
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>

View File

@@ -21,30 +21,30 @@ const props = defineProps<{
<MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
<div class="min-w-[150px]">
<button
@click="emit('edit')"
v-if="canUpdateTasks()"
:aria-label="'Edit Task ' + props.task.name"
data-testid="task_edit"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('edit')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<PencilSquareIcon
class="w-5 text-icon-active"></PencilSquareIcon>
<span>Edit</span>
</button>
<button
@click="emit('done')"
v-if="canUpdateTasks()"
:aria-label="'Mark Task ' + props.task.name + ' as done'"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('done')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon>
<span v-if="props.task.is_done">Mark as active</span>
<span v-else>Mark as done</span>
</button>
<button
v-if="canDeleteTasks()"
@click="emit('delete')"
:aria-label="'Delete Task ' + props.task.name"
v-if="canDeleteTasks()"
data-testid="task_delete"
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
@click="emit('delete')">
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
<span>Delete</span>
</button>

View File

@@ -18,7 +18,7 @@ function getNameForItem(item: Task) {
<template>
<MultiselectDropdown
search-placeholder="Search for a Task..."
searchPlaceholder="Search for a Task..."
:items="tasks"
:get-key-from-item="getKeyFromItem"
:get-name-for-item="getNameForItem">

View File

@@ -19,8 +19,8 @@ const createTask = ref(false);
<template>
<TaskCreateModal
v-model:show="createTask"
:project-id="props.projectId"></TaskCreateModal>
:project-id="props.projectId"
v-model:show="createTask"></TaskCreateModal>
<div class="flow-root">
<div class="inline-block min-w-full align-middle">
<div
@@ -37,18 +37,18 @@ const createTask = ref(false);
">
<TaskTableHeading></TaskTableHeading>
<div
v-if="tasks.length === 0"
class="col-span-5 py-24 text-center">
class="col-span-5 py-24 text-center"
v-if="tasks.length === 0">
<PlusCircleIcon
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
<h3 class="text-white font-semibold">No tasks found</h3>
<p v-if="canCreateTasks()" class="pb-5">
<p class="pb-5" v-if="canCreateTasks()">
Create your first task now!
</p>
<SecondaryButton
v-if="canCreateTasks()"
:icon="PlusIcon"
@click="createTask = true"
:icon="PlusIcon"
>Create your First Task
</SecondaryButton>
</div>

View File

@@ -75,8 +75,8 @@ const showTaskEditModal = ref(false);
@delete="deleteTask"></TaskMoreOptionsDropdown>
</div>
<TaskEditModal
v-model:show="showTaskEditModal"
:task="task"></TaskEditModal>
:task="task"
v-model:show="showTaskEditModal"></TaskEditModal>
</TableRow>
</template>

View File

@@ -11,8 +11,8 @@ const showUpgradeModal = ref(false);
solidtime Professional.
</UpgradeModal>
<button
class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1"
@click.prevent="showUpgradeModal = true">
@click.prevent="showUpgradeModal = true"
class="inline-flex bg-secondary hover:bg-tertiary px-2 py-1 rounded border border-border-secondary hover:border-border-tertiary items-center space-x-1">
<LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon>
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
</button>

View File

@@ -50,12 +50,12 @@ const show = defineModel('show', { default: false });
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<PrimaryButton
v-if="
isBillingActivated() && canUpdateOrganization()
"
type="button"
class="mt-6"
:icon="CreditCardIcon">
:icon="CreditCardIcon"
v-if="
isBillingActivated() && canUpdateOrganization()
">
Go to Billing
</PrimaryButton>
</Link>

View File

@@ -32,8 +32,8 @@ const isRunningInDifferentOrganization = computed(() => {
<template>
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
<div
v-if="isRunningInDifferentOrganization"
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center">
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
v-if="isRunningInDifferentOrganization">
<div
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></div>
<div class="flex space-x-3 items-center w-full z-20 justify-center">
@@ -50,7 +50,7 @@ const isRunningInDifferentOrganization = computed(() => {
</div>
<TimeTrackerStartStop
:active="isActive"
size="base"
@changed="setActiveState"></TimeTrackerStartStop>
@changed="setActiveState"
size="base"></TimeTrackerStartStop>
</div>
</template>

View File

@@ -15,8 +15,8 @@ defineProps<{
<DashboardCard title="Last 7 Days" :icon="CalendarIcon">
<DayOverviewCardEntry
v-for="day in last7Days"
:key="day.date"
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
:key="day.date"
:date="day.date"
:history="day.history"
:duration="day.duration"></DayOverviewCardEntry>

View File

@@ -20,8 +20,8 @@ const props = defineProps<{
<DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon">
<RecentlyTrackedTasksCardEntry
v-for="lastTask in props.latestTasks"
:key="lastTask.id"
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
:key="lastTask.id"
:project_id="lastTask.project_id"
:task_id="lastTask.id"
:title="lastTask.name"></RecentlyTrackedTasksCardEntry>

View File

@@ -29,14 +29,14 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
:icon
:current
:href></NavigationSidebarLink>
<CollapsibleRoot v-else v-model:open="open"
<CollapsibleRoot v-model:open="open" v-else
><CollapsibleTrigger class="w-full group py-0.5">
<div
class="text-muted group-hover:text-white group-hover:bg-menu-active group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center justify-between">
<div class="flex items-center gap-x-2">
<component
:is="icon"
v-if="icon"
:is="icon"
:class="[
current
? 'text-icon-active'
@@ -59,8 +59,8 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
<CollapsibleContent class="CollapsibleContent">
<div class="px-3.5">
<ul
v-if="subItems"
class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5">
class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5"
v-if="subItems">
<li
v-for="subItem in subItems"
:key="subItem.title"

View File

@@ -19,8 +19,8 @@ defineProps<{
'group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center',
]">
<component
:is="icon"
v-if="icon"
:is="icon"
:class="[
current
? 'text-icon-active'

View File

@@ -5,8 +5,8 @@
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<Notification
v-for="notification in notifications"
:key="notification.uuid"
:type="notification.type"
:key="notification.uuid"
:title="notification.title"
:message="notification.message"></Notification>
</div>

View File

@@ -110,29 +110,29 @@ const { tags } = storeToRefs(useTagsStore());
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
<div class="relative">
<TimeTrackerRunningInDifferentOrganizationOverlay
@switchOrganization="switchToTimeEntryOrganization"
v-if="
isRunningInDifferentOrganization
"
@switch-organization="switchToTimeEntryOrganization"></TimeTrackerRunningInDifferentOrganizationOverlay>
"></TimeTrackerRunningInDifferentOrganizationOverlay>
<TimeTrackerControls
v-model:current-time-entry="currentTimeEntry"
v-model:live-timer="now"
:create-project
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:can-create-project="canCreateProjects()"
:create-client
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()"
:createClient
:clients
:tags
:tasks
:projects
:create-tag
:is-active
:createTag
:isActive
:currency="getOrganizationCurrencyString()"
@start-live-timer="startLiveTimer"
@stop-live-timer="stopLiveTimer"
@start-timer="setActiveState(true)"
@stop-timer="setActiveState(false)"
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
v-model:currentTimeEntry="currentTimeEntry"
v-model:liveTimer="now"
@startLiveTimer="startLiveTimer"
@stopLiveTimer="stopLiveTimer"
@startTimer="setActiveState(true)"
@stopTimer="setActiveState(false)"
@updateTimeEntry="updateTimeEntry"></TimeTrackerControls>
</div>
</template>

View File

@@ -12,7 +12,7 @@ function openDesktopGithubRepo() {
</script>
<template>
<div v-if="showReleaseInfo" class="py-4 hidden lg:block">
<div class="py-4 hidden lg:block" v-if="showReleaseInfo">
<div
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
<div class="flex items-start justify-between">
@@ -23,8 +23,8 @@ function openDesktopGithubRepo() {
</div>
<button>
<XMarkIcon
class="w-3.5 text-text-tertiary hover:text-text-secondary"
@click="showReleaseInfo = false"></XMarkIcon>
@click="showReleaseInfo = false"
class="w-3.5 text-text-tertiary hover:text-text-secondary"></XMarkIcon>
</button>
</div>
@@ -34,9 +34,9 @@ function openDesktopGithubRepo() {
now.
</p>
<SecondaryButton
@click="openDesktopGithubRepo"
size="small"
class="w-full text-center justify-center mt-1.5"
@click="openDesktopGithubRepo"
>Download now</SecondaryButton
>
</div>

View File

@@ -85,8 +85,8 @@ const page = usePage<{
class="border-b border-default-background-separator pb-2 flex justify-between">
<OrganizationSwitcher class="w-full"></OrganizationSwitcher>
<XMarkIcon
class="w-8 lg:hidden"
@click="showSidebarMenu = false"></XMarkIcon>
@click="showSidebarMenu = false"
class="w-8 lg:hidden"></XMarkIcon>
</div>
<div class="border-b border-default-background-separator">
<CurrentSidebarTimer></CurrentSidebarTimer>
@@ -162,8 +162,8 @@ const page = usePage<{
route('clients')
"></NavigationSidebarItem>
<NavigationSidebarItem
v-if="canViewMembers()"
title="Members"
v-if="canViewMembers()"
:icon="UserGroupIcon"
:current="route().current('members')"
:href="
@@ -238,8 +238,8 @@ const page = usePage<{
<div
class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center">
<Bars3Icon
class="w-7 text-muted"
@click="showSidebarMenu = !showSidebarMenu"></Bars3Icon>
@click="showSidebarMenu = !showSidebarMenu"
class="w-7 text-muted"></Bars3Icon>
<OrganizationSwitcher></OrganizationSwitcher>
</div>

View File

@@ -151,7 +151,7 @@ const page = usePage<{
</InputLabel>
</div>
<div v-if="page.props.newsletter_consent" class="mt-4">
<div class="mt-4" v-if="page.props.newsletter_consent">
<InputLabel for="newsletter_consent">
<div class="flex items-center">
<Checkbox

View File

@@ -74,7 +74,7 @@ function refreshDashboardData() {
<MainContainer
class="grid gap-5 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 pt-3 sm:pt-5 pb-4 sm:pb-6 border-b border-default-background-separator items-stretch">
<RecentlyTrackedTasksCard
:latest-tasks="props.latestTasks"></RecentlyTrackedTasksCard>
:latestTasks="props.latestTasks"></RecentlyTrackedTasksCard>
<LastSevenDaysCard
:last7-days="props.lastSevenDays"></LastSevenDaysCard>
<ActivityGraphCard
@@ -84,13 +84,13 @@ function refreshDashboardData() {
<TeamActivityCard
v-if="canViewMembers()"
class="flex lg:hidden xl:flex"
:latest-team-activity="
:latestTeamActivity="
props.latestTeamActivity
"></TeamActivityCard>
</MainContainer>
<MainContainer class="py-5">
<ThisWeekOverview
:weekly-project-overview="props.weeklyProjectOverview"
:weeklyProjectOverview="props.weeklyProjectOverview"
:total-weekly-billable-amount="props.totalWeeklyBillableAmount"
:total-weekly-billable-time="props.totalWeeklyBillableTime"
:total-weekly-time="props.totalWeeklyTime"

View File

@@ -52,8 +52,8 @@ function isActiveTab(tab: string) {
>Invite member</SecondaryButton
>
<MemberInviteModal
v-model:show="inviteMember"
:available-roles="availableRoles"
v-model:show="inviteMember"
@close="activeTab = 'invitations'"></MemberInviteModal>
</MainContainer>
<MemberTable v-if="activeTab === 'all'"></MemberTable>

View File

@@ -204,9 +204,9 @@ const page = usePage<{
<div class="col-span-6 sm:col-span-4">
<InputLabel for="timezone" value="Timezone" />
<select
name="timezone"
id="timezone"
v-model="form.timezone"
name="timezone"
required
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a Timezone</option>
@@ -225,9 +225,9 @@ const page = usePage<{
<div class="col-span-6 sm:col-span-4">
<InputLabel for="week_start" value="Start of the week" />
<select
name="week_start"
id="week_start"
v-model="form.week_start"
name="week_start"
required
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a week day</option>

View File

@@ -129,15 +129,15 @@ const shownTasks = computed(() => {
</nav>
<div>
<SecondaryButton
v-if="canCreateProjects()"
:icon="PencilSquareIcon"
@click="showEditProjectModal = true">
@click="showEditProjectModal = true"
v-if="canCreateProjects()">
Edit Project
</SecondaryButton>
<ProjectEditModal
v-if="project"
v-model:show="showEditProjectModal"
:original-project="project"></ProjectEditModal>
:originalProject="project"
v-model:show="showEditProjectModal"></ProjectEditModal>
</div>
</MainContainer>
<MainContainer>
@@ -168,8 +168,8 @@ const shownTasks = computed(() => {
>Create Task
</SecondaryButton>
<TaskCreateModal
v-model:show="createTask"
:project-id="projectId"></TaskCreateModal>
:project-id="projectId"
v-model:show="createTask"></TaskCreateModal>
</div>
</template>
</CardTitle>
@@ -188,11 +188,11 @@ const shownTasks = computed(() => {
Add Member
</SecondaryButton>
<ProjectMemberCreateModal
:project-id="projectId"
:existing-members="projectMembers"
v-model:show="
createProjectMember
"
:project-id="projectId"
:existing-members="projectMembers"></ProjectMemberCreateModal>
"></ProjectMemberCreateModal>
</template>
</CardTitle>
<Card>

View File

@@ -94,13 +94,13 @@ const showBillableRate = computed(() => {
>Create Project
</SecondaryButton>
<ProjectCreateModal
v-model:show="showCreateProjectModal"
:create-project
:enable-estimated-time="isAllowedToPerformPremiumAction"
:create-client
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction"
:createClient
:currency="getOrganizationCurrencyString()"
:clients="clients"
@submit="createProject"></ProjectCreateModal>
@submit="createProject"
v-model:show="showCreateProjectModal"></ProjectCreateModal>
</MainContainer>
<ProjectTable
:show-billable-rate="showBillableRate"

View File

@@ -281,7 +281,7 @@ const tableData = computed(() => {
class="overflow-hidden">
<ReportingExportModal
v-model:show="showExportModal"
:export-url="exportUrl"></ReportingExportModal>
:exportUrl="exportUrl"></ReportingExportModal>
<MainContainer
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
<div class="flex items-center space-x-3 sm:space-x-6">
@@ -292,7 +292,7 @@ const tableData = computed(() => {
<ReportingExportButton
:download="downloadExport"></ReportingExportButton>
<ReportSaveButton
:report-properties="reportProperties"></ReportSaveButton>
:reportProperties="reportProperties"></ReportSaveButton>
</div>
</MainContainer>
<div class="py-2.5 w-full border-b border-default-background-separator">
@@ -302,9 +302,9 @@ const tableData = computed(() => {
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
<div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown
v-model="selectedMembers"
@submit="updateReporting">
<template #trigger>
@submit="updateReporting"
v-model="selectedMembers">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedMembers.length"
:active="selectedMembers.length > 0"
@@ -313,9 +313,9 @@ const tableData = computed(() => {
</template>
</MemberMultiselectDropdown>
<ProjectMultiselectDropdown
v-model="selectedProjects"
@submit="updateReporting">
<template #trigger>
@submit="updateReporting"
v-model="selectedProjects">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedProjects.length"
:active="selectedProjects.length > 0"
@@ -324,9 +324,9 @@ const tableData = computed(() => {
</template>
</ProjectMultiselectDropdown>
<TaskMultiselectDropdown
v-model="selectedTasks"
@submit="updateReporting">
<template #trigger>
@submit="updateReporting"
v-model="selectedTasks">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedTasks.length"
:active="selectedTasks.length > 0"
@@ -335,20 +335,20 @@ const tableData = computed(() => {
</template>
</TaskMultiselectDropdown>
<ClientMultiselectDropdown
v-model="selectedClients"
@submit="updateReporting">
<template #trigger>
@submit="updateReporting"
v-model="selectedClients">
<template v-slot:trigger>
<ReportingFilterBadge
title="Clients"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ClientMultiselectDropdown>
<TagDropdown
@submit="updateReporting"
:createTag
v-model="selectedTags"
:create-tag
:tags="tags"
@submit="updateReporting">
<template #trigger>
:tags="tags">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedTags.length"
:active="selectedTags.length > 0"
@@ -358,6 +358,7 @@ const tableData = computed(() => {
</TagDropdown>
<SelectDropdown
@changed="updateReporting"
v-model="billable"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
@@ -374,9 +375,8 @@ const tableData = computed(() => {
label: 'Non Billable',
value: 'false',
},
]"
@changed="updateReporting">
<template #trigger>
]">
<template v-slot:trigger>
<ReportingFilterBadge
:active="billable !== null"
:title="
@@ -399,8 +399,8 @@ const tableData = computed(() => {
<MainContainer>
<div class="pt-10 w-full px-3 relative">
<ReportingChart
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
:grouped-data="
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
:groupedData="
aggregatedGraphTimeEntries?.grouped_data
"></ReportingChart>
</div>
@@ -413,18 +413,18 @@ const tableData = computed(() => {
class="text-sm flex text-white items-center space-x-3 font-medium px-6 border-b border-card-background-separator pb-3">
<span>Group by</span>
<ReportingGroupBySelect
v-model="group"
:group-by-options="groupByOptions"
@changed="updateTableReporting"></ReportingGroupBySelect>
@changed="updateTableReporting"
v-model="group"></ReportingGroupBySelect>
<span>and</span>
<ReportingGroupBySelect
v-model="subGroup"
:group-by-options="
groupByOptions.filter(
(el) => el.value !== group
)
"
@changed="updateTableReporting"></ReportingGroupBySelect>
@changed="updateTableReporting"
v-model="subGroup"></ReportingGroupBySelect>
</div>
<div
class="grid items-center"
@@ -473,8 +473,8 @@ const tableData = computed(() => {
</div>
</template>
<div
v-else
class="chart flex flex-col items-center justify-center py-12 col-span-3">
class="chart flex flex-col items-center justify-center py-12 col-span-3"
v-else>
<p class="text-lg text-white font-semibold">
No time entries found
</p>

View File

@@ -252,7 +252,7 @@ async function downloadExport(format: ExportFormat) {
class="overflow-hidden">
<ReportingExportModal
v-model:show="showExportModal"
:export-url="exportUrl"></ReportingExportModal>
:exportUrl="exportUrl"></ReportingExportModal>
<MainContainer
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
<div class="flex items-center space-x-3 sm:space-x-6">
@@ -270,9 +270,9 @@ async function downloadExport(format: ExportFormat) {
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
<div class="text-sm font-medium">Filters</div>
<MemberMultiselectDropdown
v-model="selectedMembers"
@submit="updateFilteredTimeEntries">
<template #trigger>
@submit="updateFilteredTimeEntries"
v-model="selectedMembers">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedMembers.length"
:active="selectedMembers.length > 0"
@@ -281,9 +281,9 @@ async function downloadExport(format: ExportFormat) {
</template>
</MemberMultiselectDropdown>
<ProjectMultiselectDropdown
v-model="selectedProjects"
@submit="updateFilteredTimeEntries">
<template #trigger>
@submit="updateFilteredTimeEntries"
v-model="selectedProjects">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedProjects.length"
:active="selectedProjects.length > 0"
@@ -292,9 +292,9 @@ async function downloadExport(format: ExportFormat) {
</template>
</ProjectMultiselectDropdown>
<TaskMultiselectDropdown
v-model="selectedTasks"
@submit="updateFilteredTimeEntries">
<template #trigger>
@submit="updateFilteredTimeEntries"
v-model="selectedTasks">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedTasks.length"
:active="selectedTasks.length > 0"
@@ -303,20 +303,20 @@ async function downloadExport(format: ExportFormat) {
</template>
</TaskMultiselectDropdown>
<ClientMultiselectDropdown
v-model="selectedClients"
@submit="updateFilteredTimeEntries">
<template #trigger>
@submit="updateFilteredTimeEntries"
v-model="selectedClients">
<template v-slot:trigger>
<ReportingFilterBadge
title="Clients"
:icon="FolderIcon"></ReportingFilterBadge>
</template>
</ClientMultiselectDropdown>
<TagDropdown
@submit="updateFilteredTimeEntries"
:createTag
v-model="selectedTags"
:create-tag
:tags="tags"
@submit="updateFilteredTimeEntries">
<template #trigger>
:tags="tags">
<template v-slot:trigger>
<ReportingFilterBadge
:count="selectedTags.length"
:active="selectedTags.length > 0"
@@ -326,6 +326,7 @@ async function downloadExport(format: ExportFormat) {
</TagDropdown>
<SelectDropdown
@changed="updateFilteredTimeEntries"
v-model="billable"
:get-key-from-item="(item) => item.value"
:get-name-for-item="(item) => item.label"
@@ -342,9 +343,8 @@ async function downloadExport(format: ExportFormat) {
label: 'Non Billable',
value: 'false',
},
]"
@changed="updateFilteredTimeEntries">
<template #trigger>
]">
<template v-slot:trigger>
<ReportingFilterBadge
:active="billable !== null"
:title="
@@ -366,9 +366,12 @@ async function downloadExport(format: ExportFormat) {
</div>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
:can-create-project="canCreateProjects()"
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
@submit="clearSelectionAndState"
:delete-selected="deleteSelected"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:all-selected="selectedTimeEntries.length === timeEntries.length"
:projects="projects"
:tasks="tasks"
@@ -384,37 +387,34 @@ async function downloadExport(format: ExportFormat) {
"
:create-project="createProject"
:create-client="createClient"
:create-tag="createTag"
@submit="clearSelectionAndState"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
:createTag="createTag"></TimeEntryMassActionRow>
<div class="w-full relative">
<div v-for="entry in timeEntries" :key="entry.id">
<TimeEntryRow
:selected="selectedTimeEntries.includes(entry)"
:can-create-project="canCreateProjects()"
:create-client
:create-project
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:create-tag
:update-time-entry
:on-start-stop-click="() => startTimeEntryFromExisting(entry)"
:delete-time-entry="() => deleteTimeEntries([entry])"
:currency="getOrganizationCurrencyString()"
:members="members"
show-date
show-member
:time-entry="entry"
@selected="selectedTimeEntries.push(entry)"
:canCreateProject="canCreateProjects()"
@unselected="
selectedTimeEntries = selectedTimeEntries.filter(
(item) => item.id !== entry.id
)
"></TimeEntryRow>
"
:createClient
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:projects="projects"
:tasks="tasks"
:tags="tags"
:clients
:createTag
:updateTimeEntry
:onStartStopClick="() => startTimeEntryFromExisting(entry)"
:deleteTimeEntry="() => deleteTimeEntries([entry])"
:currency="getOrganizationCurrencyString()"
:members="members"
showDate
showMember
:time-entry="entry"></TimeEntryRow>
</div>
<div v-if="timeEntries.length === 0">
<div class="text-center pt-12">
@@ -431,10 +431,10 @@ async function downloadExport(format: ExportFormat) {
</div>
<PaginationRoot
v-model:page="currentPage"
:total="totalPages"
:items-per-page="pageLimit"
class="flex justify-center items-center py-8"
v-model:page="currentPage"
:sibling-count="1"
show-edges>
<PaginationList
@@ -485,13 +485,13 @@ async function downloadExport(format: ExportFormat) {
</template>
<style lang="postcss">
.navigation-item {
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
}
.pagination-item {
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
}
.pagination-item[data-selected] {
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150;
}
</style>

View File

@@ -101,12 +101,12 @@ watch(currentPage, () => {
v-if="isBillingActivated() && canManageBilling()"
href="/billing">
<PrimaryButton
v-if="
isBillingActivated() && canUpdateOrganization()
"
type="button"
class="mt-6"
:icon="CreditCardIcon">
:icon="CreditCardIcon"
v-if="
isBillingActivated() && canUpdateOrganization()
">
Go to Billing
</PrimaryButton>
</Link>
@@ -120,10 +120,10 @@ watch(currentPage, () => {
<PaginationRoot
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
v-model:page="currentPage"
:total="totalPages"
:items-per-page="pageLimit"
class="flex justify-center items-center py-8"
v-model:page="currentPage"
:sibling-count="1"
show-edges>
<PaginationList
@@ -174,13 +174,13 @@ watch(currentPage, () => {
</template>
<style lang="postcss">
.navigation-item {
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
}
.pagination-item {
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
}
.pagination-item[data-selected] {
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150;
}
</style>

View File

@@ -151,8 +151,8 @@ function getGroupLabel(key: string) {
<MainContainer>
<div class="pt-10 w-full px-3 relative">
<ReportingChart
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
:grouped-data="
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
:groupedData="
aggregatedGraphTimeEntries?.grouped_data
"></ReportingChart>
</div>
@@ -217,8 +217,8 @@ function getGroupLabel(key: string) {
</div>
</template>
<div
v-else
class="chart flex flex-col items-center justify-center py-12 col-span-3">
class="chart flex flex-col items-center justify-center py-12 col-span-3"
v-else>
<p class="text-lg text-white font-semibold">
No time entries found
</p>

View File

@@ -31,9 +31,9 @@ async function createTag(tag: string) {
>Create Tag
</SecondaryButton>
<TagCreateModal
v-model:show="showCreateTagModal"
:create-tag="createTag"></TagCreateModal>
:createTag="createTag"
v-model:show="showCreateTagModal"></TagCreateModal>
</MainContainer>
<TagTable :create-tag="createTag"></TagTable>
<TagTable :createTag="createTag"></TagTable>
</AppLayout>
</template>

View File

@@ -191,9 +191,9 @@ const showResultModal = ref(false);
<div>
<InputLabel for="importType" value="Import Type" />
<select
name="importType"
id="importType"
v-model="importType"
name="importType"
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option :value="null" selected disabled>
Select an import type to get instructions...
@@ -206,8 +206,8 @@ const showResultModal = ref(false);
</option>
</select>
<div
v-if="currentImporterDescription"
class="py-3 text-white">
class="py-3 text-white"
v-if="currentImporterDescription">
<div class="font-semibold text-muted py-1">
Instructions:
</div>
@@ -233,12 +233,12 @@ const showResultModal = ref(false);
>Upload a Toggl/Clockify Export</span
>
<input
id="file-upload"
ref="importFile"
id="file-upload"
name="file-upload"
v-on:change="updateFiles"
type="file"
class="sr-only"
@change="updateFiles" />
class="sr-only" />
</label>
</div>
<p class="text-xs leading-5 text-muted">

View File

@@ -62,10 +62,10 @@ function checkForConfirmationModal() {
<template #form>
<OrganizationBillableRateModal
v-model:show="showConfirmationModal"
@submit="submit"
:new-billable-rate="
organizationBody.billable_rate
"
@submit="submit"></OrganizationBillableRateModal>
"></OrganizationBillableRateModal>
<!-- Organization Owner Information -->
<div class="col-span-6">
<div class="col-span-6 sm:col-span-4">
@@ -75,8 +75,8 @@ function checkForConfirmationModal() {
value="Organization Billable Rate" />
<BillableRateInput
v-if="organization"
v-model="organizationBody.billable_rate"
:currency="getOrganizationCurrencyString()"
v-model="organizationBody.billable_rate"
name="organizationBillableRate"></BillableRateInput>
</div>
</div>
@@ -86,10 +86,10 @@ function checkForConfirmationModal() {
<div class="flex items-center space-x-2">
<Checkbox
v-if="organization"
id="organizationShowBillableRatesToEmployees"
v-model:checked="
organizationBody.employees_can_see_billable_rates
"></Checkbox>
"
id="organizationShowBillableRatesToEmployees"></Checkbox>
<InputLabel
for="organizationShowBillableRatesToEmployees"
value="Show Billable Rates to Employees" />

View File

@@ -89,9 +89,9 @@ const updateTeamName = () => {
<div class="col-span-6 sm:col-span-4">
<InputLabel for="currency" value="Currency" />
<select
name="currency"
id="currency"
v-model="form.currency"
name="currency"
:disabled="!permissions.canUpdateTeam"
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
<option value="" disabled>Select a currency</option>

View File

@@ -120,16 +120,16 @@ function deleteSelected() {
<template>
<TimeEntryCreateModal
v-model:show="showManualTimeEntryModal"
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:create-project="createProject"
:create-client="createClient"
:create-tag="createTag"
:create-time-entry="createTimeEntry"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:createProject="createProject"
:createClient="createClient"
:createTag="createTag"
:createTimeEntry="createTimeEntry"
:projects
:tasks
:tags
:clients></TimeEntryCreateModal>
:clients
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
<AppLayout title="Dashboard" data-testid="time_view">
<MainContainer
class="pt-5 lg:pt-8 pb-4 lg:pb-6 border-b border-default-background-separator">
@@ -141,8 +141,8 @@ function deleteSelected() {
<div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center">
<SecondaryButton
class="w-full text-center flex justify-center"
:icon="PlusIcon"
@click="showManualTimeEntryModal = true"
:icon="PlusIcon"
>Manual time entry
</SecondaryButton>
</div>
@@ -150,9 +150,12 @@ function deleteSelected() {
</MainContainer>
<TimeEntryMassActionRow
:selected-time-entries="selectedTimeEntries"
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:can-create-project="canCreateProjects()"
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()"
@submit="clearSelectionAndState"
:all-selected="selectedTimeEntries.length === timeEntries.length"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"
:delete-selected="deleteSelected"
:projects="projects"
:tasks="tasks"
@@ -168,26 +171,23 @@ function deleteSelected() {
"
:create-project="createProject"
:create-client="createClient"
:create-tag="createTag"
@submit="clearSelectionAndState"
@select-all="selectedTimeEntries = [...timeEntries]"
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
:createTag="createTag"></TimeEntryMassActionRow>
<TimeEntryGroupedTable
v-model:selected="selectedTimeEntries"
:create-project
:enable-estimated-time="isAllowedToPerformPremiumAction()"
:can-create-project="canCreateProjects()"
:createProject
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
:canCreateProject="canCreateProjects()"
:clients
:create-client
:update-time-entry
:update-time-entries
:delete-time-entries
:create-time-entry="startTimeEntry"
:create-tag
:createClient
:updateTimeEntry
:updateTimeEntries
:deleteTimeEntries
:createTimeEntry="startTimeEntry"
:createTag
:projects="projects"
:tasks="tasks"
:currency="getOrganizationCurrencyString()"
:time-entries="timeEntries"
:timeEntries="timeEntries"
:tags="tags"></TimeEntryGroupedTable>
<div v-if="timeEntries.length === 0" class="text-center pt-12">
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>

File diff suppressed because it is too large Load Diff

View File

@@ -57,13 +57,13 @@
"peerDependencies": {
"@floating-ui/vue": "^1.1.4",
"@heroicons/vue": "^2.1.5",
"@vueuse/core": "^12.5.0",
"@vueuse/core": "^10.11.0",
"@zodios/core": "^10.9.6",
"dayjs": "^1.11.13",
"parse-duration": "^2.0.1",
"parse-duration": "^1.1.0",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.1.0",
"vue": "^3.5.0",
"vue-tsc": "^2.2.0"
"vue": "^3.4.38",
"vue-tsc": "^2.0.29"
}
}

View File

@@ -4,11 +4,11 @@ import { computed } from 'vue';
const props = withDefaults(
defineProps<{
size?: 'base' | 'large' | 'xlarge';
tag?: string;
size: 'base' | 'large' | 'xlarge';
tag: string;
class?: string;
color?: string;
border?: boolean;
color: string;
border: boolean;
}>(),
{
size: 'base',
@@ -30,13 +30,6 @@ const borderClasses = computed(() => {
}
return '';
});
const tagClasses = computed(() => {
if (props.tag === 'button') {
return 'hover:bg-tertiary';
}
return '';
});
</script>
<template>
@@ -44,10 +37,9 @@ const tagClasses = computed(() => {
:is="tag"
:class="
twMerge(
tagClasses,
badgeClasses[size],
borderClasses,
'rounded transition inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
'rounded inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80',
props.class
)
">

View File

@@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge';
const props = withDefaults(
defineProps<{
type?: HtmlButtonType;
type: HtmlButtonType;
icon?: Component;
loading?: boolean;
loading: boolean;
}>(),
{
type: 'submit',
@@ -21,15 +21,15 @@ const props = withDefaults(
<button
:type="type"
:disabled="loading"
class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
<span
:class="
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
">
<LoadingSpinner v-if="loading"></LoadingSpinner>
<component
:is="props.icon"
v-if="props.icon && !loading"
:is="props.icon"
class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
<span>
<slot />

View File

@@ -6,10 +6,10 @@ import LoadingSpinner from '../LoadingSpinner.vue';
const props = withDefaults(
defineProps<{
type?: HtmlButtonType;
type: HtmlButtonType;
icon?: Component;
size?: 'small' | 'base';
loading?: boolean;
size: 'small' | 'base';
loading: boolean;
class?: string;
}>(),
{
@@ -31,7 +31,7 @@ const sizeClasses = {
:disabled="loading"
:class="
twMerge(
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:outline-none focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring focus:border-transparent disabled:opacity-25 ease-in-out',
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:border-input-border-active focus:outline-none focus:ring-0 disabled:opacity-25 ease-in-out',
sizeClasses[props.size],
props.class
)
@@ -42,8 +42,8 @@ const sizeClasses = {
">
<LoadingSpinner v-if="loading"></LoadingSpinner>
<component
:is="props.icon"
v-if="props.icon && !loading"
:is="props.icon"
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
<span>
<slot />

View File

@@ -12,8 +12,8 @@ defineProps<{
<h3
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
<component
:is="icon"
v-if="icon"
:is="icon"
class="w-5 lg:w-6 text-icon-default"></component>
<span>
{{ title }}

Some files were not shown because too many files have changed in this diff Show More