mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 08:12:17 +01:00
Compare commits
8 Commits
feature/ba
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
857dc7f4a2 | ||
|
|
b71ac06cb6 | ||
|
|
0e8f9c58fd | ||
|
|
71ec1e9e0f | ||
|
|
a8318a458e | ||
|
|
84622c3fcc | ||
|
|
1a03637e31 | ||
|
|
cd207c6173 |
@@ -1,13 +0,0 @@
|
|||||||
/* 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'],
|
|
||||||
}
|
|
||||||
@@ -109,7 +109,7 @@ services:
|
|||||||
- sail
|
- sail
|
||||||
- reverse-proxy
|
- reverse-proxy
|
||||||
playwright:
|
playwright:
|
||||||
image: mcr.microsoft.com/playwright:v1.46.1-jammy
|
image: mcr.microsoft.com/playwright:v1.50.0-jammy
|
||||||
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
|
command: ['npx', 'playwright', 'test', '--ui-port=8080', '--ui-host=0.0.0.0']
|
||||||
working_dir: /src
|
working_dir: /src
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
|
|||||||
'time_entry_range_selector'
|
'time_entry_range_selector'
|
||||||
);
|
);
|
||||||
await timeEntryRangeElement.click();
|
await timeEntryRangeElement.click();
|
||||||
await page.getByTestId('time_picker_input').first().fill('1');
|
await page.getByTestId('time_entry_range_start').first().fill('1');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForResponse(async (response) => {
|
page.waitForResponse(async (response) => {
|
||||||
return (
|
return (
|
||||||
@@ -204,10 +204,7 @@ test('test that updating a the start of an existing time entry in the overview w
|
|||||||
(await response.json()).data.end !== null
|
(await response.json()).data.end !== null
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
page
|
page.getByTestId('time_entry_range_end').press('Enter'),
|
||||||
.getByTestId('time_entry_range_end')
|
|
||||||
.getByTestId('time_picker_input')
|
|
||||||
.press('Enter'),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ test('test that starting and updating the time while running works', async ({
|
|||||||
JSON.stringify([])
|
JSON.stringify([])
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
page.getByTestId('time_entry_time').press('Tab'),
|
page.getByTestId('time_entry_time').press('Enter'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);
|
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);
|
||||||
|
|||||||
36
eslint.config.mjs
Normal file
36
eslint.config.mjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
3424
package-lock.json
generated
3424
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -4,53 +4,60 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
"lint": "eslint resources/js",
|
||||||
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore resources/js",
|
"lint:fix": "eslint --fix resources/js",
|
||||||
"type-check": "vue-tsc --noEmit",
|
"type-check": "vue-tsc --noEmit",
|
||||||
"test:e2e": "rm -rf test-results/.auth && npx playwright test",
|
"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"
|
"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": {
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.19.0",
|
||||||
"@inertiajs/vue3": "^1.0.0",
|
"@inertiajs/vue3": "^1.0.0",
|
||||||
"@playwright/test": "^1.41.1",
|
"@playwright/test": "^1.41.1",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@types/node": "^20.11.5",
|
"@types/node": "^22.10.10",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^1.6.4",
|
"axios": "^1.6.4",
|
||||||
"eslint-plugin-unused-imports": "^3.1.0",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"laravel-vite-plugin": "^1.0.0",
|
"laravel-vite-plugin": "^1.0.0",
|
||||||
"openapi-zod-client": "^1.16.2",
|
"openapi-zod-client": "^1.16.2",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"postcss-nesting": "^12.1.5",
|
"postcss-nesting": "^12.1.5",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.0.0",
|
"vite": "^6.0.11",
|
||||||
"vite-plugin-checker": "^0.7.2",
|
"vite-plugin-checker": "^0.8.0",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.5.0",
|
||||||
"vue-tsc": "^2.0.28"
|
"vue-tsc": "^2.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/core": "^1.6.0",
|
"@floating-ui/core": "^1.6.0",
|
||||||
"@floating-ui/vue": "^1.0.6",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
"@heroicons/vue": "^2.1.1",
|
"@heroicons/vue": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.7.0",
|
"@rushstack/eslint-patch": "^1.10.5",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tanstack/vue-query": "^5.56.2",
|
"@tanstack/vue-query": "^5.56.2",
|
||||||
"@tanstack/vue-query-devtools": "^5.58.0",
|
"@tanstack/vue-query-devtools": "^5.58.0",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^14.3.0",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^12.5.0",
|
||||||
"@vueuse/integrations": "^11.1.0",
|
"@vueuse/integrations": "^12.5.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"focus-trap": "^7.6.0",
|
"focus-trap": "^7.6.0",
|
||||||
"parse-duration": "^1.1.0",
|
"parse-duration": "^2.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"radix-vue": "^1.9.6",
|
"radix-vue": "^1.9.6",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"vue-echarts": "^6.7.2"
|
"vue-echarts": "^7.0.3"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"vite-plugin-checker": {
|
||||||
|
"vue-tsc": "$vue-tsc"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const showBlackFridayBanner = computed(() => {
|
|||||||
<span>Upgrade now</span>
|
<span>Upgrade now</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<button @click="hideBlackFridayBanner = true" class="p-1">
|
<button class="p-1" @click="hideBlackFridayBanner = true">
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
@@ -142,7 +142,7 @@ const showBlackFridayBanner = computed(() => {
|
|||||||
<span>Upgrade now</span>
|
<span>Upgrade now</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<button @click="hideTrialBanner = true" class="p-1">
|
<button class="p-1" @click="hideTrialBanner = true">
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
@@ -174,7 +174,7 @@ const showBlackFridayBanner = computed(() => {
|
|||||||
<span>Upgrade now</span>
|
<span>Upgrade now</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<button @click="hideBlockedBanner = true" class="p-1">
|
<button class="p-1" @click="hideBlockedBanner = true">
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
@@ -206,7 +206,7 @@ const showBlackFridayBanner = computed(() => {
|
|||||||
<span>Upgrade now</span>
|
<span>Upgrade now</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<button @click="hideFreeUpgradeBanner = true" class="p-1">
|
<button class="p-1" @click="hideFreeUpgradeBanner = true">
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
class="w-4 opacity-50 hover:opacity-100"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
v-model="client.name"
|
v-model="client.name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Client Name"
|
placeholder="Client Name"
|
||||||
@keydown.enter="submit"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="clientName" />
|
autocomplete="clientName"
|
||||||
|
@keydown.enter="submit" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
v-model="clientBody.name"
|
v-model="clientBody.name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Client Name"
|
placeholder="Client Name"
|
||||||
@keydown.enter="submit"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="clientName" />
|
autocomplete="clientName"
|
||||||
|
@keydown.enter="submit" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -23,28 +23,28 @@ const props = defineProps<{
|
|||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
<button
|
<button
|
||||||
v-if="canUpdateClients()"
|
v-if="canUpdateClients()"
|
||||||
@click="emit('edit')"
|
|
||||||
:aria-label="'Edit Client ' + props.client.name"
|
:aria-label="'Edit Client ' + props.client.name"
|
||||||
data-testid="client_edit"
|
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">
|
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')">
|
||||||
<PencilSquareIcon
|
<PencilSquareIcon
|
||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('archive')"
|
|
||||||
v-if="canUpdateClients()"
|
v-if="canUpdateClients()"
|
||||||
:aria-label="'Archive Client ' + props.client.name"
|
: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">
|
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')">
|
||||||
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
||||||
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
<span>{{ client.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDeleteClients()"
|
v-if="canDeleteClients()"
|
||||||
@click="emit('delete')"
|
|
||||||
:aria-label="'Delete Client ' + props.client.name"
|
:aria-label="'Delete Client ' + props.client.name"
|
||||||
data-testid="client_delete"
|
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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Client) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MultiselectDropdown
|
<MultiselectDropdown
|
||||||
searchPlaceholder="Search for a Client..."
|
search-placeholder="Search for a Client..."
|
||||||
:items="clients"
|
:items="clients"
|
||||||
:get-key-from-item="getKeyFromItem"
|
:get-key-from-item="getKeyFromItem"
|
||||||
:get-name-for-item="getNameForItem">
|
:get-name-for-item="getNameForItem">
|
||||||
|
|||||||
@@ -25,18 +25,18 @@ const createClient = ref(false);
|
|||||||
style="grid-template-columns: 1fr 150px 200px 80px">
|
style="grid-template-columns: 1fr 150px 200px 80px">
|
||||||
<ClientTableHeading></ClientTableHeading>
|
<ClientTableHeading></ClientTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-2 py-24 text-center"
|
v-if="clients.length === 0"
|
||||||
v-if="clients.length === 0">
|
class="col-span-2 py-24 text-center">
|
||||||
<UserCircleIcon
|
<UserCircleIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
|
class="w-8 text-icon-default inline pb-2"></UserCircleIcon>
|
||||||
<h3 class="text-white font-semibold">No clients found</h3>
|
<h3 class="text-white font-semibold">No clients found</h3>
|
||||||
<p class="pb-5" v-if="canCreateClients()">
|
<p v-if="canCreateClients()" class="pb-5">
|
||||||
Create your first client now!
|
Create your first client now!
|
||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateClients()"
|
v-if="canCreateClients()"
|
||||||
@click="createClient = true"
|
|
||||||
:icon="PlusIcon as Component"
|
:icon="PlusIcon as Component"
|
||||||
|
@click="createClient = true"
|
||||||
>Create your First Client
|
>Create your First Client
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const showEditModal = ref(false);
|
|||||||
<template>
|
<template>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<ClientEditModal
|
<ClientEditModal
|
||||||
:client="client"
|
v-model:show="showEditModal"
|
||||||
v-model:show="showEditModal"></ClientEditModal>
|
:client="client"></ClientEditModal>
|
||||||
<div
|
<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">
|
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>
|
<span>
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ const emit = defineEmits<{
|
|||||||
<template>
|
<template>
|
||||||
<MoreOptionsDropdown label="Actions for the invitation">
|
<MoreOptionsDropdown label="Actions for the invitation">
|
||||||
<button
|
<button
|
||||||
@click="emit('resend')"
|
|
||||||
data-testid="invitation_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">
|
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')">
|
||||||
<ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon>
|
<ArrowPathIcon class="w-5 text-icon-active"></ArrowPathIcon>
|
||||||
<span>Resend Invitation</span>
|
<span>Resend Invitation</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="emit('delete')"
|
|
||||||
data-testid="invitation_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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BillableRateModal
|
<BillableRateModal
|
||||||
@submit="$emit('submit')"
|
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
v-model:saving="saving"
|
v-model:saving="saving"
|
||||||
title="Update Member Billable Rate">
|
title="Update Member Billable Rate"
|
||||||
|
@submit="$emit('submit')">
|
||||||
<p class="py-1 text-center">
|
<p class="py-1 text-center">
|
||||||
The billable rate of {{ memberName }} will be updated to
|
The billable rate of {{ memberName }} will be updated to
|
||||||
<strong>{{
|
<strong>{{
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ const model = defineModel<string>({
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
hiddenMembers: ProjectMember[];
|
hiddenMembers?: ProjectMember[];
|
||||||
disabled: boolean;
|
disabled?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
hiddenMembers: () => [] as ProjectMember[],
|
hiddenMembers: () => [] as ProjectMember[],
|
||||||
@@ -76,7 +76,7 @@ const currentValue = computed(() => {
|
|||||||
:items="filteredMembers"
|
:items="filteredMembers"
|
||||||
:get-key-from-item="(member) => member.id"
|
:get-key-from-item="(member) => member.id"
|
||||||
:get-name-for-item="(member) => member.name">
|
:get-name-for-item="(member) => member.name">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<Badge
|
<Badge
|
||||||
tag="button"
|
tag="button"
|
||||||
class="flex w-full text-base text-left space-x-3 px-3 text-text-secondary font-normal cursor py-1.5">
|
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">
|
<div v-if="currentValue" class="flex-1 truncate">
|
||||||
{{ currentValue }}
|
{{ currentValue }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1" v-else>Select a member...</div>
|
<div v-else class="flex-1">Select a member...</div>
|
||||||
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
|
<ChevronDownIcon class="w-4 text-muted"></ChevronDownIcon>
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -108,11 +108,11 @@ const roleDescription = computed(() => {
|
|||||||
v-model:saving="saving"
|
v-model:saving="saving"
|
||||||
v-model:show="showBillableRateModal"
|
v-model:show="showBillableRateModal"
|
||||||
:member-name="member.name"
|
:member-name="member.name"
|
||||||
:newBillableRate="memberBody.billable_rate"
|
:new-billable-rate="memberBody.billable_rate"
|
||||||
@submit="submitBillableRate"></MemberBillableRateModal>
|
@submit="submitBillableRate"></MemberBillableRateModal>
|
||||||
<MemberOwnershipTransferConfirmModal
|
<MemberOwnershipTransferConfirmModal
|
||||||
:member-name="member.name"
|
|
||||||
v-model:show="showOwnershipTransferConfirmModal"
|
v-model:show="showOwnershipTransferConfirmModal"
|
||||||
|
:member-name="member.name"
|
||||||
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
@submit="submit"></MemberOwnershipTransferConfirmModal>
|
||||||
<DialogModal closeable :show="show" @close="show = false">
|
<DialogModal closeable :show="show" @close="show = false">
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -127,9 +127,9 @@ const roleDescription = computed(() => {
|
|||||||
<div>
|
<div>
|
||||||
<InputLabel for="role" value="Role" />
|
<InputLabel for="role" value="Role" />
|
||||||
<MemberRoleSelect
|
<MemberRoleSelect
|
||||||
|
v-model="memberBody.role"
|
||||||
class="mt-2"
|
class="mt-2"
|
||||||
name="role"
|
name="role"></MemberRoleSelect>
|
||||||
v-model="memberBody.role"></MemberRoleSelect>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 text-xs flex items-center pt-6">
|
<div class="flex-1 text-xs flex items-center pt-6">
|
||||||
<p>{{ roleDescription }}</p>
|
<p>{{ roleDescription }}</p>
|
||||||
@@ -140,28 +140,28 @@ const roleDescription = computed(() => {
|
|||||||
<div>
|
<div>
|
||||||
<InputLabel for="billableType" value="Billable" />
|
<InputLabel for="billableType" value="Billable" />
|
||||||
<MemberBillableSelect
|
<MemberBillableSelect
|
||||||
class="mt-2"
|
|
||||||
name="billableType"
|
|
||||||
v-model="
|
v-model="
|
||||||
billableRateSelect
|
billableRateSelect
|
||||||
"></MemberBillableSelect>
|
"
|
||||||
|
class="mt-2"
|
||||||
|
name="billableType"></MemberBillableSelect>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex-1"
|
v-if="billableRateSelect === 'custom-rate'"
|
||||||
v-if="billableRateSelect === 'custom-rate'">
|
class="flex-1">
|
||||||
<InputLabel
|
<InputLabel
|
||||||
for="memberBillableRate"
|
for="memberBillableRate"
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
value="Billable Rate" />
|
value="Billable Rate" />
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
|
v-model="
|
||||||
|
memberBody.billable_rate
|
||||||
|
"
|
||||||
focus
|
focus
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
@keydown.enter="saveWithChecks()"
|
|
||||||
name="memberBillableRate"
|
name="memberBillableRate"
|
||||||
v-model="
|
@keydown.enter="saveWithChecks()"></BillableRateInput>
|
||||||
memberBody.billable_rate
|
|
||||||
"></BillableRateInput>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,11 +112,11 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
v-if="isBillingActivated() && canManageBilling()"
|
v-if="isBillingActivated() && canManageBilling()"
|
||||||
href="/billing">
|
href="/billing">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
|
||||||
class="mt-6"
|
|
||||||
v-if="
|
v-if="
|
||||||
isBillingActivated() && canUpdateOrganization()
|
isBillingActivated() && canUpdateOrganization()
|
||||||
">
|
"
|
||||||
|
type="button"
|
||||||
|
class="mt-6">
|
||||||
<CreditCardIcon class="w-5 h-5 me-2" />
|
<CreditCardIcon class="w-5 h-5 me-2" />
|
||||||
Go to Billing
|
Go to Billing
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
@@ -128,15 +128,15 @@ useFocus(clientNameInput, { initialValue: true });
|
|||||||
<InputLabel for="email" value="Email" />
|
<InputLabel for="email" value="Email" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
|
||||||
ref="memberEmailInput"
|
ref="memberEmailInput"
|
||||||
v-model="addTeamMemberForm.email"
|
v-model="addTeamMemberForm.email"
|
||||||
|
name="email"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Member Email"
|
placeholder="Member Email"
|
||||||
@keydown.enter="submit"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="memberName" />
|
autocomplete="memberName"
|
||||||
|
@keydown.enter="submit" />
|
||||||
<InputError :message="errors.email" class="mt-2" />
|
<InputError :message="errors.email" class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ const props = defineProps<{
|
|||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
<button
|
<button
|
||||||
v-if="canUpdateMembers()"
|
v-if="canUpdateMembers()"
|
||||||
@click="emit('edit')"
|
|
||||||
:aria-label="'Edit Member ' + props.member.name"
|
: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">
|
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')">
|
||||||
<PencilSquareIcon
|
<PencilSquareIcon
|
||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDeleteMembers()"
|
v-if="canDeleteMembers()"
|
||||||
@click="emit('delete')"
|
|
||||||
:aria-label="'Delete Member ' + props.member.name"
|
:aria-label="'Delete Member ' + props.member.name"
|
||||||
data-testid="member_delete"
|
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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Member) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MultiselectDropdown
|
<MultiselectDropdown
|
||||||
searchPlaceholder="Search for a Member..."
|
search-placeholder="Search for a Member..."
|
||||||
:items="members"
|
:items="members"
|
||||||
:get-key-from-item="getKeyFromItem"
|
:get-key-from-item="getKeyFromItem"
|
||||||
:get-name-for-item="getNameForItem">
|
:get-name-for-item="getNameForItem">
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ const emit = defineEmits<{
|
|||||||
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel</SecondaryButton>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
class="ms-3"
|
class="ms-3"
|
||||||
@click="emit('submit')"
|
|
||||||
:class="{ 'opacity-25': saving }"
|
:class="{ 'opacity-25': saving }"
|
||||||
:disabled="saving">
|
:disabled="saving"
|
||||||
|
@click="emit('submit')">
|
||||||
Confirm Transfer
|
Confirm Transfer
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ async function invitePlaceholder(id: string) {
|
|||||||
member.is_placeholder === true &&
|
member.is_placeholder === true &&
|
||||||
canInvitePlaceholderMembers()
|
canInvitePlaceholderMembers()
|
||||||
"
|
"
|
||||||
@click="invitePlaceholder(member.id)"
|
|
||||||
size="small"
|
size="small"
|
||||||
|
@click="invitePlaceholder(member.id)"
|
||||||
>Invite</SecondaryButton
|
>Invite</SecondaryButton
|
||||||
>
|
>
|
||||||
<MemberMoreOptionsDropdown
|
<MemberMoreOptionsDropdown
|
||||||
@@ -99,8 +99,8 @@ async function invitePlaceholder(id: string) {
|
|||||||
@delete="removeMember"></MemberMoreOptionsDropdown>
|
@delete="removeMember"></MemberMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
<MemberEditModal
|
<MemberEditModal
|
||||||
:member="member"
|
v-model:show="showEditMemberModal"
|
||||||
v-model:show="showEditMemberModal"></MemberEditModal>
|
:member="member"></MemberEditModal>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@
|
|||||||
<div class="ml-4 flex flex-shrink-0">
|
<div class="ml-4 flex flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@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"
|
||||||
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">
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
|
<XMarkIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BillableRateModal
|
<BillableRateModal
|
||||||
@submit="$emit('submit')"
|
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
v-model:saving="saving"
|
v-model:saving="saving"
|
||||||
title="Update Organization Billable Rate">
|
title="Update Organization Billable Rate"
|
||||||
|
@submit="$emit('submit')">
|
||||||
<p class="py-0.5 text-center">
|
<p class="py-0.5 text-center">
|
||||||
The organization billable rate will be updated to
|
The organization billable rate will be updated to
|
||||||
<strong>{{
|
<strong>{{
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const shownProjects = computed(() => {
|
|||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
border: boolean;
|
border?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
border: true,
|
border: true,
|
||||||
@@ -123,17 +123,17 @@ function updateValue(project: Project) {
|
|||||||
<template #content>
|
<template #content>
|
||||||
<ComboboxRoot
|
<ComboboxRoot
|
||||||
:open="open"
|
:open="open"
|
||||||
:modelValue="currentProject"
|
:model-value="currentProject"
|
||||||
@update:modelValue="updateValue"
|
:search-term="searchValue"
|
||||||
@update:searchTerm="(e) => console.log(e)"
|
class="relative"
|
||||||
:searchTerm="searchValue"
|
@update:model-value="updateValue"
|
||||||
class="relative">
|
@update:search-term="(e) => console.log(e)">
|
||||||
<ComboboxAnchor>
|
<ComboboxAnchor>
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
@keydown.enter="addProjectIfNoneExists"
|
|
||||||
ref="searchInput"
|
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"
|
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..." />
|
placeholder="Search for a project..."
|
||||||
|
@keydown.enter="addProjectIfNoneExists" />
|
||||||
</ComboboxAnchor>
|
</ComboboxAnchor>
|
||||||
<ComboboxContent>
|
<ComboboxContent>
|
||||||
<ComboboxViewport
|
<ComboboxViewport
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ async function submitBillableRate() {
|
|||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<InputLabel for="color" value="Color" />
|
<InputLabel for="color" value="Color" />
|
||||||
<ProjectColorSelector
|
<ProjectColorSelector
|
||||||
class="mt-1"
|
v-model="project.color"
|
||||||
v-model="project.color"></ProjectColorSelector>
|
class="mt-1"></ProjectColorSelector>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
@@ -102,18 +102,18 @@ async function submitBillableRate() {
|
|||||||
v-model="project.name"
|
v-model="project.name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Project Name"
|
placeholder="Project Name"
|
||||||
@keydown.enter="submit()"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="projectName" />
|
autocomplete="projectName"
|
||||||
|
@keydown.enter="submit()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<InputLabel for="client" value="Client" />
|
<InputLabel for="client" value="Client" />
|
||||||
<ClientDropdown
|
<ClientDropdown
|
||||||
:createClient
|
v-model="project.client_id"
|
||||||
|
:create-client
|
||||||
:clients="clients"
|
:clients="clients"
|
||||||
class="mt-1"
|
class="mt-1">
|
||||||
v-model="project.client_id">
|
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<Badge
|
<Badge
|
||||||
class="bg-input-background cursor-pointer hover:bg-tertiary"
|
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 class="lg:grid grid-cols-2 gap-12">
|
||||||
<div>
|
<div>
|
||||||
<ProjectEditBillableSection
|
<ProjectEditBillableSection
|
||||||
@submit="submit"
|
v-model:is-billable="project.is_billable"
|
||||||
:currency="getOrganizationCurrencyString()"
|
v-model:billable-rate="
|
||||||
v-model:isBillable="project.is_billable"
|
|
||||||
v-model:billableRate="
|
|
||||||
project.billable_rate
|
project.billable_rate
|
||||||
"></ProjectEditBillableSection>
|
"
|
||||||
|
:currency="getOrganizationCurrencyString()"
|
||||||
|
@submit="submit"></ProjectEditBillableSection>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<EstimatedTimeSection
|
<EstimatedTimeSection
|
||||||
v-if="isAllowedToPerformPremiumAction()"
|
v-if="isAllowedToPerformPremiumAction()"
|
||||||
@submit="submit()"
|
v-model="project.estimated_time"
|
||||||
v-model="project.estimated_time"></EstimatedTimeSection>
|
@submit="submit()"></EstimatedTimeSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,9 +163,9 @@ async function submitBillableRate() {
|
|||||||
<ProjectBillableRateModal
|
<ProjectBillableRateModal
|
||||||
v-model:show="showBillableRateModal"
|
v-model:show="showBillableRateModal"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
@submit="submitBillableRate"
|
|
||||||
:new-billable-rate="project.billable_rate"
|
:new-billable-rate="project.billable_rate"
|
||||||
:project-name="project.name"></ProjectBillableRateModal>
|
:project-name="project.name"
|
||||||
|
@submit="submitBillableRate"></ProjectBillableRateModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -21,29 +21,29 @@ const props = defineProps<{
|
|||||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
|
<MoreOptionsDropdown :label="'Actions for Project ' + props.project.name">
|
||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('edit')"
|
|
||||||
v-if="canUpdateProjects()"
|
v-if="canUpdateProjects()"
|
||||||
:aria-label="'Edit Project ' + props.project.name"
|
:aria-label="'Edit Project ' + props.project.name"
|
||||||
data-testid="project_edit"
|
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">
|
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')">
|
||||||
<PencilSquareIcon
|
<PencilSquareIcon
|
||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('archive')"
|
|
||||||
v-if="canUpdateProjects()"
|
v-if="canUpdateProjects()"
|
||||||
:aria-label="'Archive Project ' + props.project.name"
|
: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">
|
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')">
|
||||||
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
<ArchiveBoxIcon class="w-5 text-icon-active"></ArchiveBoxIcon>
|
||||||
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
<span>{{ project.is_archived ? 'Unarchive' : 'Archive' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('delete')"
|
v-if="canDeleteProjects()"
|
||||||
:aria-label="'Delete Project ' + props.project.name"
|
:aria-label="'Delete Project ' + props.project.name"
|
||||||
data-testid="project_delete"
|
data-testid="project_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"
|
||||||
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Project) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MultiselectDropdown
|
<MultiselectDropdown
|
||||||
searchPlaceholder="Search for a Project..."
|
search-placeholder="Search for a Project..."
|
||||||
:items="projects"
|
:items="projects"
|
||||||
:get-key-from-item="getKeyFromItem"
|
:get-key-from-item="getKeyFromItem"
|
||||||
:get-name-for-item="getNameForItem">
|
:get-name-for-item="getNameForItem">
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
:createProject
|
v-model:show="showCreateProjectModal"
|
||||||
:createClient
|
:create-project
|
||||||
|
:create-client
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
:clients="clients"
|
:clients="clients"
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
:enable-estimated-time="isAllowedToPerformPremiumAction"></ProjectCreateModal>
|
||||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
|
||||||
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
<div class="flow-root max-w-[100vw] overflow-x-auto">
|
||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
@@ -57,12 +57,12 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|||||||
class="grid min-w-full"
|
class="grid min-w-full"
|
||||||
:style="gridTemplate">
|
:style="gridTemplate">
|
||||||
<ProjectTableHeading
|
<ProjectTableHeading
|
||||||
:showBillableRate="
|
:show-billable-rate="
|
||||||
props.showBillableRate
|
props.showBillableRate
|
||||||
"></ProjectTableHeading>
|
"></ProjectTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
v-if="projects.length === 0"
|
||||||
v-if="projects.length === 0">
|
class="col-span-5 py-24 text-center">
|
||||||
<FolderPlusIcon
|
<FolderPlusIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||||
<h3 class="text-white font-semibold">
|
<h3 class="text-white font-semibold">
|
||||||
@@ -81,14 +81,14 @@ import { isAllowedToPerformPremiumAction } from '@/utils/billing';
|
|||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateProjects()"
|
v-if="canCreateProjects()"
|
||||||
@click="showCreateProjectModal = true"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="showCreateProjectModal = true"
|
||||||
>Create your First Project
|
>Create your First Project
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="project in projects" :key="project.id">
|
<template v-for="project in projects" :key="project.id">
|
||||||
<ProjectTableRow
|
<ProjectTableRow
|
||||||
:showBillableRate="props.showBillableRate"
|
:show-billable-rate="props.showBillableRate"
|
||||||
:project="project"></ProjectTableRow>
|
:project="project"></ProjectTableRow>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ defineProps<{
|
|||||||
Progress
|
Progress
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="px-3 py-1.5 text-left font-semibold text-white"
|
v-if="showBillableRate"
|
||||||
v-if="showBillableRate">
|
class="px-3 py-1.5 text-left font-semibold text-white">
|
||||||
Billable Rate
|
Billable Rate
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>
|
<div class="px-3 py-1.5 text-left font-semibold text-white">Status</div>
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ const showEditProjectModal = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
|
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-muted">
|
||||||
<div
|
<div
|
||||||
class="overflow-ellipsis overflow-hidden"
|
v-if="project.client_id"
|
||||||
v-if="project.client_id">
|
class="overflow-ellipsis overflow-hidden">
|
||||||
{{ client?.name }}
|
{{ client?.name }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>No client</div>
|
<div v-else>No client</div>
|
||||||
@@ -106,8 +106,8 @@ const showEditProjectModal = ref(false);
|
|||||||
<span v-else> -- </span>
|
<span v-else> -- </span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="whitespace-nowrap px-3 py-4 text-sm text-muted"
|
v-if="showBillableRate"
|
||||||
v-if="showBillableRate">
|
class="whitespace-nowrap px-3 py-4 text-sm text-muted">
|
||||||
{{ billableRateInfo }}
|
{{ billableRateInfo }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BillableRateModal
|
<BillableRateModal
|
||||||
@submit="$emit('submit')"
|
|
||||||
v-model:show="show"
|
v-model:show="show"
|
||||||
v-model:saving="saving"
|
v-model:saving="saving"
|
||||||
title="Update Project Member Billable Rate">
|
title="Update Project Member Billable Rate"
|
||||||
|
@submit="$emit('submit')">
|
||||||
<p class="py-1 text-center">
|
<p class="py-1 text-center">
|
||||||
The billable rate of {{ memberName }} will be updated to
|
The billable rate of {{ memberName }} will be updated to
|
||||||
<strong>{{
|
<strong>{{
|
||||||
|
|||||||
@@ -52,16 +52,16 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
<div class="grid grid-cols-3 items-center space-x-4">
|
<div class="grid grid-cols-3 items-center space-x-4">
|
||||||
<div class="col-span-3 sm:col-span-2">
|
<div class="col-span-3 sm:col-span-2">
|
||||||
<MemberCombobox
|
<MemberCombobox
|
||||||
:hidden-members="props.existingMembers"
|
v-model="projectMember.member_id"
|
||||||
v-model="projectMember.member_id"></MemberCombobox>
|
:hidden-members="props.existingMembers"></MemberCombobox>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-3 sm:col-span-1 flex-1">
|
<div class="col-span-3 sm:col-span-1 flex-1">
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
name="billable_rate"
|
|
||||||
:currency="getOrganizationCurrencyString()"
|
|
||||||
v-model="
|
v-model="
|
||||||
projectMember.billable_rate
|
projectMember.billable_rate
|
||||||
"></BillableRateInput>
|
"
|
||||||
|
name="billable_rate"
|
||||||
|
:currency="getOrganizationCurrencyString()"></BillableRateInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<ProjectMemberBillableRateModal
|
<ProjectMemberBillableRateModal
|
||||||
:member-name="props.name"
|
|
||||||
v-model:show="showBillableRateModal"
|
v-model:show="showBillableRateModal"
|
||||||
|
:member-name="props.name"
|
||||||
:new-billable-rate="projectMemberBody.billable_rate"
|
:new-billable-rate="projectMemberBody.billable_rate"
|
||||||
@close="showBillableRateModal = false"
|
@close="showBillableRateModal = false"
|
||||||
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
@submit="submitBillableRate"></ProjectMemberBillableRateModal>
|
||||||
@@ -92,12 +92,12 @@ useFocus(projectNameInput, { initialValue: true });
|
|||||||
class="mb-2"
|
class="mb-2"
|
||||||
value="Billable Rate"></InputLabel>
|
value="Billable Rate"></InputLabel>
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
@keydown.enter="submit"
|
|
||||||
:currency="getOrganizationCurrencyString()"
|
|
||||||
name="billable_rate"
|
|
||||||
v-model="
|
v-model="
|
||||||
projectMemberBody.billable_rate
|
projectMemberBody.billable_rate
|
||||||
"></BillableRateInput>
|
"
|
||||||
|
:currency="getOrganizationCurrencyString()"
|
||||||
|
name="billable_rate"
|
||||||
|
@keydown.enter="submit"></BillableRateInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ const currentMember = computed(() => {
|
|||||||
<MoreOptionsDropdown
|
<MoreOptionsDropdown
|
||||||
:label="'Actions for Project Member ' + currentMember?.name">
|
:label="'Actions for Project Member ' + currentMember?.name">
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('edit')"
|
|
||||||
:aria-label="'Edit Project Member ' + currentMember?.name"
|
: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">
|
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')">
|
||||||
<PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
|
<PencilSquareIcon class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('delete')"
|
|
||||||
:aria-label="'Delete Project Member ' + currentMember?.name"
|
:aria-label="'Delete Project Member ' + currentMember?.name"
|
||||||
data-testid="project_delete"
|
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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Remove from Team</span>
|
<span>Remove from Team</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ const createProjectMember = ref(false);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ProjectMemberCreateModal
|
<ProjectMemberCreateModal
|
||||||
|
v-model:show="createProjectMember"
|
||||||
:existing-members="projectMembers"
|
:existing-members="projectMembers"
|
||||||
:project-id="projectId"
|
:project-id="projectId"></ProjectMemberCreateModal>
|
||||||
v-model:show="createProjectMember"></ProjectMemberCreateModal>
|
|
||||||
<div class="flow-root">
|
<div class="flow-root">
|
||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
@@ -29,15 +29,15 @@ const createProjectMember = ref(false);
|
|||||||
style="grid-template-columns: 1fr 150px 150px 80px">
|
style="grid-template-columns: 1fr 150px 150px 80px">
|
||||||
<ProjectMemberTableHeading></ProjectMemberTableHeading>
|
<ProjectMemberTableHeading></ProjectMemberTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
v-if="projectMembers.length === 0"
|
||||||
v-if="projectMembers.length === 0">
|
class="col-span-5 py-24 text-center">
|
||||||
<UserGroupIcon
|
<UserGroupIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
class="w-8 text-icon-default inline pb-2"></UserGroupIcon>
|
||||||
<h3 class="text-white font-semibold">No project members</h3>
|
<h3 class="text-white font-semibold">No project members</h3>
|
||||||
<p class="pb-5">Add the first project member!</p>
|
<p class="pb-5">Add the first project member!</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
@click="createProjectMember = true"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="createProjectMember = true"
|
||||||
>Add a new Project Member
|
>Add a new Project Member
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ const showEditModal = ref(false);
|
|||||||
<template>
|
<template>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<ProjectMemberEditModal
|
<ProjectMemberEditModal
|
||||||
:name="member?.name"
|
|
||||||
v-model:show="showEditModal"
|
v-model:show="showEditModal"
|
||||||
|
:name="member?.name"
|
||||||
:project-member="projectMember"></ProjectMemberEditModal>
|
:project-member="projectMember"></ProjectMemberEditModal>
|
||||||
<div
|
<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">
|
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">
|
||||||
|
|||||||
@@ -82,22 +82,22 @@ async function submit() {
|
|||||||
<InputLabel for="name" value="Name" />
|
<InputLabel for="name" value="Name" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="name"
|
id="name"
|
||||||
class="mt-1.5 w-full"
|
v-model="report.name"
|
||||||
v-model="report.name"></TextInput>
|
class="mt-1.5 w-full"></TextInput>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel for="description" value="Description" />
|
<InputLabel for="description" value="Description" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="description"
|
id="description"
|
||||||
class="mt-1.5 w-full"
|
v-model="report.description"
|
||||||
v-model="report.description"></TextInput>
|
class="mt-1.5 w-full"></TextInput>
|
||||||
</div>
|
</div>
|
||||||
<InputLabel value="Visibility" />
|
<InputLabel value="Visibility" />
|
||||||
<div class="flex items-center space-x-12">
|
<div class="flex items-center space-x-12">
|
||||||
<div class="flex items-center space-x-3 px-2 py-3">
|
<div class="flex items-center space-x-3 px-2 py-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model:checked="report.is_public"
|
id="is_public"
|
||||||
id="is_public"></Checkbox>
|
v-model:checked="report.is_public"></Checkbox>
|
||||||
<InputLabel for="is_public" value="Public" />
|
<InputLabel for="is_public" value="Public" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -96,22 +96,22 @@ async function submit() {
|
|||||||
<InputLabel for="name" value="Name" />
|
<InputLabel for="name" value="Name" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="name"
|
id="name"
|
||||||
class="mt-1.5 w-full"
|
v-model="report.name"
|
||||||
v-model="report.name"></TextInput>
|
class="mt-1.5 w-full"></TextInput>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel for="description" value="Description" />
|
<InputLabel for="description" value="Description" />
|
||||||
<TextInput
|
<TextInput
|
||||||
id="description"
|
id="description"
|
||||||
class="mt-1.5 w-full"
|
v-model="report.description"
|
||||||
v-model="report.description"></TextInput>
|
class="mt-1.5 w-full"></TextInput>
|
||||||
</div>
|
</div>
|
||||||
<InputLabel value="Visibility" />
|
<InputLabel value="Visibility" />
|
||||||
<div class="flex items-center space-x-12">
|
<div class="flex items-center space-x-12">
|
||||||
<div class="flex items-center space-x-2 px-2 py-3">
|
<div class="flex items-center space-x-2 px-2 py-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-model:checked="report.is_public"
|
id="is_public"
|
||||||
id="is_public"></Checkbox>
|
v-model:checked="report.is_public"></Checkbox>
|
||||||
<InputLabel for="is_public" value="Public" />
|
<InputLabel for="is_public" value="Public" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ const props = defineProps<{
|
|||||||
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
|
<MoreOptionsDropdown :label="'Actions for Project ' + props.report.name">
|
||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('edit')"
|
|
||||||
v-if="canUpdateReport()"
|
v-if="canUpdateReport()"
|
||||||
:aria-label="'Edit Report ' + props.report.name"
|
: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">
|
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')">
|
||||||
<PencilSquareIcon
|
<PencilSquareIcon
|
||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click.prevent="emit('delete')"
|
|
||||||
:aria-label="'Delete Report ' + props.report.name"
|
|
||||||
v-if="canDeleteReport()"
|
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">
|
: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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ function onSaveReportClick() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ReportCreateModal
|
<ReportCreateModal
|
||||||
:properties="reportProperties"
|
v-model:show="showCreateReportModal"
|
||||||
v-model:show="showCreateReportModal"></ReportCreateModal>
|
:properties="reportProperties"></ReportCreateModal>
|
||||||
<UpgradeModal v-model:show="showPremiumModal">
|
<UpgradeModal v-model:show="showPremiumModal">
|
||||||
<strong>Sharable Reports</strong> is only available in solidtime
|
<strong>Sharable Reports</strong> is only available in solidtime
|
||||||
Professional.
|
Professional.
|
||||||
|
|||||||
@@ -27,19 +27,19 @@ const gridTemplate = computed(() => {
|
|||||||
:style="gridTemplate">
|
:style="gridTemplate">
|
||||||
<ReportTableHeading></ReportTableHeading>
|
<ReportTableHeading></ReportTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
v-if="reports.length === 0"
|
||||||
v-if="reports.length === 0">
|
class="col-span-5 py-24 text-center">
|
||||||
<FolderPlusIcon
|
<FolderPlusIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||||
<h3 class="text-white font-semibold">
|
<h3 class="text-white font-semibold">
|
||||||
No shared reports found
|
No shared reports found
|
||||||
</h3>
|
</h3>
|
||||||
<p class="pb-5" v-if="canCreateProjects()">
|
<p v-if="canCreateProjects()" class="pb-5">
|
||||||
Create your first project now!
|
Create your first project now!
|
||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
@click="router.visit(route('reporting'))"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="router.visit(route('reporting'))"
|
||||||
>Go to the overview to create a report
|
>Go to the overview to create a report
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ async function deleteReport() {
|
|||||||
<span v-else>Copied!</span>
|
<span v-else>Copied!</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<button
|
<button
|
||||||
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-white/80"
|
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-ring"
|
||||||
@click="openSharableLink">
|
@click="openSharableLink">
|
||||||
<ArrowTopRightOnSquareIcon
|
<ArrowTopRightOnSquareIcon
|
||||||
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const option = ref({
|
|||||||
:autoresize="true"
|
:autoresize="true"
|
||||||
class="chart"
|
class="chart"
|
||||||
:option="option" />
|
:option="option" />
|
||||||
<div class="chart flex flex-col items-center justify-center" v-else>
|
<div v-else class="chart flex flex-col items-center justify-center">
|
||||||
<p class="text-lg text-white font-semibold">
|
<p class="text-lg text-white font-semibold">
|
||||||
No time entries found
|
No time entries found
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ function downloadCurrentExport() {
|
|||||||
<Modal
|
<Modal
|
||||||
closeable
|
closeable
|
||||||
max-width="lg"
|
max-width="lg"
|
||||||
@close="showExportModal = false"
|
:show="showExportModal"
|
||||||
:show="showExportModal">
|
@close="showExportModal = false">
|
||||||
<button
|
<button
|
||||||
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">
|
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">
|
||||||
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-center text-text-primary py-6">
|
<div class="text-center text-text-primary py-6">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const activeClass = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<Badge
|
<Badge
|
||||||
size="large"
|
size="large"
|
||||||
|
tag="button"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'cursor-pointer hover:bg-card-background transition flex',
|
'cursor-pointer hover:bg-card-background transition flex',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const title = computed(() => {
|
|||||||
:get-key-from-item="(item) => item.value"
|
:get-key-from-item="(item) => item.value"
|
||||||
:get-name-for-item="(item) => item.label"
|
:get-name-for-item="(item) => item.label"
|
||||||
:items="groupByOptions">
|
:items="groupByOptions">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<Badge
|
<Badge
|
||||||
size="large"
|
size="large"
|
||||||
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">
|
class="cursor-pointer hover:bg-card-background transition space-x-5 flex">
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ const expanded = ref(false);
|
|||||||
)
|
)
|
||||||
">
|
">
|
||||||
<GroupedItemsCountButton
|
<GroupedItemsCountButton
|
||||||
|
v-if="entry.grouped_data && entry.grouped_data?.length > 0"
|
||||||
:expanded="expanded"
|
:expanded="expanded"
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded">
|
||||||
v-if="entry.grouped_data && entry.grouped_data?.length > 0">
|
|
||||||
{{ entry.grouped_data?.length }}
|
{{ entry.grouped_data?.length }}
|
||||||
</GroupedItemsCountButton>
|
</GroupedItemsCountButton>
|
||||||
<span>
|
<span>
|
||||||
@@ -52,13 +52,13 @@ const expanded = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
v-if="expanded && entry.grouped_data"
|
||||||
class="col-span-3 grid bg-quaternary"
|
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
|
<ReportingRow
|
||||||
indent
|
|
||||||
v-for="subEntry in entry.grouped_data"
|
v-for="subEntry in entry.grouped_data"
|
||||||
:key="subEntry.description ?? 'none'"
|
:key="subEntry.description ?? 'none'"
|
||||||
|
indent
|
||||||
:entry="subEntry"></ReportingRow>
|
:entry="subEntry"></ReportingRow>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<TabBar>
|
<TabBar>
|
||||||
<TabBarItem
|
<TabBarItem
|
||||||
@click="router.visit(route('reporting'))"
|
|
||||||
:active="active === 'reporting'"
|
:active="active === 'reporting'"
|
||||||
|
@click="router.visit(route('reporting'))"
|
||||||
>Overview</TabBarItem
|
>Overview</TabBarItem
|
||||||
>
|
>
|
||||||
<TabBarItem
|
<TabBarItem
|
||||||
@click="router.visit(route('reporting.detailed'))"
|
|
||||||
:active="active === 'detailed'"
|
:active="active === 'detailed'"
|
||||||
|
@click="router.visit(route('reporting.detailed'))"
|
||||||
>Detailed</TabBarItem
|
>Detailed</TabBarItem
|
||||||
>
|
>
|
||||||
<TabBarItem
|
<TabBarItem
|
||||||
@click="router.visit(route('reporting.shared'))"
|
|
||||||
:active="active === 'shared'"
|
:active="active === 'shared'"
|
||||||
|
@click="router.visit(route('reporting.shared'))"
|
||||||
>Shared</TabBarItem
|
>Shared</TabBarItem
|
||||||
>
|
>
|
||||||
</TabBar>
|
</TabBar>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ const props = defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
|
<MoreOptionsDropdown :label="'Actions for Tag ' + props.tag.name">
|
||||||
<button
|
<button
|
||||||
@click="emit('delete')"
|
|
||||||
:aria-label="'Delete Tag ' + props.tag.name"
|
:aria-label="'Delete Tag ' + props.tag.name"
|
||||||
data-testid="tag_delete"
|
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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const showCreateTagModal = ref(false);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TagCreateModal
|
<TagCreateModal
|
||||||
:createTag
|
v-model:show="showCreateTagModal"
|
||||||
v-model:show="showCreateTagModal"></TagCreateModal>
|
:create-tag></TagCreateModal>
|
||||||
<div class="flow-root">
|
<div class="flow-root">
|
||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
@@ -29,18 +29,18 @@ const showCreateTagModal = ref(false);
|
|||||||
style="grid-template-columns: 1fr 80px">
|
style="grid-template-columns: 1fr 80px">
|
||||||
<TagTableHeading></TagTableHeading>
|
<TagTableHeading></TagTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
v-if="tags.length === 0"
|
||||||
v-if="tags.length === 0">
|
class="col-span-5 py-24 text-center">
|
||||||
<FolderPlusIcon
|
<FolderPlusIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
class="w-8 text-icon-default inline pb-2"></FolderPlusIcon>
|
||||||
<h3 class="text-white font-semibold">No tags found</h3>
|
<h3 class="text-white font-semibold">No tags found</h3>
|
||||||
<p class="pb-5" v-if="canCreateTags()">
|
<p v-if="canCreateTags()" class="pb-5">
|
||||||
Create your first tag now!
|
Create your first tag now!
|
||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateTags()"
|
v-if="canCreateTags()"
|
||||||
@click="showCreateTagModal = true"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="showCreateTagModal = true"
|
||||||
>Create your First Tag</SecondaryButton
|
>Create your First Tag</SecondaryButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -53,19 +53,19 @@ useFocus(taskNameInput, { initialValue: true });
|
|||||||
v-model="taskName"
|
v-model="taskName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Task Name"
|
placeholder="Task Name"
|
||||||
@keydown.enter="submit()"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="taskName" />
|
autocomplete="taskName"
|
||||||
|
@keydown.enter="submit()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<ProjectDropdown :modelValue="projectId"></ProjectDropdown>
|
<ProjectDropdown :model-value="projectId"></ProjectDropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EstimatedTimeSection
|
<EstimatedTimeSection
|
||||||
v-if="isAllowedToPerformPremiumAction()"
|
v-if="isAllowedToPerformPremiumAction()"
|
||||||
@submit="submit()"
|
v-model="estimatedTime"
|
||||||
v-model="estimatedTime"></EstimatedTimeSection>
|
@submit="submit()"></EstimatedTimeSection>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ useFocus(taskNameInput, { initialValue: true });
|
|||||||
v-model="taskBody.name"
|
v-model="taskBody.name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Task Name"
|
placeholder="Task Name"
|
||||||
@keydown.enter="submit()"
|
|
||||||
class="mt-1 block w-full"
|
class="mt-1 block w-full"
|
||||||
required
|
required
|
||||||
autocomplete="taskName" />
|
autocomplete="taskName"
|
||||||
|
@keydown.enter="submit()" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EstimatedTimeSection
|
<EstimatedTimeSection
|
||||||
v-if="isAllowedToPerformPremiumAction()"
|
v-if="isAllowedToPerformPremiumAction()"
|
||||||
@submit="submit()"
|
v-model="taskBody.estimated_time"
|
||||||
v-model="taskBody.estimated_time"></EstimatedTimeSection>
|
@submit="submit()"></EstimatedTimeSection>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
<SecondaryButton @click="show = false"> Cancel </SecondaryButton>
|
||||||
|
|||||||
@@ -21,30 +21,30 @@ const props = defineProps<{
|
|||||||
<MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
|
<MoreOptionsDropdown :label="'Actions for Task ' + props.task.name">
|
||||||
<div class="min-w-[150px]">
|
<div class="min-w-[150px]">
|
||||||
<button
|
<button
|
||||||
@click="emit('edit')"
|
|
||||||
v-if="canUpdateTasks()"
|
v-if="canUpdateTasks()"
|
||||||
:aria-label="'Edit Task ' + props.task.name"
|
:aria-label="'Edit Task ' + props.task.name"
|
||||||
data-testid="task_edit"
|
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">
|
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')">
|
||||||
<PencilSquareIcon
|
<PencilSquareIcon
|
||||||
class="w-5 text-icon-active"></PencilSquareIcon>
|
class="w-5 text-icon-active"></PencilSquareIcon>
|
||||||
<span>Edit</span>
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="emit('done')"
|
|
||||||
v-if="canUpdateTasks()"
|
v-if="canUpdateTasks()"
|
||||||
:aria-label="'Mark Task ' + props.task.name + ' as done'"
|
: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">
|
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')">
|
||||||
<CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon>
|
<CheckCircleIcon class="w-5 text-icon-active"></CheckCircleIcon>
|
||||||
<span v-if="props.task.is_done">Mark as active</span>
|
<span v-if="props.task.is_done">Mark as active</span>
|
||||||
<span v-else>Mark as done</span>
|
<span v-else>Mark as done</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="emit('delete')"
|
|
||||||
:aria-label="'Delete Task ' + props.task.name"
|
|
||||||
v-if="canDeleteTasks()"
|
v-if="canDeleteTasks()"
|
||||||
|
:aria-label="'Delete Task ' + props.task.name"
|
||||||
data-testid="task_delete"
|
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">
|
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')">
|
||||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getNameForItem(item: Task) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MultiselectDropdown
|
<MultiselectDropdown
|
||||||
searchPlaceholder="Search for a Task..."
|
search-placeholder="Search for a Task..."
|
||||||
:items="tasks"
|
:items="tasks"
|
||||||
:get-key-from-item="getKeyFromItem"
|
:get-key-from-item="getKeyFromItem"
|
||||||
:get-name-for-item="getNameForItem">
|
:get-name-for-item="getNameForItem">
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ const createTask = ref(false);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TaskCreateModal
|
<TaskCreateModal
|
||||||
:project-id="props.projectId"
|
v-model:show="createTask"
|
||||||
v-model:show="createTask"></TaskCreateModal>
|
:project-id="props.projectId"></TaskCreateModal>
|
||||||
<div class="flow-root">
|
<div class="flow-root">
|
||||||
<div class="inline-block min-w-full align-middle">
|
<div class="inline-block min-w-full align-middle">
|
||||||
<div
|
<div
|
||||||
@@ -37,18 +37,18 @@ const createTask = ref(false);
|
|||||||
">
|
">
|
||||||
<TaskTableHeading></TaskTableHeading>
|
<TaskTableHeading></TaskTableHeading>
|
||||||
<div
|
<div
|
||||||
class="col-span-5 py-24 text-center"
|
v-if="tasks.length === 0"
|
||||||
v-if="tasks.length === 0">
|
class="col-span-5 py-24 text-center">
|
||||||
<PlusCircleIcon
|
<PlusCircleIcon
|
||||||
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
class="w-8 text-icon-default inline pb-2"></PlusCircleIcon>
|
||||||
<h3 class="text-white font-semibold">No tasks found</h3>
|
<h3 class="text-white font-semibold">No tasks found</h3>
|
||||||
<p class="pb-5" v-if="canCreateTasks()">
|
<p v-if="canCreateTasks()" class="pb-5">
|
||||||
Create your first task now!
|
Create your first task now!
|
||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
v-if="canCreateTasks()"
|
v-if="canCreateTasks()"
|
||||||
@click="createTask = true"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="createTask = true"
|
||||||
>Create your First Task
|
>Create your First Task
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ const showTaskEditModal = ref(false);
|
|||||||
@delete="deleteTask"></TaskMoreOptionsDropdown>
|
@delete="deleteTask"></TaskMoreOptionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
<TaskEditModal
|
<TaskEditModal
|
||||||
:task="task"
|
v-model:show="showTaskEditModal"
|
||||||
v-model:show="showTaskEditModal"></TaskEditModal>
|
:task="task"></TaskEditModal>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ const showUpgradeModal = ref(false);
|
|||||||
solidtime Professional.
|
solidtime Professional.
|
||||||
</UpgradeModal>
|
</UpgradeModal>
|
||||||
<button
|
<button
|
||||||
@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"
|
||||||
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">
|
||||||
<LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon>
|
<LockClosedIcon class="w-3 text-text-tertiary"></LockClosedIcon>
|
||||||
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
|
<span class="text-xs text-text-secondary font-semibold"> Upgrade </span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -50,12 +50,12 @@ const show = defineModel('show', { default: false });
|
|||||||
v-if="isBillingActivated() && canManageBilling()"
|
v-if="isBillingActivated() && canManageBilling()"
|
||||||
href="/billing">
|
href="/billing">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
|
||||||
class="mt-6"
|
|
||||||
:icon="CreditCardIcon"
|
|
||||||
v-if="
|
v-if="
|
||||||
isBillingActivated() && canUpdateOrganization()
|
isBillingActivated() && canUpdateOrganization()
|
||||||
">
|
"
|
||||||
|
type="button"
|
||||||
|
class="mt-6"
|
||||||
|
:icon="CreditCardIcon">
|
||||||
Go to Billing
|
Go to Billing
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const isRunningInDifferentOrganization = computed(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
|
<div class="pt-3 pb-2.5 px-2 flex justify-between items-center relative">
|
||||||
<div
|
<div
|
||||||
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center"
|
v-if="isRunningInDifferentOrganization"
|
||||||
v-if="isRunningInDifferentOrganization">
|
class="absolute w-full h-full backdrop-blur-sm z-10 flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
class="w-full h-[calc(100%+10px)] absolute bg-default-background opacity-75 backdrop-blur-sm"></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">
|
<div class="flex space-x-3 items-center w-full z-20 justify-center">
|
||||||
@@ -50,7 +50,7 @@ const isRunningInDifferentOrganization = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
:active="isActive"
|
:active="isActive"
|
||||||
@changed="setActiveState"
|
size="base"
|
||||||
size="base"></TimeTrackerStartStop>
|
@changed="setActiveState"></TimeTrackerStartStop>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ defineProps<{
|
|||||||
<DashboardCard title="Last 7 Days" :icon="CalendarIcon">
|
<DashboardCard title="Last 7 Days" :icon="CalendarIcon">
|
||||||
<DayOverviewCardEntry
|
<DayOverviewCardEntry
|
||||||
v-for="day in last7Days"
|
v-for="day in last7Days"
|
||||||
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
|
|
||||||
:key="day.date"
|
:key="day.date"
|
||||||
|
:class="last7Days.length === 7 ? 'last:border-0 first:pt-3' : ''"
|
||||||
:date="day.date"
|
:date="day.date"
|
||||||
:history="day.history"
|
:history="day.history"
|
||||||
:duration="day.duration"></DayOverviewCardEntry>
|
:duration="day.duration"></DayOverviewCardEntry>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const props = defineProps<{
|
|||||||
<DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon">
|
<DashboardCard title="Recently Tracked Tasks" :icon="CheckCircleIcon">
|
||||||
<RecentlyTrackedTasksCardEntry
|
<RecentlyTrackedTasksCardEntry
|
||||||
v-for="lastTask in props.latestTasks"
|
v-for="lastTask in props.latestTasks"
|
||||||
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
|
|
||||||
:key="lastTask.id"
|
:key="lastTask.id"
|
||||||
|
:class="props.latestTasks.length === 4 ? 'last:border-0' : ''"
|
||||||
:project_id="lastTask.project_id"
|
:project_id="lastTask.project_id"
|
||||||
:task_id="lastTask.id"
|
:task_id="lastTask.id"
|
||||||
:title="lastTask.name"></RecentlyTrackedTasksCardEntry>
|
:title="lastTask.name"></RecentlyTrackedTasksCardEntry>
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
|
|||||||
:icon
|
:icon
|
||||||
:current
|
:current
|
||||||
:href></NavigationSidebarLink>
|
:href></NavigationSidebarLink>
|
||||||
<CollapsibleRoot v-model:open="open" v-else
|
<CollapsibleRoot v-else v-model:open="open"
|
||||||
><CollapsibleTrigger class="w-full group py-0.5">
|
><CollapsibleTrigger class="w-full group py-0.5">
|
||||||
<div
|
<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">
|
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">
|
<div class="flex items-center gap-x-2">
|
||||||
<component
|
<component
|
||||||
v-if="icon"
|
|
||||||
:is="icon"
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
:class="[
|
:class="[
|
||||||
current
|
current
|
||||||
? 'text-icon-active'
|
? 'text-icon-active'
|
||||||
@@ -59,8 +59,8 @@ const open = useSessionStorage('nav-collapse-state-' + props.title, true);
|
|||||||
<CollapsibleContent class="CollapsibleContent">
|
<CollapsibleContent class="CollapsibleContent">
|
||||||
<div class="px-3.5">
|
<div class="px-3.5">
|
||||||
<ul
|
<ul
|
||||||
class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5"
|
v-if="subItems"
|
||||||
v-if="subItems">
|
class="flex min-w-0 flex-col border-l border-border-secondary px-3 w-full my-0.5">
|
||||||
<li
|
<li
|
||||||
v-for="subItem in subItems"
|
v-for="subItem in subItems"
|
||||||
:key="subItem.title"
|
:key="subItem.title"
|
||||||
|
|||||||
@@ -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',
|
'group flex gap-x-2 rounded-md transition leading-6 py-1 px-2 font-medium text-sm items-center',
|
||||||
]">
|
]">
|
||||||
<component
|
<component
|
||||||
v-if="icon"
|
|
||||||
:is="icon"
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
:class="[
|
:class="[
|
||||||
current
|
current
|
||||||
? 'text-icon-active'
|
? 'text-icon-active'
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
||||||
<Notification
|
<Notification
|
||||||
v-for="notification in notifications"
|
v-for="notification in notifications"
|
||||||
:type="notification.type"
|
|
||||||
:key="notification.uuid"
|
:key="notification.uuid"
|
||||||
|
:type="notification.type"
|
||||||
:title="notification.title"
|
:title="notification.title"
|
||||||
:message="notification.message"></Notification>
|
:message="notification.message"></Notification>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,29 +110,29 @@ const { tags } = storeToRefs(useTagsStore());
|
|||||||
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
<CardTitle title="Time Tracker" :icon="ClockIcon"></CardTitle>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<TimeTrackerRunningInDifferentOrganizationOverlay
|
<TimeTrackerRunningInDifferentOrganizationOverlay
|
||||||
@switchOrganization="switchToTimeEntryOrganization"
|
|
||||||
v-if="
|
v-if="
|
||||||
isRunningInDifferentOrganization
|
isRunningInDifferentOrganization
|
||||||
"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
"
|
||||||
|
@switch-organization="switchToTimeEntryOrganization"></TimeTrackerRunningInDifferentOrganizationOverlay>
|
||||||
|
|
||||||
<TimeTrackerControls
|
<TimeTrackerControls
|
||||||
:createProject
|
v-model:current-time-entry="currentTimeEntry"
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
v-model:live-timer="now"
|
||||||
:canCreateProject="canCreateProjects()"
|
:create-project
|
||||||
:createClient
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
|
:can-create-project="canCreateProjects()"
|
||||||
|
:create-client
|
||||||
:clients
|
:clients
|
||||||
:tags
|
:tags
|
||||||
:tasks
|
:tasks
|
||||||
:projects
|
:projects
|
||||||
:createTag
|
:create-tag
|
||||||
:isActive
|
:is-active
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
v-model:currentTimeEntry="currentTimeEntry"
|
@start-live-timer="startLiveTimer"
|
||||||
v-model:liveTimer="now"
|
@stop-live-timer="stopLiveTimer"
|
||||||
@startLiveTimer="startLiveTimer"
|
@start-timer="setActiveState(true)"
|
||||||
@stopLiveTimer="stopLiveTimer"
|
@stop-timer="setActiveState(false)"
|
||||||
@startTimer="setActiveState(true)"
|
@update-time-entry="updateTimeEntry"></TimeTrackerControls>
|
||||||
@stopTimer="setActiveState(false)"
|
|
||||||
@updateTimeEntry="updateTimeEntry"></TimeTrackerControls>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function openDesktopGithubRepo() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="py-4 hidden lg:block" v-if="showReleaseInfo">
|
<div v-if="showReleaseInfo" class="py-4 hidden lg:block">
|
||||||
<div
|
<div
|
||||||
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
|
class="rounded-lg px-2.5 py-2 bg-card-background border border-border-secondary">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
@@ -23,8 +23,8 @@ function openDesktopGithubRepo() {
|
|||||||
</div>
|
</div>
|
||||||
<button>
|
<button>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
@click="showReleaseInfo = false"
|
class="w-3.5 text-text-tertiary hover:text-text-secondary"
|
||||||
class="w-3.5 text-text-tertiary hover:text-text-secondary"></XMarkIcon>
|
@click="showReleaseInfo = false"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ function openDesktopGithubRepo() {
|
|||||||
now.
|
now.
|
||||||
</p>
|
</p>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
@click="openDesktopGithubRepo"
|
|
||||||
size="small"
|
size="small"
|
||||||
class="w-full text-center justify-center mt-1.5"
|
class="w-full text-center justify-center mt-1.5"
|
||||||
|
@click="openDesktopGithubRepo"
|
||||||
>Download now</SecondaryButton
|
>Download now</SecondaryButton
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ const page = usePage<{
|
|||||||
class="border-b border-default-background-separator pb-2 flex justify-between">
|
class="border-b border-default-background-separator pb-2 flex justify-between">
|
||||||
<OrganizationSwitcher class="w-full"></OrganizationSwitcher>
|
<OrganizationSwitcher class="w-full"></OrganizationSwitcher>
|
||||||
<XMarkIcon
|
<XMarkIcon
|
||||||
@click="showSidebarMenu = false"
|
class="w-8 lg:hidden"
|
||||||
class="w-8 lg:hidden"></XMarkIcon>
|
@click="showSidebarMenu = false"></XMarkIcon>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-b border-default-background-separator">
|
<div class="border-b border-default-background-separator">
|
||||||
<CurrentSidebarTimer></CurrentSidebarTimer>
|
<CurrentSidebarTimer></CurrentSidebarTimer>
|
||||||
@@ -162,8 +162,8 @@ const page = usePage<{
|
|||||||
route('clients')
|
route('clients')
|
||||||
"></NavigationSidebarItem>
|
"></NavigationSidebarItem>
|
||||||
<NavigationSidebarItem
|
<NavigationSidebarItem
|
||||||
title="Members"
|
|
||||||
v-if="canViewMembers()"
|
v-if="canViewMembers()"
|
||||||
|
title="Members"
|
||||||
:icon="UserGroupIcon"
|
:icon="UserGroupIcon"
|
||||||
:current="route().current('members')"
|
:current="route().current('members')"
|
||||||
:href="
|
:href="
|
||||||
@@ -238,8 +238,8 @@ const page = usePage<{
|
|||||||
<div
|
<div
|
||||||
class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center">
|
class="lg:hidden w-full px-3 py-1 border-b border-b-default-background-separator text-muted flex justify-between items-center">
|
||||||
<Bars3Icon
|
<Bars3Icon
|
||||||
@click="showSidebarMenu = !showSidebarMenu"
|
class="w-7 text-muted"
|
||||||
class="w-7 text-muted"></Bars3Icon>
|
@click="showSidebarMenu = !showSidebarMenu"></Bars3Icon>
|
||||||
<OrganizationSwitcher></OrganizationSwitcher>
|
<OrganizationSwitcher></OrganizationSwitcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const page = usePage<{
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4" v-if="page.props.newsletter_consent">
|
<div v-if="page.props.newsletter_consent" class="mt-4">
|
||||||
<InputLabel for="newsletter_consent">
|
<InputLabel for="newsletter_consent">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function refreshDashboardData() {
|
|||||||
<MainContainer
|
<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">
|
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
|
<RecentlyTrackedTasksCard
|
||||||
:latestTasks="props.latestTasks"></RecentlyTrackedTasksCard>
|
:latest-tasks="props.latestTasks"></RecentlyTrackedTasksCard>
|
||||||
<LastSevenDaysCard
|
<LastSevenDaysCard
|
||||||
:last7-days="props.lastSevenDays"></LastSevenDaysCard>
|
:last7-days="props.lastSevenDays"></LastSevenDaysCard>
|
||||||
<ActivityGraphCard
|
<ActivityGraphCard
|
||||||
@@ -84,13 +84,13 @@ function refreshDashboardData() {
|
|||||||
<TeamActivityCard
|
<TeamActivityCard
|
||||||
v-if="canViewMembers()"
|
v-if="canViewMembers()"
|
||||||
class="flex lg:hidden xl:flex"
|
class="flex lg:hidden xl:flex"
|
||||||
:latestTeamActivity="
|
:latest-team-activity="
|
||||||
props.latestTeamActivity
|
props.latestTeamActivity
|
||||||
"></TeamActivityCard>
|
"></TeamActivityCard>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MainContainer class="py-5">
|
<MainContainer class="py-5">
|
||||||
<ThisWeekOverview
|
<ThisWeekOverview
|
||||||
:weeklyProjectOverview="props.weeklyProjectOverview"
|
:weekly-project-overview="props.weeklyProjectOverview"
|
||||||
:total-weekly-billable-amount="props.totalWeeklyBillableAmount"
|
:total-weekly-billable-amount="props.totalWeeklyBillableAmount"
|
||||||
:total-weekly-billable-time="props.totalWeeklyBillableTime"
|
:total-weekly-billable-time="props.totalWeeklyBillableTime"
|
||||||
:total-weekly-time="props.totalWeeklyTime"
|
:total-weekly-time="props.totalWeeklyTime"
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ function isActiveTab(tab: string) {
|
|||||||
>Invite member</SecondaryButton
|
>Invite member</SecondaryButton
|
||||||
>
|
>
|
||||||
<MemberInviteModal
|
<MemberInviteModal
|
||||||
:available-roles="availableRoles"
|
|
||||||
v-model:show="inviteMember"
|
v-model:show="inviteMember"
|
||||||
|
:available-roles="availableRoles"
|
||||||
@close="activeTab = 'invitations'"></MemberInviteModal>
|
@close="activeTab = 'invitations'"></MemberInviteModal>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MemberTable v-if="activeTab === 'all'"></MemberTable>
|
<MemberTable v-if="activeTab === 'all'"></MemberTable>
|
||||||
|
|||||||
@@ -204,9 +204,9 @@ const page = usePage<{
|
|||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="timezone" value="Timezone" />
|
<InputLabel for="timezone" value="Timezone" />
|
||||||
<select
|
<select
|
||||||
name="timezone"
|
|
||||||
id="timezone"
|
id="timezone"
|
||||||
v-model="form.timezone"
|
v-model="form.timezone"
|
||||||
|
name="timezone"
|
||||||
required
|
required
|
||||||
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
|
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>
|
<option value="" disabled>Select a Timezone</option>
|
||||||
@@ -225,9 +225,9 @@ const page = usePage<{
|
|||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="week_start" value="Start of the week" />
|
<InputLabel for="week_start" value="Start of the week" />
|
||||||
<select
|
<select
|
||||||
name="week_start"
|
|
||||||
id="week_start"
|
id="week_start"
|
||||||
v-model="form.week_start"
|
v-model="form.week_start"
|
||||||
|
name="week_start"
|
||||||
required
|
required
|
||||||
class="mt-1 block w-full border-input-border bg-input-background text-white focus:border-input-border-active rounded-md shadow-sm">
|
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>
|
<option value="" disabled>Select a week day</option>
|
||||||
|
|||||||
@@ -129,15 +129,15 @@ const shownTasks = computed(() => {
|
|||||||
</nav>
|
</nav>
|
||||||
<div>
|
<div>
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
|
v-if="canCreateProjects()"
|
||||||
:icon="PencilSquareIcon"
|
:icon="PencilSquareIcon"
|
||||||
@click="showEditProjectModal = true"
|
@click="showEditProjectModal = true">
|
||||||
v-if="canCreateProjects()">
|
|
||||||
Edit Project
|
Edit Project
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<ProjectEditModal
|
<ProjectEditModal
|
||||||
v-if="project"
|
v-if="project"
|
||||||
:originalProject="project"
|
v-model:show="showEditProjectModal"
|
||||||
v-model:show="showEditProjectModal"></ProjectEditModal>
|
:original-project="project"></ProjectEditModal>
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
@@ -168,8 +168,8 @@ const shownTasks = computed(() => {
|
|||||||
>Create Task
|
>Create Task
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<TaskCreateModal
|
<TaskCreateModal
|
||||||
:project-id="projectId"
|
v-model:show="createTask"
|
||||||
v-model:show="createTask"></TaskCreateModal>
|
:project-id="projectId"></TaskCreateModal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
@@ -188,11 +188,11 @@ const shownTasks = computed(() => {
|
|||||||
Add Member
|
Add Member
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<ProjectMemberCreateModal
|
<ProjectMemberCreateModal
|
||||||
:project-id="projectId"
|
|
||||||
:existing-members="projectMembers"
|
|
||||||
v-model:show="
|
v-model:show="
|
||||||
createProjectMember
|
createProjectMember
|
||||||
"></ProjectMemberCreateModal>
|
"
|
||||||
|
:project-id="projectId"
|
||||||
|
:existing-members="projectMembers"></ProjectMemberCreateModal>
|
||||||
</template>
|
</template>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ const showBillableRate = computed(() => {
|
|||||||
>Create Project
|
>Create Project
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
:createProject
|
v-model:show="showCreateProjectModal"
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction"
|
:create-project
|
||||||
:createClient
|
:enable-estimated-time="isAllowedToPerformPremiumAction"
|
||||||
|
:create-client
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
:clients="clients"
|
:clients="clients"
|
||||||
@submit="createProject"
|
@submit="createProject"></ProjectCreateModal>
|
||||||
v-model:show="showCreateProjectModal"></ProjectCreateModal>
|
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<ProjectTable
|
<ProjectTable
|
||||||
:show-billable-rate="showBillableRate"
|
:show-billable-rate="showBillableRate"
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ const tableData = computed(() => {
|
|||||||
class="overflow-hidden">
|
class="overflow-hidden">
|
||||||
<ReportingExportModal
|
<ReportingExportModal
|
||||||
v-model:show="showExportModal"
|
v-model:show="showExportModal"
|
||||||
:exportUrl="exportUrl"></ReportingExportModal>
|
:export-url="exportUrl"></ReportingExportModal>
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
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">
|
<div class="flex items-center space-x-3 sm:space-x-6">
|
||||||
@@ -292,7 +292,7 @@ const tableData = computed(() => {
|
|||||||
<ReportingExportButton
|
<ReportingExportButton
|
||||||
:download="downloadExport"></ReportingExportButton>
|
:download="downloadExport"></ReportingExportButton>
|
||||||
<ReportSaveButton
|
<ReportSaveButton
|
||||||
:reportProperties="reportProperties"></ReportSaveButton>
|
:report-properties="reportProperties"></ReportSaveButton>
|
||||||
</div>
|
</div>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<div class="py-2.5 w-full border-b border-default-background-separator">
|
<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">
|
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
|
||||||
<div class="text-sm font-medium">Filters</div>
|
<div class="text-sm font-medium">Filters</div>
|
||||||
<MemberMultiselectDropdown
|
<MemberMultiselectDropdown
|
||||||
@submit="updateReporting"
|
v-model="selectedMembers"
|
||||||
v-model="selectedMembers">
|
@submit="updateReporting">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedMembers.length"
|
:count="selectedMembers.length"
|
||||||
:active="selectedMembers.length > 0"
|
:active="selectedMembers.length > 0"
|
||||||
@@ -313,9 +313,9 @@ const tableData = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</MemberMultiselectDropdown>
|
</MemberMultiselectDropdown>
|
||||||
<ProjectMultiselectDropdown
|
<ProjectMultiselectDropdown
|
||||||
@submit="updateReporting"
|
v-model="selectedProjects"
|
||||||
v-model="selectedProjects">
|
@submit="updateReporting">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedProjects.length"
|
:count="selectedProjects.length"
|
||||||
:active="selectedProjects.length > 0"
|
:active="selectedProjects.length > 0"
|
||||||
@@ -324,9 +324,9 @@ const tableData = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</ProjectMultiselectDropdown>
|
</ProjectMultiselectDropdown>
|
||||||
<TaskMultiselectDropdown
|
<TaskMultiselectDropdown
|
||||||
@submit="updateReporting"
|
v-model="selectedTasks"
|
||||||
v-model="selectedTasks">
|
@submit="updateReporting">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedTasks.length"
|
:count="selectedTasks.length"
|
||||||
:active="selectedTasks.length > 0"
|
:active="selectedTasks.length > 0"
|
||||||
@@ -335,20 +335,20 @@ const tableData = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</TaskMultiselectDropdown>
|
</TaskMultiselectDropdown>
|
||||||
<ClientMultiselectDropdown
|
<ClientMultiselectDropdown
|
||||||
@submit="updateReporting"
|
v-model="selectedClients"
|
||||||
v-model="selectedClients">
|
@submit="updateReporting">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
title="Clients"
|
title="Clients"
|
||||||
:icon="FolderIcon"></ReportingFilterBadge>
|
:icon="FolderIcon"></ReportingFilterBadge>
|
||||||
</template>
|
</template>
|
||||||
</ClientMultiselectDropdown>
|
</ClientMultiselectDropdown>
|
||||||
<TagDropdown
|
<TagDropdown
|
||||||
@submit="updateReporting"
|
|
||||||
:createTag
|
|
||||||
v-model="selectedTags"
|
v-model="selectedTags"
|
||||||
:tags="tags">
|
:create-tag
|
||||||
<template v-slot:trigger>
|
:tags="tags"
|
||||||
|
@submit="updateReporting">
|
||||||
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedTags.length"
|
:count="selectedTags.length"
|
||||||
:active="selectedTags.length > 0"
|
:active="selectedTags.length > 0"
|
||||||
@@ -358,7 +358,6 @@ const tableData = computed(() => {
|
|||||||
</TagDropdown>
|
</TagDropdown>
|
||||||
|
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
@changed="updateReporting"
|
|
||||||
v-model="billable"
|
v-model="billable"
|
||||||
:get-key-from-item="(item) => item.value"
|
:get-key-from-item="(item) => item.value"
|
||||||
:get-name-for-item="(item) => item.label"
|
:get-name-for-item="(item) => item.label"
|
||||||
@@ -375,8 +374,9 @@ const tableData = computed(() => {
|
|||||||
label: 'Non Billable',
|
label: 'Non Billable',
|
||||||
value: 'false',
|
value: 'false',
|
||||||
},
|
},
|
||||||
]">
|
]"
|
||||||
<template v-slot:trigger>
|
@changed="updateReporting">
|
||||||
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:active="billable !== null"
|
:active="billable !== null"
|
||||||
:title="
|
:title="
|
||||||
@@ -399,8 +399,8 @@ const tableData = computed(() => {
|
|||||||
<MainContainer>
|
<MainContainer>
|
||||||
<div class="pt-10 w-full px-3 relative">
|
<div class="pt-10 w-full px-3 relative">
|
||||||
<ReportingChart
|
<ReportingChart
|
||||||
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
|
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||||
:groupedData="
|
:grouped-data="
|
||||||
aggregatedGraphTimeEntries?.grouped_data
|
aggregatedGraphTimeEntries?.grouped_data
|
||||||
"></ReportingChart>
|
"></ReportingChart>
|
||||||
</div>
|
</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">
|
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>
|
<span>Group by</span>
|
||||||
<ReportingGroupBySelect
|
<ReportingGroupBySelect
|
||||||
|
v-model="group"
|
||||||
:group-by-options="groupByOptions"
|
:group-by-options="groupByOptions"
|
||||||
@changed="updateTableReporting"
|
@changed="updateTableReporting"></ReportingGroupBySelect>
|
||||||
v-model="group"></ReportingGroupBySelect>
|
|
||||||
<span>and</span>
|
<span>and</span>
|
||||||
<ReportingGroupBySelect
|
<ReportingGroupBySelect
|
||||||
|
v-model="subGroup"
|
||||||
:group-by-options="
|
:group-by-options="
|
||||||
groupByOptions.filter(
|
groupByOptions.filter(
|
||||||
(el) => el.value !== group
|
(el) => el.value !== group
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@changed="updateTableReporting"
|
@changed="updateTableReporting"></ReportingGroupBySelect>
|
||||||
v-model="subGroup"></ReportingGroupBySelect>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid items-center"
|
class="grid items-center"
|
||||||
@@ -473,8 +473,8 @@ const tableData = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="chart flex flex-col items-center justify-center py-12 col-span-3"
|
v-else
|
||||||
v-else>
|
class="chart flex flex-col items-center justify-center py-12 col-span-3">
|
||||||
<p class="text-lg text-white font-semibold">
|
<p class="text-lg text-white font-semibold">
|
||||||
No time entries found
|
No time entries found
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
class="overflow-hidden">
|
class="overflow-hidden">
|
||||||
<ReportingExportModal
|
<ReportingExportModal
|
||||||
v-model:show="showExportModal"
|
v-model:show="showExportModal"
|
||||||
:exportUrl="exportUrl"></ReportingExportModal>
|
:export-url="exportUrl"></ReportingExportModal>
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="py-3 sm:py-5 border-b border-default-background-separator flex justify-between items-center">
|
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">
|
<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">
|
class="flex flex-wrap items-center space-y-2 sm:space-y-0 space-x-4">
|
||||||
<div class="text-sm font-medium">Filters</div>
|
<div class="text-sm font-medium">Filters</div>
|
||||||
<MemberMultiselectDropdown
|
<MemberMultiselectDropdown
|
||||||
@submit="updateFilteredTimeEntries"
|
v-model="selectedMembers"
|
||||||
v-model="selectedMembers">
|
@submit="updateFilteredTimeEntries">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedMembers.length"
|
:count="selectedMembers.length"
|
||||||
:active="selectedMembers.length > 0"
|
:active="selectedMembers.length > 0"
|
||||||
@@ -281,9 +281,9 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</template>
|
</template>
|
||||||
</MemberMultiselectDropdown>
|
</MemberMultiselectDropdown>
|
||||||
<ProjectMultiselectDropdown
|
<ProjectMultiselectDropdown
|
||||||
@submit="updateFilteredTimeEntries"
|
v-model="selectedProjects"
|
||||||
v-model="selectedProjects">
|
@submit="updateFilteredTimeEntries">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedProjects.length"
|
:count="selectedProjects.length"
|
||||||
:active="selectedProjects.length > 0"
|
:active="selectedProjects.length > 0"
|
||||||
@@ -292,9 +292,9 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</template>
|
</template>
|
||||||
</ProjectMultiselectDropdown>
|
</ProjectMultiselectDropdown>
|
||||||
<TaskMultiselectDropdown
|
<TaskMultiselectDropdown
|
||||||
@submit="updateFilteredTimeEntries"
|
v-model="selectedTasks"
|
||||||
v-model="selectedTasks">
|
@submit="updateFilteredTimeEntries">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedTasks.length"
|
:count="selectedTasks.length"
|
||||||
:active="selectedTasks.length > 0"
|
:active="selectedTasks.length > 0"
|
||||||
@@ -303,20 +303,20 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</template>
|
</template>
|
||||||
</TaskMultiselectDropdown>
|
</TaskMultiselectDropdown>
|
||||||
<ClientMultiselectDropdown
|
<ClientMultiselectDropdown
|
||||||
@submit="updateFilteredTimeEntries"
|
v-model="selectedClients"
|
||||||
v-model="selectedClients">
|
@submit="updateFilteredTimeEntries">
|
||||||
<template v-slot:trigger>
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
title="Clients"
|
title="Clients"
|
||||||
:icon="FolderIcon"></ReportingFilterBadge>
|
:icon="FolderIcon"></ReportingFilterBadge>
|
||||||
</template>
|
</template>
|
||||||
</ClientMultiselectDropdown>
|
</ClientMultiselectDropdown>
|
||||||
<TagDropdown
|
<TagDropdown
|
||||||
@submit="updateFilteredTimeEntries"
|
|
||||||
:createTag
|
|
||||||
v-model="selectedTags"
|
v-model="selectedTags"
|
||||||
:tags="tags">
|
:create-tag
|
||||||
<template v-slot:trigger>
|
:tags="tags"
|
||||||
|
@submit="updateFilteredTimeEntries">
|
||||||
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:count="selectedTags.length"
|
:count="selectedTags.length"
|
||||||
:active="selectedTags.length > 0"
|
:active="selectedTags.length > 0"
|
||||||
@@ -326,7 +326,6 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</TagDropdown>
|
</TagDropdown>
|
||||||
|
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
@changed="updateFilteredTimeEntries"
|
|
||||||
v-model="billable"
|
v-model="billable"
|
||||||
:get-key-from-item="(item) => item.value"
|
:get-key-from-item="(item) => item.value"
|
||||||
:get-name-for-item="(item) => item.label"
|
:get-name-for-item="(item) => item.label"
|
||||||
@@ -343,8 +342,9 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
label: 'Non Billable',
|
label: 'Non Billable',
|
||||||
value: 'false',
|
value: 'false',
|
||||||
},
|
},
|
||||||
]">
|
]"
|
||||||
<template v-slot:trigger>
|
@changed="updateFilteredTimeEntries">
|
||||||
|
<template #trigger>
|
||||||
<ReportingFilterBadge
|
<ReportingFilterBadge
|
||||||
:active="billable !== null"
|
:active="billable !== null"
|
||||||
:title="
|
:title="
|
||||||
@@ -366,12 +366,9 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</div>
|
</div>
|
||||||
<TimeEntryMassActionRow
|
<TimeEntryMassActionRow
|
||||||
:selected-time-entries="selectedTimeEntries"
|
:selected-time-entries="selectedTimeEntries"
|
||||||
:canCreateProject="canCreateProjects()"
|
:can-create-project="canCreateProjects()"
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
@submit="clearSelectionAndState"
|
|
||||||
:delete-selected="deleteSelected"
|
:delete-selected="deleteSelected"
|
||||||
@select-all="selectedTimeEntries = [...timeEntries]"
|
|
||||||
@unselect-all="selectedTimeEntries = []"
|
|
||||||
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
@@ -387,34 +384,37 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
"
|
"
|
||||||
:create-project="createProject"
|
:create-project="createProject"
|
||||||
:create-client="createClient"
|
:create-client="createClient"
|
||||||
:createTag="createTag"></TimeEntryMassActionRow>
|
:create-tag="createTag"
|
||||||
|
@submit="clearSelectionAndState"
|
||||||
|
@select-all="selectedTimeEntries = [...timeEntries]"
|
||||||
|
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<div v-for="entry in timeEntries" :key="entry.id">
|
<div v-for="entry in timeEntries" :key="entry.id">
|
||||||
<TimeEntryRow
|
<TimeEntryRow
|
||||||
:selected="selectedTimeEntries.includes(entry)"
|
:selected="selectedTimeEntries.includes(entry)"
|
||||||
@selected="selectedTimeEntries.push(entry)"
|
:can-create-project="canCreateProjects()"
|
||||||
:canCreateProject="canCreateProjects()"
|
:create-client
|
||||||
@unselected="
|
:create-project
|
||||||
selectedTimeEntries = selectedTimeEntries.filter(
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
(item) => item.id !== entry.id
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:createClient
|
|
||||||
:createProject
|
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:tags="tags"
|
:tags="tags"
|
||||||
:clients
|
:clients
|
||||||
:createTag
|
:create-tag
|
||||||
:updateTimeEntry
|
:update-time-entry
|
||||||
:onStartStopClick="() => startTimeEntryFromExisting(entry)"
|
:on-start-stop-click="() => startTimeEntryFromExisting(entry)"
|
||||||
:deleteTimeEntry="() => deleteTimeEntries([entry])"
|
:delete-time-entry="() => deleteTimeEntries([entry])"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
:members="members"
|
:members="members"
|
||||||
showDate
|
show-date
|
||||||
showMember
|
show-member
|
||||||
:time-entry="entry"></TimeEntryRow>
|
:time-entry="entry"
|
||||||
|
@selected="selectedTimeEntries.push(entry)"
|
||||||
|
@unselected="
|
||||||
|
selectedTimeEntries = selectedTimeEntries.filter(
|
||||||
|
(item) => item.id !== entry.id
|
||||||
|
)
|
||||||
|
"></TimeEntryRow>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="timeEntries.length === 0">
|
<div v-if="timeEntries.length === 0">
|
||||||
<div class="text-center pt-12">
|
<div class="text-center pt-12">
|
||||||
@@ -431,10 +431,10 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PaginationRoot
|
<PaginationRoot
|
||||||
|
v-model:page="currentPage"
|
||||||
:total="totalPages"
|
:total="totalPages"
|
||||||
:items-per-page="pageLimit"
|
:items-per-page="pageLimit"
|
||||||
class="flex justify-center items-center py-8"
|
class="flex justify-center items-center py-8"
|
||||||
v-model:page="currentPage"
|
|
||||||
:sibling-count="1"
|
:sibling-count="1"
|
||||||
show-edges>
|
show-edges>
|
||||||
<PaginationList
|
<PaginationList
|
||||||
@@ -485,13 +485,13 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</template>
|
</template>
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.navigation-item {
|
.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-white/80;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-item {
|
.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-white/80;
|
@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;
|
||||||
}
|
}
|
||||||
.pagination-item[data-selected] {
|
.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-white/80 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-ring transition ease-in-out duration-150;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ watch(currentPage, () => {
|
|||||||
v-if="isBillingActivated() && canManageBilling()"
|
v-if="isBillingActivated() && canManageBilling()"
|
||||||
href="/billing">
|
href="/billing">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
|
||||||
class="mt-6"
|
|
||||||
:icon="CreditCardIcon"
|
|
||||||
v-if="
|
v-if="
|
||||||
isBillingActivated() && canUpdateOrganization()
|
isBillingActivated() && canUpdateOrganization()
|
||||||
">
|
"
|
||||||
|
type="button"
|
||||||
|
class="mt-6"
|
||||||
|
:icon="CreditCardIcon">
|
||||||
Go to Billing
|
Go to Billing
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -120,10 +120,10 @@ watch(currentPage, () => {
|
|||||||
|
|
||||||
<PaginationRoot
|
<PaginationRoot
|
||||||
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
|
v-if="reports.length > 0 || isAllowedToPerformPremiumAction()"
|
||||||
|
v-model:page="currentPage"
|
||||||
:total="totalPages"
|
:total="totalPages"
|
||||||
:items-per-page="pageLimit"
|
:items-per-page="pageLimit"
|
||||||
class="flex justify-center items-center py-8"
|
class="flex justify-center items-center py-8"
|
||||||
v-model:page="currentPage"
|
|
||||||
:sibling-count="1"
|
:sibling-count="1"
|
||||||
show-edges>
|
show-edges>
|
||||||
<PaginationList
|
<PaginationList
|
||||||
@@ -174,13 +174,13 @@ watch(currentPage, () => {
|
|||||||
</template>
|
</template>
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.navigation-item {
|
.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-white/80;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-item {
|
.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-white/80;
|
@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;
|
||||||
}
|
}
|
||||||
.pagination-item[data-selected] {
|
.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-white/80 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-ring transition ease-in-out duration-150;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ function getGroupLabel(key: string) {
|
|||||||
<MainContainer>
|
<MainContainer>
|
||||||
<div class="pt-10 w-full px-3 relative">
|
<div class="pt-10 w-full px-3 relative">
|
||||||
<ReportingChart
|
<ReportingChart
|
||||||
:groupedType="aggregatedGraphTimeEntries?.grouped_type"
|
:grouped-type="aggregatedGraphTimeEntries?.grouped_type"
|
||||||
:groupedData="
|
:grouped-data="
|
||||||
aggregatedGraphTimeEntries?.grouped_data
|
aggregatedGraphTimeEntries?.grouped_data
|
||||||
"></ReportingChart>
|
"></ReportingChart>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,8 +217,8 @@ function getGroupLabel(key: string) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
class="chart flex flex-col items-center justify-center py-12 col-span-3"
|
v-else
|
||||||
v-else>
|
class="chart flex flex-col items-center justify-center py-12 col-span-3">
|
||||||
<p class="text-lg text-white font-semibold">
|
<p class="text-lg text-white font-semibold">
|
||||||
No time entries found
|
No time entries found
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ async function createTag(tag: string) {
|
|||||||
>Create Tag
|
>Create Tag
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<TagCreateModal
|
<TagCreateModal
|
||||||
:createTag="createTag"
|
v-model:show="showCreateTagModal"
|
||||||
v-model:show="showCreateTagModal"></TagCreateModal>
|
:create-tag="createTag"></TagCreateModal>
|
||||||
</MainContainer>
|
</MainContainer>
|
||||||
<TagTable :createTag="createTag"></TagTable>
|
<TagTable :create-tag="createTag"></TagTable>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -191,9 +191,9 @@ const showResultModal = ref(false);
|
|||||||
<div>
|
<div>
|
||||||
<InputLabel for="importType" value="Import Type" />
|
<InputLabel for="importType" value="Import Type" />
|
||||||
<select
|
<select
|
||||||
name="importType"
|
|
||||||
id="importType"
|
id="importType"
|
||||||
v-model="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">
|
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>
|
<option :value="null" selected disabled>
|
||||||
Select an import type to get instructions...
|
Select an import type to get instructions...
|
||||||
@@ -206,8 +206,8 @@ const showResultModal = ref(false);
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div
|
<div
|
||||||
class="py-3 text-white"
|
v-if="currentImporterDescription"
|
||||||
v-if="currentImporterDescription">
|
class="py-3 text-white">
|
||||||
<div class="font-semibold text-muted py-1">
|
<div class="font-semibold text-muted py-1">
|
||||||
Instructions:
|
Instructions:
|
||||||
</div>
|
</div>
|
||||||
@@ -233,12 +233,12 @@ const showResultModal = ref(false);
|
|||||||
>Upload a Toggl/Clockify Export</span
|
>Upload a Toggl/Clockify Export</span
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="importFile"
|
|
||||||
id="file-upload"
|
id="file-upload"
|
||||||
|
ref="importFile"
|
||||||
name="file-upload"
|
name="file-upload"
|
||||||
v-on:change="updateFiles"
|
|
||||||
type="file"
|
type="file"
|
||||||
class="sr-only" />
|
class="sr-only"
|
||||||
|
@change="updateFiles" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs leading-5 text-muted">
|
<p class="text-xs leading-5 text-muted">
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ function checkForConfirmationModal() {
|
|||||||
<template #form>
|
<template #form>
|
||||||
<OrganizationBillableRateModal
|
<OrganizationBillableRateModal
|
||||||
v-model:show="showConfirmationModal"
|
v-model:show="showConfirmationModal"
|
||||||
@submit="submit"
|
|
||||||
:new-billable-rate="
|
:new-billable-rate="
|
||||||
organizationBody.billable_rate
|
organizationBody.billable_rate
|
||||||
"></OrganizationBillableRateModal>
|
"
|
||||||
|
@submit="submit"></OrganizationBillableRateModal>
|
||||||
<!-- Organization Owner Information -->
|
<!-- Organization Owner Information -->
|
||||||
<div class="col-span-6">
|
<div class="col-span-6">
|
||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
@@ -75,8 +75,8 @@ function checkForConfirmationModal() {
|
|||||||
value="Organization Billable Rate" />
|
value="Organization Billable Rate" />
|
||||||
<BillableRateInput
|
<BillableRateInput
|
||||||
v-if="organization"
|
v-if="organization"
|
||||||
:currency="getOrganizationCurrencyString()"
|
|
||||||
v-model="organizationBody.billable_rate"
|
v-model="organizationBody.billable_rate"
|
||||||
|
:currency="getOrganizationCurrencyString()"
|
||||||
name="organizationBillableRate"></BillableRateInput>
|
name="organizationBillableRate"></BillableRateInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,10 +86,10 @@ function checkForConfirmationModal() {
|
|||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="organization"
|
v-if="organization"
|
||||||
|
id="organizationShowBillableRatesToEmployees"
|
||||||
v-model:checked="
|
v-model:checked="
|
||||||
organizationBody.employees_can_see_billable_rates
|
organizationBody.employees_can_see_billable_rates
|
||||||
"
|
"></Checkbox>
|
||||||
id="organizationShowBillableRatesToEmployees"></Checkbox>
|
|
||||||
<InputLabel
|
<InputLabel
|
||||||
for="organizationShowBillableRatesToEmployees"
|
for="organizationShowBillableRatesToEmployees"
|
||||||
value="Show Billable Rates to Employees" />
|
value="Show Billable Rates to Employees" />
|
||||||
|
|||||||
@@ -89,9 +89,9 @@ const updateTeamName = () => {
|
|||||||
<div class="col-span-6 sm:col-span-4">
|
<div class="col-span-6 sm:col-span-4">
|
||||||
<InputLabel for="currency" value="Currency" />
|
<InputLabel for="currency" value="Currency" />
|
||||||
<select
|
<select
|
||||||
name="currency"
|
|
||||||
id="currency"
|
id="currency"
|
||||||
v-model="form.currency"
|
v-model="form.currency"
|
||||||
|
name="currency"
|
||||||
:disabled="!permissions.canUpdateTeam"
|
: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">
|
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>
|
<option value="" disabled>Select a currency</option>
|
||||||
|
|||||||
@@ -120,16 +120,16 @@ function deleteSelected() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TimeEntryCreateModal
|
<TimeEntryCreateModal
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
v-model:show="showManualTimeEntryModal"
|
||||||
:createProject="createProject"
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
:createClient="createClient"
|
:create-project="createProject"
|
||||||
:createTag="createTag"
|
:create-client="createClient"
|
||||||
:createTimeEntry="createTimeEntry"
|
:create-tag="createTag"
|
||||||
|
:create-time-entry="createTimeEntry"
|
||||||
:projects
|
:projects
|
||||||
:tasks
|
:tasks
|
||||||
:tags
|
:tags
|
||||||
:clients
|
:clients></TimeEntryCreateModal>
|
||||||
v-model:show="showManualTimeEntryModal"></TimeEntryCreateModal>
|
|
||||||
<AppLayout title="Dashboard" data-testid="time_view">
|
<AppLayout title="Dashboard" data-testid="time_view">
|
||||||
<MainContainer
|
<MainContainer
|
||||||
class="pt-5 lg:pt-8 pb-4 lg:pb-6 border-b border-default-background-separator">
|
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">
|
<div class="pb-2 pt-2 lg:pt-0 lg:pl-4 flex justify-center">
|
||||||
<SecondaryButton
|
<SecondaryButton
|
||||||
class="w-full text-center flex justify-center"
|
class="w-full text-center flex justify-center"
|
||||||
@click="showManualTimeEntryModal = true"
|
|
||||||
:icon="PlusIcon"
|
:icon="PlusIcon"
|
||||||
|
@click="showManualTimeEntryModal = true"
|
||||||
>Manual time entry
|
>Manual time entry
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,12 +150,9 @@ function deleteSelected() {
|
|||||||
</MainContainer>
|
</MainContainer>
|
||||||
<TimeEntryMassActionRow
|
<TimeEntryMassActionRow
|
||||||
:selected-time-entries="selectedTimeEntries"
|
:selected-time-entries="selectedTimeEntries"
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
:canCreateProject="canCreateProjects()"
|
:can-create-project="canCreateProjects()"
|
||||||
@submit="clearSelectionAndState"
|
|
||||||
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
:all-selected="selectedTimeEntries.length === timeEntries.length"
|
||||||
@select-all="selectedTimeEntries = [...timeEntries]"
|
|
||||||
@unselect-all="selectedTimeEntries = []"
|
|
||||||
:delete-selected="deleteSelected"
|
:delete-selected="deleteSelected"
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
@@ -171,23 +168,26 @@ function deleteSelected() {
|
|||||||
"
|
"
|
||||||
:create-project="createProject"
|
:create-project="createProject"
|
||||||
:create-client="createClient"
|
:create-client="createClient"
|
||||||
:createTag="createTag"></TimeEntryMassActionRow>
|
:create-tag="createTag"
|
||||||
|
@submit="clearSelectionAndState"
|
||||||
|
@select-all="selectedTimeEntries = [...timeEntries]"
|
||||||
|
@unselect-all="selectedTimeEntries = []"></TimeEntryMassActionRow>
|
||||||
<TimeEntryGroupedTable
|
<TimeEntryGroupedTable
|
||||||
v-model:selected="selectedTimeEntries"
|
v-model:selected="selectedTimeEntries"
|
||||||
:createProject
|
:create-project
|
||||||
:enableEstimatedTime="isAllowedToPerformPremiumAction()"
|
:enable-estimated-time="isAllowedToPerformPremiumAction()"
|
||||||
:canCreateProject="canCreateProjects()"
|
:can-create-project="canCreateProjects()"
|
||||||
:clients
|
:clients
|
||||||
:createClient
|
:create-client
|
||||||
:updateTimeEntry
|
:update-time-entry
|
||||||
:updateTimeEntries
|
:update-time-entries
|
||||||
:deleteTimeEntries
|
:delete-time-entries
|
||||||
:createTimeEntry="startTimeEntry"
|
:create-time-entry="startTimeEntry"
|
||||||
:createTag
|
:create-tag
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:currency="getOrganizationCurrencyString()"
|
:currency="getOrganizationCurrencyString()"
|
||||||
:timeEntries="timeEntries"
|
:time-entries="timeEntries"
|
||||||
:tags="tags"></TimeEntryGroupedTable>
|
:tags="tags"></TimeEntryGroupedTable>
|
||||||
<div v-if="timeEntries.length === 0" class="text-center pt-12">
|
<div v-if="timeEntries.length === 0" class="text-center pt-12">
|
||||||
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>
|
<ClockIcon class="w-8 text-icon-default inline pb-2"></ClockIcon>
|
||||||
|
|||||||
995
resources/js/packages/ui/package-lock.json
generated
995
resources/js/packages/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,13 +57,13 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@floating-ui/vue": "^1.1.4",
|
"@floating-ui/vue": "^1.1.4",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@heroicons/vue": "^2.1.5",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^12.5.0",
|
||||||
"@zodios/core": "^10.9.6",
|
"@zodios/core": "^10.9.6",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"parse-duration": "^1.1.0",
|
"parse-duration": "^2.0.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vue": "^3.4.38",
|
"vue": "^3.5.0",
|
||||||
"vue-tsc": "^2.0.29"
|
"vue-tsc": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import { computed } from 'vue';
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
size: 'base' | 'large' | 'xlarge';
|
size?: 'base' | 'large' | 'xlarge';
|
||||||
tag: string;
|
tag?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
color: string;
|
color?: string;
|
||||||
border: boolean;
|
border?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
size: 'base',
|
size: 'base',
|
||||||
@@ -30,6 +30,13 @@ const borderClasses = computed(() => {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tagClasses = computed(() => {
|
||||||
|
if (props.tag === 'button') {
|
||||||
|
return 'hover:bg-tertiary';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -37,9 +44,10 @@ const borderClasses = computed(() => {
|
|||||||
:is="tag"
|
:is="tag"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
|
tagClasses,
|
||||||
badgeClasses[size],
|
badgeClasses[size],
|
||||||
borderClasses,
|
borderClasses,
|
||||||
'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',
|
'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',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
type: HtmlButtonType;
|
type?: HtmlButtonType;
|
||||||
icon?: Component;
|
icon?: Component;
|
||||||
loading: boolean;
|
loading?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
@@ -21,15 +21,15 @@ const props = withDefaults(
|
|||||||
<button
|
<button
|
||||||
:type="type"
|
:type="type"
|
||||||
:disabled="loading"
|
: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:ring-2 focus:ring-indigo-500 focus:ring-offset-2 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-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
|
||||||
<span
|
<span
|
||||||
:class="
|
:class="
|
||||||
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
|
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
|
||||||
">
|
">
|
||||||
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
||||||
<component
|
<component
|
||||||
v-if="props.icon && !loading"
|
|
||||||
:is="props.icon"
|
:is="props.icon"
|
||||||
|
v-if="props.icon && !loading"
|
||||||
class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
|
class="text-text-secondary w-4 -ml-0.5 mr-1"></component>
|
||||||
<span>
|
<span>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import LoadingSpinner from '../LoadingSpinner.vue';
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
type: HtmlButtonType;
|
type?: HtmlButtonType;
|
||||||
icon?: Component;
|
icon?: Component;
|
||||||
size: 'small' | 'base';
|
size?: 'small' | 'base';
|
||||||
loading: boolean;
|
loading?: boolean;
|
||||||
class?: string;
|
class?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,7 @@ const sizeClasses = {
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
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:border-input-border-active focus:outline-none focus:ring-0 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:outline-none focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring focus:border-transparent disabled:opacity-25 ease-in-out',
|
||||||
sizeClasses[props.size],
|
sizeClasses[props.size],
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
@@ -42,8 +42,8 @@ const sizeClasses = {
|
|||||||
">
|
">
|
||||||
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
<LoadingSpinner v-if="loading"></LoadingSpinner>
|
||||||
<component
|
<component
|
||||||
v-if="props.icon && !loading"
|
|
||||||
:is="props.icon"
|
:is="props.icon"
|
||||||
|
v-if="props.icon && !loading"
|
||||||
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
|
class="text-text-tertiary w-4 -ml-0.5 mr-1"></component>
|
||||||
<span>
|
<span>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ defineProps<{
|
|||||||
<h3
|
<h3
|
||||||
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
|
class="text-white font-bold text-sm lg:text-base flex items-center space-x-2 lg:space-x-2.5">
|
||||||
<component
|
<component
|
||||||
v-if="icon"
|
|
||||||
:is="icon"
|
:is="icon"
|
||||||
|
v-if="icon"
|
||||||
class="w-5 lg:w-6 text-icon-default"></component>
|
class="w-5 lg:w-6 text-icon-default"></component>
|
||||||
<span>
|
<span>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
|||||||
@@ -127,28 +127,28 @@ const highlightedItem = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dropdown width="120" v-model="open" :closeOnContentClick="true">
|
<Dropdown v-model="open" width="120" :close-on-content-click="true">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<slot name="trigger"></slot>
|
<slot name="trigger"></slot>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<input
|
<input
|
||||||
|
ref="searchInput"
|
||||||
:value="searchValue"
|
:value="searchValue"
|
||||||
|
data-testid="client_dropdown_search"
|
||||||
|
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 client..."
|
||||||
@input="updateSearchValue"
|
@input="updateSearchValue"
|
||||||
@keydown.enter="addClientIfNoneExists"
|
@keydown.enter="addClientIfNoneExists"
|
||||||
data-testid="client_dropdown_search"
|
|
||||||
@keydown.up.prevent="moveHighlightUp"
|
@keydown.up.prevent="moveHighlightUp"
|
||||||
@keydown.down.prevent="moveHighlightDown"
|
@keydown.down.prevent="moveHighlightDown" />
|
||||||
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 client..." />
|
|
||||||
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
|
<div ref="dropdownViewport" class="w-60 max-h-60 overflow-y-scroll">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
searchValue.length > 0 && filteredClients.length === 0
|
searchValue.length > 0 && filteredClients.length === 0
|
||||||
"
|
"
|
||||||
@click="addClientIfNoneExists"
|
class="bg-card-background-active"
|
||||||
class="bg-card-background-active">
|
@click="addClientIfNoneExists">
|
||||||
<div
|
<div
|
||||||
class="flex space-x-3 items-center px-4 py-3 text-xs text-white font-medium border-t rounded-b-lg border-card-background-separator">
|
class="flex space-x-3 items-center px-4 py-3 text-xs text-white font-medium border-t rounded-b-lg border-card-background-separator">
|
||||||
<PlusCircleIcon
|
<PlusCircleIcon
|
||||||
@@ -170,8 +170,8 @@ const highlightedItem = computed(() => {
|
|||||||
:data-client-id="client.id">
|
:data-client-id="client.id">
|
||||||
<ClientDropdownItem
|
<ClientDropdownItem
|
||||||
:selected="isClientSelected(client.id)"
|
:selected="isClientSelected(client.id)"
|
||||||
@click="updateClient(client.id)"
|
:name="client.name"
|
||||||
:name="client.name"></ClientDropdownItem>
|
@click="updateClient(client.id)"></ClientDropdownItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ const emit = defineEmits(['submit']);
|
|||||||
<InputLabel for="billable" value="Time Estimated" />
|
<InputLabel for="billable" value="Time Estimated" />
|
||||||
</div>
|
</div>
|
||||||
<DurationInput
|
<DurationInput
|
||||||
@submit="emit('submit')"
|
|
||||||
v-model="model"
|
v-model="model"
|
||||||
class="max-w-[150px]"></DurationInput>
|
class="max-w-[150px]"
|
||||||
|
@submit="emit('submit')"></DurationInput>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { computed } from 'vue';
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
size: string;
|
size?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
expanded: false,
|
expanded: false,
|
||||||
@@ -17,7 +17,7 @@ const expandedStatusClasses = computed(() => {
|
|||||||
if (props.expanded) {
|
if (props.expanded) {
|
||||||
return 'border-card-border border bg-card-background-active text-white';
|
return 'border-card-border border bg-card-background-active text-white';
|
||||||
}
|
}
|
||||||
return 'border-card-border border bg-card-background text-muted';
|
return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-muted';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const expandedStatusClasses = computed(() => {
|
|||||||
<button
|
<button
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'font-medium rounded flex items-center transition justify-center',
|
'font-medium rounded flex items-center transition justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent',
|
||||||
expandedStatusClasses,
|
expandedStatusClasses,
|
||||||
props.size
|
props.size
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ const inputValue = ref(formatValue(model.value));
|
|||||||
:id="name"
|
:id="name"
|
||||||
ref="billableRateInput"
|
ref="billableRateInput"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
@blur="updateRate($event.target.value)"
|
|
||||||
@keydown.enter="updateRate($event.target.value)"
|
|
||||||
type="text"
|
type="text"
|
||||||
:name="name"
|
:name="name"
|
||||||
placeholder="Billable Rate"
|
placeholder="Billable Rate"
|
||||||
class="block w-full"
|
class="block w-full"
|
||||||
autocomplete="teamMemberRate" />
|
autocomplete="teamMemberRate"
|
||||||
|
@blur="updateRate($event.target.value)"
|
||||||
|
@keydown.enter="updateRate($event.target.value)" />
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
|
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium pointer-events-none">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function toggleBillable() {
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
size: 'small' | 'base';
|
size?: 'small' | 'base';
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
size: 'base',
|
size: 'base',
|
||||||
@@ -35,19 +35,19 @@ const iconSizeClasses = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const iconSizeWrapperClasses =
|
const iconSizeWrapperClasses =
|
||||||
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-11 h-11';
|
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-10 h-10';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
@click="toggleBillable"
|
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
iconColorClasses,
|
iconColorClasses,
|
||||||
iconSizeWrapperClasses,
|
iconSizeWrapperClasses,
|
||||||
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
'flex-shrink-0 ring-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
||||||
)
|
)
|
||||||
">
|
"
|
||||||
|
@click="toggleBillable">
|
||||||
<BillableIcon :class="iconSizeClasses"></BillableIcon>
|
<BillableIcon :class="iconSizeClasses"></BillableIcon>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ const proxyChecked = computed({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input
|
<input
|
||||||
|
:id="id"
|
||||||
v-model="proxyChecked"
|
v-model="proxyChecked"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:id="id"
|
|
||||||
:value="value"
|
:value="value"
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:outline-none focus:ring-ring/50 focus-visible:outline-none focus-visible:ring-ring/50" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -48,10 +48,8 @@ const emit = defineEmits(['changed']);
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center text-muted">
|
<div class="flex items-center text-muted">
|
||||||
<input
|
<input
|
||||||
|
id="start"
|
||||||
ref="datePicker"
|
ref="datePicker"
|
||||||
@change="updateTempValue"
|
|
||||||
@blur="updateDate"
|
|
||||||
@keydown.enter="updateDate"
|
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md',
|
'bg-input-background border text-white border-input-border focus-visible:outline-0 focus-visible:border-input-border-active focus-visible:ring-0 rounded-md',
|
||||||
@@ -59,9 +57,11 @@ const emit = defineEmits(['changed']);
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
type="date"
|
type="date"
|
||||||
id="start"
|
|
||||||
name="trip-start"
|
name="trip-start"
|
||||||
:value="tempDate" />
|
:value="tempDate"
|
||||||
|
@change="updateTempValue"
|
||||||
|
@blur="updateDate"
|
||||||
|
@keydown.enter="updateDate" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
flip,
|
flip,
|
||||||
limitShift,
|
limitShift,
|
||||||
type Placement,
|
type Placement,
|
||||||
type ReferenceElement,
|
|
||||||
shift,
|
shift,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/vue';
|
} from '@floating-ui/vue';
|
||||||
@@ -15,8 +14,8 @@ import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
align: Placement;
|
align?: Placement;
|
||||||
closeOnContentClick: boolean;
|
closeOnContentClick?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
align: 'bottom-start',
|
align: 'bottom-start',
|
||||||
@@ -45,6 +44,11 @@ watch(open, (value) => {
|
|||||||
layers.value.push(id);
|
layers.value.push(id);
|
||||||
} else {
|
} else {
|
||||||
layers.value = layers.value.filter((layer) => layer !== id);
|
layers.value = layers.value.filter((layer) => layer !== id);
|
||||||
|
reference.value
|
||||||
|
?.querySelector<HTMLElement>(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,7 +73,7 @@ function onBackgroundClick() {
|
|||||||
open.value = false;
|
open.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reference = ref<null | ReferenceElement>(null);
|
const reference = ref<null | HTMLElement>(null);
|
||||||
const floating = ref(null);
|
const floating = ref(null);
|
||||||
const { floatingStyles } = useFloating(reference, floating, {
|
const { floatingStyles } = useFloating(reference, floating, {
|
||||||
placement: props.align,
|
placement: props.align,
|
||||||
@@ -90,7 +94,7 @@ const { floatingStyles } = useFloating(reference, floating, {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="min-w-0 isolate">
|
<div class="min-w-0 isolate">
|
||||||
<div @click.prevent="toggleOpen" ref="reference" class="min-w-0">
|
<div ref="reference" class="min-w-0" @click.prevent="toggleOpen">
|
||||||
<slot name="trigger" />
|
<slot name="trigger" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,8 +113,8 @@ const { floatingStyles } = useFloating(reference, floating, {
|
|||||||
leave-to-class="transform opacity-0 scale-95">
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
v-if="open"
|
||||||
class="z-50"
|
|
||||||
ref="floating"
|
ref="floating"
|
||||||
|
class="z-50"
|
||||||
:style="floatingStyles"
|
:style="floatingStyles"
|
||||||
@click="onContentClick">
|
@click="onContentClick">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -78,11 +78,11 @@ function updateTimeEntryInputValue() {
|
|||||||
<template>
|
<template>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
@blur="updateDuration"
|
|
||||||
@keydown.enter="updateDuration"
|
|
||||||
v-model="temporaryCustomTimerEntry"
|
v-model="temporaryCustomTimerEntry"
|
||||||
:class="twMerge('text-text-secondary', props.class)"
|
:class="twMerge('text-text-secondary', props.class)"
|
||||||
type="text" />
|
type="text"
|
||||||
|
@blur="updateDuration"
|
||||||
|
@keydown.enter="updateDuration" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ function updateAndSubmit() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
v-model="currentTime"
|
||||||
class="w-full overflow-hidden pr-14"
|
class="w-full overflow-hidden pr-14"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
@focus="selectInput"
|
@focus="selectInput"
|
||||||
@blur="updateDuration"
|
@blur="updateDuration"
|
||||||
@keydown.enter="updateAndSubmit"
|
@keydown.enter="updateAndSubmit">
|
||||||
v-model="currentTime">
|
|
||||||
</TextInput>
|
</TextInput>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium">
|
class="absolute top-0 right-0 h-full flex items-center px-4 font-medium">
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user