mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 03:32:15 +01:00
Compare commits
3 Commits
main
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3f19ebbed | ||
|
|
16baafa50d | ||
|
|
50e279d466 |
@@ -6,8 +6,8 @@ import {
|
|||||||
} from '@/Components/ui/popover';
|
} from '@/Components/ui/popover';
|
||||||
import { Button } from '@/Components/ui/button';
|
import { Button } from '@/Components/ui/button';
|
||||||
import { Calendar } from '@/Components/ui/calendar';
|
import { Calendar } from '@/Components/ui/calendar';
|
||||||
import { CalendarIcon } from 'lucide-vue-next';
|
import { CalendarIcon, XIcon } from 'lucide-vue-next';
|
||||||
import { formatDateLocalized } from '@/packages/ui/src/utils/time';
|
import { formatDate } from '@/packages/ui/src/utils/time';
|
||||||
import { parseDate } from '@internationalized/date';
|
import { parseDate } from '@internationalized/date';
|
||||||
import { computed, inject, type ComputedRef } from 'vue';
|
import { computed, inject, type ComputedRef } from 'vue';
|
||||||
import { type Organization } from '@/packages/api/src';
|
import { type Organization } from '@/packages/api/src';
|
||||||
@@ -17,6 +17,10 @@ const emit = defineEmits<{
|
|||||||
blur: [];
|
blur: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
clearable?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const handleChange = (date: string) => {
|
const handleChange = (date: string) => {
|
||||||
model.value = date;
|
model.value = date;
|
||||||
};
|
};
|
||||||
@@ -25,6 +29,11 @@ const handleBlur = () => {
|
|||||||
emit('blur');
|
emit('blur');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClear = (event: Event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
model.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
const date = computed(() => {
|
const date = computed(() => {
|
||||||
return model.value ? parseDate(model.value) : undefined;
|
return model.value ? parseDate(model.value) : undefined;
|
||||||
});
|
});
|
||||||
@@ -44,7 +53,17 @@ const organization = inject<ComputedRef<Organization>>('organization');
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
{{ model ? formatDateLocalized(model, organization?.date_format) : 'Pick a date' }}
|
<span class="flex-1">
|
||||||
|
{{ model ? formatDate(model, organization?.date_format) : 'Pick a date' }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="clearable && model"
|
||||||
|
class="ml-2 hover:bg-muted rounded p-1 transition-colors"
|
||||||
|
type="button"
|
||||||
|
@click="handleClear"
|
||||||
|
>
|
||||||
|
<XIcon class="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-auto p-0">
|
<PopoverContent class="w-auto p-0">
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const InvoiceResource = z
|
|||||||
status: z.string(),
|
status: z.string(),
|
||||||
date: z.string(),
|
date: z.string(),
|
||||||
due_at: z.string(),
|
due_at: z.string(),
|
||||||
|
paid_date: z.string(),
|
||||||
created_at: z.union([z.string(), z.null()]),
|
created_at: z.union([z.string(), z.null()]),
|
||||||
updated_at: z.union([z.string(), z.null()]),
|
updated_at: z.union([z.string(), z.null()]),
|
||||||
})
|
})
|
||||||
@@ -76,7 +77,7 @@ const InvoiceDiscountType = z.enum(['percentage', 'fixed']);
|
|||||||
const InvoiceStoreRequest = z
|
const InvoiceStoreRequest = z
|
||||||
.object({
|
.object({
|
||||||
due_at: z.union([z.string(), z.null()]).optional(),
|
due_at: z.union([z.string(), z.null()]).optional(),
|
||||||
paid_at: z.union([z.string(), z.null()]).optional(),
|
paid_date: z.union([z.string(), z.null()]).optional(),
|
||||||
seller_name: z.string(),
|
seller_name: z.string(),
|
||||||
seller_vatin: z.union([z.string(), z.null()]).optional(),
|
seller_vatin: z.union([z.string(), z.null()]).optional(),
|
||||||
seller_address_line_1: z.union([z.string(), z.null()]).optional(),
|
seller_address_line_1: z.union([z.string(), z.null()]).optional(),
|
||||||
@@ -102,8 +103,13 @@ const InvoiceStoreRequest = z
|
|||||||
billing_period_end: z.union([z.string(), z.null()]).optional(),
|
billing_period_end: z.union([z.string(), z.null()]).optional(),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
tax_rate: z.number().int().optional(),
|
tax_rate: z.number().int().gte(0).lte(2147483647).optional(),
|
||||||
discount_amount: z.number().int().optional(),
|
discount_amount: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(0)
|
||||||
|
.lte(9223372036854776000)
|
||||||
|
.optional(),
|
||||||
discount_type: InvoiceDiscountType.optional(),
|
discount_type: InvoiceDiscountType.optional(),
|
||||||
footer: z.union([z.string(), z.null()]).optional(),
|
footer: z.union([z.string(), z.null()]).optional(),
|
||||||
notes: z.union([z.string(), z.null()]).optional(),
|
notes: z.union([z.string(), z.null()]).optional(),
|
||||||
@@ -115,8 +121,12 @@ const InvoiceStoreRequest = z
|
|||||||
.object({
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.union([z.string(), z.null()]).optional(),
|
description: z.union([z.string(), z.null()]).optional(),
|
||||||
unit_price: z.number().int().gte(0).lte(99999999),
|
unit_price: z
|
||||||
quantity: z.number().gte(0),
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(0)
|
||||||
|
.lte(9223372036854776000),
|
||||||
|
quantity: z.number().gte(0).lte(99999999),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
)
|
)
|
||||||
@@ -161,7 +171,7 @@ const DetailedInvoiceResource = z
|
|||||||
buyer_address_country: z.string(),
|
buyer_address_country: z.string(),
|
||||||
buyer_phone: z.string(),
|
buyer_phone: z.string(),
|
||||||
buyer_email: z.string(),
|
buyer_email: z.string(),
|
||||||
paid_at: z.union([z.string(), z.null()]),
|
paid_date: z.string(),
|
||||||
due_at: z.string(),
|
due_at: z.string(),
|
||||||
discount_type: z.string(),
|
discount_type: z.string(),
|
||||||
discount_amount: z.number().int(),
|
discount_amount: z.number().int(),
|
||||||
@@ -185,7 +195,7 @@ const InvoiceUpdateRequest = z
|
|||||||
.object({
|
.object({
|
||||||
status: InvoiceStatus,
|
status: InvoiceStatus,
|
||||||
due_at: z.union([z.string(), z.null()]),
|
due_at: z.union([z.string(), z.null()]),
|
||||||
paid_at: z.union([z.string(), z.null()]),
|
paid_date: z.union([z.string(), z.null()]),
|
||||||
seller_name: z.string(),
|
seller_name: z.string(),
|
||||||
seller_vatin: z.union([z.string(), z.null()]),
|
seller_vatin: z.union([z.string(), z.null()]),
|
||||||
seller_address_line_1: z.union([z.string(), z.null()]),
|
seller_address_line_1: z.union([z.string(), z.null()]),
|
||||||
@@ -211,8 +221,8 @@ const InvoiceUpdateRequest = z
|
|||||||
billing_period_end: z.union([z.string(), z.null()]),
|
billing_period_end: z.union([z.string(), z.null()]),
|
||||||
reference: z.string(),
|
reference: z.string(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
tax_rate: z.number().int(),
|
tax_rate: z.number().int().gte(0).lte(2147483647),
|
||||||
discount_amount: z.number().int(),
|
discount_amount: z.number().int().gte(0).lte(9223372036854776000),
|
||||||
discount_type: InvoiceDiscountType,
|
discount_type: InvoiceDiscountType,
|
||||||
footer: z.union([z.string(), z.null()]),
|
footer: z.union([z.string(), z.null()]),
|
||||||
notes: z.union([z.string(), z.null()]),
|
notes: z.union([z.string(), z.null()]),
|
||||||
@@ -224,8 +234,12 @@ const InvoiceUpdateRequest = z
|
|||||||
id: z.union([z.string(), z.null()]).optional(),
|
id: z.union([z.string(), z.null()]).optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.union([z.string(), z.null()]).optional(),
|
description: z.union([z.string(), z.null()]).optional(),
|
||||||
unit_price: z.number().int().gte(0).lte(99999999),
|
unit_price: z
|
||||||
quantity: z.number().gte(0),
|
.number()
|
||||||
|
.int()
|
||||||
|
.gte(0)
|
||||||
|
.lte(9223372036854776000),
|
||||||
|
quantity: z.number().gte(0).lte(99999999),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ function onSelectChange(checked: boolean) {
|
|||||||
"></BillableToggleButton>
|
"></BillableToggleButton>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<button
|
<button
|
||||||
:class="twMerge('hidden lg:block text-text-secondary w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary', organization?.time_format === '12-hours' ? 'w-[160px]' : 'w-[110px]')"
|
:class="twMerge('text-text-secondary w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary', organization?.time_format === '12-hours' ? 'w-[160px]' : 'w-[110px]')"
|
||||||
@click="expanded = !expanded">
|
@click="expanded = !expanded">
|
||||||
{{ formatStartEnd(timeEntry.start, timeEntry.end, organization?.time_format) }}
|
{{ formatStartEnd(timeEntry.start, timeEntry.end, organization?.time_format) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="text-text-primary min-w-[90px] px-2.5 py-1.5 bg-transparent text-right hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
|
class="text-text-primary min-w-[90px] px-2.5 py-1.5 bg-transparent text-right hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
|
||||||
@click="expanded = !expanded">
|
@click="expanded = !expanded">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(
|
formatHumanReadableDuration(
|
||||||
@@ -173,7 +173,7 @@ function onSelectChange(checked: boolean) {
|
|||||||
|
|
||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||||
class="opacity-20 hidden sm:flex group-hover:opacity-100 focus-visible:opacity-100"
|
class="opacity-20 flex group-hover:opacity-100 focus-visible:opacity-100"
|
||||||
@changed="
|
@changed="
|
||||||
onStartStopClick(timeEntry)
|
onStartStopClick(timeEntry)
|
||||||
"></TimeTrackerStartStop>
|
"></TimeTrackerStartStop>
|
||||||
|
|||||||
@@ -144,7 +144,6 @@ function onSelectChange(checked : boolean) {
|
|||||||
"></BillableToggleButton>
|
"></BillableToggleButton>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<TimeEntryRangeSelector
|
<TimeEntryRangeSelector
|
||||||
class="hidden lg:block"
|
|
||||||
:start="timeEntry.start"
|
:start="timeEntry.start"
|
||||||
:end="timeEntry.end"
|
:end="timeEntry.end"
|
||||||
:show-date
|
:show-date
|
||||||
@@ -160,7 +159,7 @@ function onSelectChange(checked : boolean) {
|
|||||||
"></TimeEntryRowDurationInput>
|
"></TimeEntryRowDurationInput>
|
||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||||
class="opacity-20 hidden sm:flex focus-visible:opacity-100 group-hover:opacity-100"
|
class="opacity-20 flex focus-visible:opacity-100 group-hover:opacity-100"
|
||||||
@changed="onStartStopClick"></TimeTrackerStartStop>
|
@changed="onStartStopClick"></TimeTrackerStartStop>
|
||||||
<TimeEntryMoreOptionsDropdown
|
<TimeEntryMoreOptionsDropdown
|
||||||
@delete="
|
@delete="
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ function selectInput(event: Event) {
|
|||||||
v-model="currentTime"
|
v-model="currentTime"
|
||||||
data-testid="time_entry_duration_input"
|
data-testid="time_entry_duration_input"
|
||||||
name="Duration"
|
name="Duration"
|
||||||
class="text-text-primary w-[90px] px-2.5 py-1.5 bg-transparent text-right hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
|
class="text-text-primary w-[90px] px-2.5 py-1.5 bg-transparent text-right hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
@focus="selectInput"
|
@focus="selectInput"
|
||||||
@keydown.tab="open = false"
|
@keydown.tab="open = false"
|
||||||
@blur="updateTimerAndStartLiveTimerUpdate"
|
@blur="updateTimerAndStartLiveTimerUpdate"
|
||||||
|
|||||||
@@ -160,12 +160,29 @@ export function formatWeek(date: string | null): string {
|
|||||||
* @param date - date in the format of 'YYYY-MM-DD'
|
* @param date - date in the format of 'YYYY-MM-DD'
|
||||||
*/
|
*/
|
||||||
export function formatHumanReadableDate(date: string) {
|
export function formatHumanReadableDate(date: string) {
|
||||||
if (dayjs(date).isToday()) {
|
const dateObj = dayjs(date);
|
||||||
|
const today = dayjs();
|
||||||
|
|
||||||
|
if (dateObj.isToday()) {
|
||||||
return 'Today';
|
return 'Today';
|
||||||
} else if (dayjs(date).isYesterday()) {
|
} else if (dateObj.isYesterday()) {
|
||||||
return 'Yesterday';
|
return 'Yesterday';
|
||||||
}
|
}
|
||||||
return dayjs(date).fromNow();
|
|
||||||
|
// Calculate difference in days
|
||||||
|
const diffInDays = today.diff(dateObj, 'day');
|
||||||
|
|
||||||
|
if (diffInDays > 0 && diffInDays <= 30) {
|
||||||
|
// For dates in the past (2-30 days ago)
|
||||||
|
return `${diffInDays} ${diffInDays === 1 ? 'day' : 'days'} ago`;
|
||||||
|
} else if (diffInDays < 0 && diffInDays >= -30) {
|
||||||
|
// For dates in the future (within 30 days)
|
||||||
|
const futureDays = Math.abs(diffInDays);
|
||||||
|
return `In ${futureDays} ${futureDays === 1 ? 'day' : 'days'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For dates older than 30 days, show the actual date
|
||||||
|
return dateObj.format('MMM D, YYYY');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatWeekday(date: string) {
|
export function formatWeekday(date: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user