Add Euro Symbol as Billable Icon when EUR is the organization currency.

fixes #423
This commit is contained in:
Gregor Vostrak
2026-02-10 12:47:27 +01:00
parent 15e61e9789
commit 562ee234a8
18 changed files with 94 additions and 46 deletions

View File

@@ -201,7 +201,7 @@ const firstProjectId = computed(() => projects.value[0]?.id ?? '');
<TimeTrackerProjectTaskDropdown
v-model:project="currentTimeEntry.project_id"
v-model:task="currentTimeEntry.task_id"
variant="input"
variant="outline"
:projects="projects"
:tasks="tasks"
:clients="activeClients"
@@ -225,7 +225,7 @@ const firstProjectId = computed(() => projects.value[0]?.id ?? '');
<TimeTrackerProjectTaskDropdown
v-model:project="currentTimeEntry.project_id"
v-model:task="currentTimeEntry.task_id"
variant="input"
variant="outline"
:projects="projects"
:tasks="tasks"
:clients="activeClients"

View File

@@ -103,7 +103,7 @@ async function createTag(name: string) {
<Select v-model="billable" @update:model-value="emit('submit')">
<SelectTrigger
size="small"
size="sm"
variant="outline"
:active="billable !== null"
:show-chevron="false">

View File

@@ -25,7 +25,7 @@ const title = computed(() => {
<template>
<Select v-model="model" @update:model-value="emit('changed')">
<SelectTrigger size="small" :show-chevron="false">
<SelectTrigger size="sm" :show-chevron="false">
<SelectValue class="flex items-center gap-2">
<component :is="icon" class="h-4 text-icon-default" />
<span>{{ title }}</span>

View File

@@ -48,8 +48,7 @@ import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
import { getCurrentOrganizationId } from '@/utils/useUser';
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
import { twMerge } from 'tailwind-merge';
import { Button as UiButton } from '@/packages/ui/src';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import { openFeedback } from '@/utils/feedback';
import { CommandPaletteProvider } from '@/Components/CommandPalette';
import { useCommandPalette } from '@/utils/useCommandPalette';
@@ -263,14 +262,14 @@ const page = usePage<{
:icon="Cog6ToothIcon"
:href="route('profile.show')"></NavigationSidebarItem>
<UiButton
<Button
v-if="page.props.has_services_extension"
variant="outline"
size="xs"
class="rounded-full ml-2 flex h-6 w-6 items-center text-xs text-icon-default justify-center"
@click="openFeedback">
?
</UiButton>
</Button>
</ul>
</div>
</div>

View File

@@ -85,7 +85,7 @@ function updateValue(client: { id: string | null; name: string }) {
<UseFocusTrap v-if="open" :options="{ immediate: true, allowOutsideClick: true }">
<ComboboxRoot
v-model:search-term="searchValue"
:open="open"
v-model:open="open"
:model-value="currentClient"
class="relative"
@update:model-value="updateValue">

View File

@@ -1,14 +1,14 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { computed } from 'vue';
import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
import { getCurrentOrganizationId } from '@/utils/useUser';
import DollarIcon from './DollarIcon.vue';
import EuroIcon from './EuroIcon.vue';
const { organization } = useOrganizationQuery(getCurrentOrganizationId()!);
const icon = computed(() => (organization.value?.currency === 'EUR' ? EuroIcon : DollarIcon));
</script>
<template>
<svg viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 1V13M1 10.182L1.879 10.841C3.05 11.72 4.949 11.72 6.121 10.841C7.293 9.962 7.293 8.538 6.121 7.659C5.536 7.219 4.768 7 4 7C3.275 7 2.55 6.78 1.997 6.341C0.891 5.462 0.891 4.038 1.997 3.159C3.103 2.28 4.897 2.28 6.003 3.159L6.418 3.489"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<component :is="icon" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,10 @@
<template>
<svg viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4 1V13M1 10.182L1.879 10.841C3.05 11.72 4.949 11.72 6.121 10.841C7.293 9.962 7.293 8.538 6.121 7.659C5.536 7.219 4.768 7 4 7C3.275 7 2.55 6.78 1.997 6.341C0.891 5.462 0.891 4.038 1.997 3.159C3.103 2.28 4.897 2.28 6.003 3.159L6.418 3.489"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>

View File

@@ -0,0 +1,10 @@
<template>
<svg viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.44214 2.80599C8.64364 1.84779 7.44588 1.20898 6.0485 1.20898C3.85262 1.20898 2.05599 3.36494 2.05599 6C2.05599 8.63506 3.85262 10.791 6.0485 10.791C7.44588 10.791 8.64364 10.1522 9.44214 9.19401M1.05786 4.80225H6.64738M1.05786 7.19775H6.64738"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</template>

View File

@@ -2,6 +2,12 @@
import { computed } from 'vue';
import { twMerge } from 'tailwind-merge';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/packages/ui/src/tooltip';
const active = defineModel({ default: false });
const emit = defineEmits(['changed']);
function toggleBillable() {
@@ -12,12 +18,16 @@ function toggleBillable() {
const props = withDefaults(
defineProps<{
size?: 'small' | 'base';
faded?: boolean;
}>(),
{
size: 'base',
faded: false,
}
);
const tooltipLabel = computed(() => (active.value ? 'Billable' : 'Non Billable'));
const iconColorClasses = computed(() => {
if (active.value) {
return 'text-input-select-active focus:text-input-select-active-hover hover:text-input-select-active-hover';
@@ -38,17 +48,28 @@ const iconSizeWrapperClasses = props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8'
</script>
<template>
<button
:class="
twMerge(
iconColorClasses,
iconSizeWrapperClasses,
'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>
</button>
<TooltipProvider>
<Tooltip disable-closing-trigger>
<TooltipTrigger as-child>
<button
:aria-label="tooltipLabel"
:class="
twMerge(
iconColorClasses,
iconSizeWrapperClasses,
'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',
faded
? 'opacity-50 group-hover:opacity-100 focus-visible:opacity-100'
: ''
)
"
@click="toggleBillable">
<BillableIcon :class="iconSizeClasses"></BillableIcon>
</button>
</TooltipTrigger>
<TooltipContent>
{{ tooltipLabel }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>
<style scoped></style>

View File

@@ -8,7 +8,7 @@ import {
} from '@/packages/ui/src/utils/time';
import { Popover, PopoverContent, PopoverTrigger } from '@/packages/ui/src/popover';
import { Calendar } from '@/Components/ui/calendar';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import { CalendarIcon } from 'lucide-vue-next';
import { parseDate, type DateValue } from '@internationalized/date';
import type { Organization } from '@/packages/api/src';

View File

@@ -4,7 +4,7 @@ import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
import { getDayJsInstance, getLocalizedDayJs } from '@/packages/ui/src/utils/time';
import dayjs from 'dayjs';
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
const props = defineProps<{
start: string;

View File

@@ -5,7 +5,7 @@ import { computed, ref, watch } from 'vue';
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
import Checkbox from '@/packages/ui/src/Input/Checkbox.vue';
import type { Tag } from '@/packages/api/src';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
import {
ComboboxAnchor,
ComboboxContent,

View File

@@ -136,7 +136,7 @@ function onSelectChange(checked: boolean) {
<BillableToggleButton
:model-value="timeEntry.billable"
size="small"
class="opacity-50 focus-visible:opacity-100 group-hover:opacity-100"
faded
@changed="updateTimeEntryBillable"></BillableToggleButton>
<div class="flex-1">
<button

View File

@@ -21,7 +21,6 @@ import BillableToggleButton from '@/packages/ui/src/Input/BillableToggleButton.v
import { computed, ref } from 'vue';
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
import { Checkbox } from '@/packages/ui/src';
import { twMerge } from 'tailwind-merge';
const props = defineProps<{
timeEntry: TimeEntry;
@@ -150,9 +149,7 @@ async function handleDeleteTimeEntry() {
<BillableToggleButton
:model-value="timeEntry.billable"
size="small"
:class="
twMerge('opacity-50 group-hover:opacity-100 focus-visible:opacity-100')
"
faded
@changed="updateTimeEntryBillable"></BillableToggleButton>
<div class="flex-1">
<TimeEntryRangeSelector

View File

@@ -14,7 +14,7 @@ import type {
import { PlusIcon, PlusCircleIcon, MinusIcon, XMarkIcon } from '@heroicons/vue/16/solid';
import ProjectCreateModal from '@/packages/ui/src/Project/ProjectCreateModal.vue';
import { twMerge } from 'tailwind-merge';
import { Button } from '@/Components/ui/button';
import { Button } from '@/packages/ui/src/Buttons';
const task = defineModel<string | null>('task', {
default: null,
@@ -61,7 +61,7 @@ const props = withDefaults(
class?: string;
variant?: 'input' | 'ghost' | 'outline';
align?: 'center' | 'end' | 'start';
size?: 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'input';
size?: 'default' | 'xs' | 'sm' | 'lg' | 'icon';
}>(),
{
emptyPlaceholder: 'No Project',

View File

@@ -76,6 +76,17 @@ watch(
}
);
// Re-run filter when items are dynamically added/removed (e.g. entity search results)
watch(
allItems,
() => {
if (filterState.search) {
filterItems();
}
},
{ deep: true }
);
// Sync external searchTerm prop changes to internal state
watch(
() => props.searchTerm,

View File

@@ -54,7 +54,7 @@
--theme-color-input-background: var(--color-bg-secondary);
--theme-color-input-select-active: rgb(var(--color-accent-300));
--theme-color-input-select-active-hover: rgb(var(--color-accent-200));
--theme-color-input-select-active-hover: rgb(var(--color-accent-400));
--color-accent-default: rgba(var(--color-accent-300), 0.2);
--color-accent-foreground: rgb(var(--color-accent-100));

View File

@@ -20,10 +20,10 @@ import {
SunIcon,
MoonIcon,
ComputerDesktopIcon,
CurrencyDollarIcon,
ClipboardDocumentListIcon,
BuildingOfficeIcon,
} from '@heroicons/vue/20/solid';
import BillableIcon from '@/packages/ui/src/Icons/BillableIcon.vue';
import type { Organization } from '@/types/models';
export type CommandGroup =
@@ -340,7 +340,7 @@ export function createActiveTimerCommands(
{
id: 'timer-toggle-billable',
label: 'Toggle Billable',
icon: CurrencyDollarIcon,
icon: BillableIcon,
keywords: ['billable', 'non-billable', 'money'],
group: 'active-timer',
action: activeTimerActions.toggleBillable,