mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
108 lines
4.5 KiB
Vue
108 lines
4.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import { CheckIcon } from '@heroicons/vue/16/solid';
|
|
import DurationSecondsInput from '@/packages/ui/src/Input/DurationSecondsInput.vue';
|
|
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/packages/ui/src/tooltip';
|
|
import type { TimesheetCell } from '@/utils/useTimesheetGrid';
|
|
import type { CellSaveStatus } from '@/utils/timesheet/useTimesheetCellMutations';
|
|
|
|
const props = defineProps<{
|
|
cell?: TimesheetCell;
|
|
dayIndex: number;
|
|
date: string;
|
|
isToday: boolean;
|
|
hasRunningEntry: boolean;
|
|
readonly?: boolean;
|
|
saveStatus?: CellSaveStatus;
|
|
pendingSeconds?: number;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
update: [newSeconds: number];
|
|
}>();
|
|
|
|
// Show the optimistic value while saving; `??` (not `||`) so a pending 0 (delete) wins.
|
|
const displaySeconds = computed(() => props.pendingSeconds ?? props.cell?.totalSeconds ?? 0);
|
|
const isSaving = computed(() => props.saveStatus === 'saving');
|
|
|
|
// A cell is non-editable while its entry is running or when the row itself is
|
|
// read-only (e.g. a leftover break row after breaks were disabled). Both render
|
|
// the same disabled input, differing only in the tooltip explanation.
|
|
const isReadonly = computed(() => props.hasRunningEntry || props.readonly === true);
|
|
const readonlyTooltip = computed(() =>
|
|
props.hasRunningEntry
|
|
? 'Stop the running time entry to edit the timesheet'
|
|
: 'Breaks are disabled for this organization'
|
|
);
|
|
|
|
// Swap the border color (don't layer) to avoid same-specificity fights.
|
|
const inputClass = computed(() => {
|
|
const border = props.saveStatus === 'error' ? 'border-red-500/70' : 'border-input-border';
|
|
return [
|
|
'w-[80px] mx-auto text-center font-medium',
|
|
'bg-transparent text-text-primary placeholder:text-text-quaternary',
|
|
'rounded-lg border shadow-none',
|
|
border,
|
|
'hover:bg-card-background',
|
|
'focus-visible:bg-tertiary focus-visible:border-transparent',
|
|
'focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
|
|
'disabled:cursor-wait disabled:opacity-70',
|
|
].join(' ');
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
data-testid="timesheet_cell"
|
|
class="flex items-center justify-center border-t border-default-background-separator"
|
|
:class="{ 'bg-default-background': isToday }">
|
|
<TooltipProvider v-if="isReadonly" :delay-duration="100">
|
|
<Tooltip>
|
|
<TooltipTrigger as-child>
|
|
<span class="inline-block cursor-not-allowed">
|
|
<DurationSecondsInput
|
|
:model-value="cell?.totalSeconds ?? 0"
|
|
disabled
|
|
default-unit="hours"
|
|
placeholder="-"
|
|
size="sm"
|
|
input-class="w-[80px] mx-auto text-center font-medium
|
|
bg-transparent text-text-primary placeholder:text-text-quaternary
|
|
rounded-lg border border-input-border shadow-none
|
|
pointer-events-none
|
|
disabled:opacity-50 disabled:cursor-not-allowed" />
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{{ readonlyTooltip }}</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
<template v-else>
|
|
<span class="relative inline-flex items-center">
|
|
<DurationSecondsInput
|
|
:model-value="displaySeconds"
|
|
default-unit="hours"
|
|
placeholder="-"
|
|
size="sm"
|
|
:disabled="isSaving"
|
|
:input-class="inputClass"
|
|
@commit="(seconds) => emit('update', seconds ?? 0)" />
|
|
<span
|
|
v-if="saveStatus === 'saving' || saveStatus === 'saved'"
|
|
class="pointer-events-none absolute left-full top-1/2 ml-1.5 flex -translate-y-1/2 items-center"
|
|
:aria-label="saveStatus === 'saving' ? 'Saving' : 'Saved'">
|
|
<LoadingSpinner
|
|
v-if="saveStatus === 'saving'"
|
|
class="h-3 w-3 m-0 text-text-tertiary" />
|
|
<CheckIcon v-else class="h-3 w-3 text-text-tertiary" />
|
|
</span>
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</template>
|