fix: dynamic currency symbols and integrated datetime picker
- Replace all hardcoded prefix="$" with :prefix="getCurrencySymbol()" in Settings, Projects, and Invoices views - Replace hardcoded ($) labels with dynamic currency symbol - Extend AppDatePicker with showTime prop + hour/minute v-models for integrated date+time selection - Simplify Entries.vue to use single AppDatePicker with showTime instead of separate hour/minute inputs
This commit is contained in:
@@ -6,14 +6,22 @@ import { getLocaleCode } from '../utils/locale'
|
|||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
|
showTime?: boolean
|
||||||
|
hour?: number
|
||||||
|
minute?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
placeholder: 'Select date',
|
placeholder: 'Select date',
|
||||||
|
showTime: false,
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string]
|
'update:modelValue': [value: string]
|
||||||
|
'update:hour': [value: number]
|
||||||
|
'update:minute': [value: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
@@ -30,13 +38,44 @@ const displayText = computed(() => {
|
|||||||
if (!props.modelValue) return null
|
if (!props.modelValue) return null
|
||||||
const [y, m, d] = props.modelValue.split('-').map(Number)
|
const [y, m, d] = props.modelValue.split('-').map(Number)
|
||||||
const date = new Date(y, m - 1, d)
|
const date = new Date(y, m - 1, d)
|
||||||
return date.toLocaleDateString(getLocaleCode(), {
|
const datePart = date.toLocaleDateString(getLocaleCode(), {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})
|
})
|
||||||
|
if (props.showTime) {
|
||||||
|
const hh = String(props.hour).padStart(2, '0')
|
||||||
|
const mm = String(props.minute).padStart(2, '0')
|
||||||
|
return `${datePart} ${hh}:${mm}`
|
||||||
|
}
|
||||||
|
return datePart
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ── Time helpers ──────────────────────────────────────────────────────
|
||||||
|
const internalHour = ref(props.hour)
|
||||||
|
const internalMinute = ref(props.minute)
|
||||||
|
|
||||||
|
watch(() => props.hour, (v) => { internalHour.value = v })
|
||||||
|
watch(() => props.minute, (v) => { internalMinute.value = v })
|
||||||
|
|
||||||
|
function onHourInput(e: Event) {
|
||||||
|
const val = parseInt((e.target as HTMLInputElement).value)
|
||||||
|
if (!isNaN(val)) {
|
||||||
|
const clamped = Math.min(23, Math.max(0, val))
|
||||||
|
internalHour.value = clamped
|
||||||
|
emit('update:hour', clamped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMinuteInput(e: Event) {
|
||||||
|
const val = parseInt((e.target as HTMLInputElement).value)
|
||||||
|
if (!isNaN(val)) {
|
||||||
|
const clamped = Math.min(59, Math.max(0, val))
|
||||||
|
internalMinute.value = clamped
|
||||||
|
emit('update:minute', clamped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const viewMonthLabel = computed(() => {
|
const viewMonthLabel = computed(() => {
|
||||||
const date = new Date(viewYear.value, viewMonth.value, 1)
|
const date = new Date(viewYear.value, viewMonth.value, 1)
|
||||||
return date.toLocaleDateString(getLocaleCode(), { month: 'long', year: 'numeric' })
|
return date.toLocaleDateString(getLocaleCode(), { month: 'long', year: 'numeric' })
|
||||||
@@ -354,6 +393,28 @@ onBeforeUnmount(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Time inputs (when showTime is true) -->
|
||||||
|
<div v-if="showTime" class="border-t border-border-subtle px-3 py-2.5 flex items-center justify-center gap-2">
|
||||||
|
<span class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em]">Time</span>
|
||||||
|
<input
|
||||||
|
:value="String(internalHour).padStart(2, '0')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="23"
|
||||||
|
@input="onHourInput"
|
||||||
|
class="w-11 px-1.5 py-1 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary text-center font-mono focus:outline-none focus:border-border-visible [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
/>
|
||||||
|
<span class="text-text-tertiary text-sm font-mono">:</span>
|
||||||
|
<input
|
||||||
|
:value="String(internalMinute).padStart(2, '0')"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
@input="onMinuteInput"
|
||||||
|
class="w-11 px-1.5 py-1 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary text-center font-mono focus:outline-none focus:border-border-visible [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Today shortcut -->
|
<!-- Today shortcut -->
|
||||||
<div class="border-t border-border-subtle px-3 py-2">
|
<div class="border-t border-border-subtle px-3 py-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -166,32 +166,14 @@
|
|||||||
<!-- Start Date & Time -->
|
<!-- Start Date & Time -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Start Date & Time</label>
|
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Start Date & Time</label>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="flex-1">
|
|
||||||
<AppDatePicker
|
<AppDatePicker
|
||||||
v-model="editDate"
|
v-model="editDate"
|
||||||
placeholder="Date"
|
:show-time="true"
|
||||||
|
v-model:hour="editHour"
|
||||||
|
v-model:minute="editMinute"
|
||||||
|
placeholder="Date & Time"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input
|
|
||||||
v-model="editHour"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="23"
|
|
||||||
class="w-12 px-2 py-2 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary text-center font-mono focus:outline-none focus:border-border-visible [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
||||||
/>
|
|
||||||
<span class="text-text-tertiary text-sm font-mono">:</span>
|
|
||||||
<input
|
|
||||||
v-model="editMinute"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="59"
|
|
||||||
class="w-12 px-2 py-2 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary text-center font-mono focus:outline-none focus:border-border-visible [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<div class="flex justify-end gap-3 pt-4">
|
<div class="flex justify-end gap-3 pt-4">
|
||||||
|
|||||||
@@ -168,13 +168,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Discount ($)</label>
|
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Discount ({{ getCurrencySymbol() }})</label>
|
||||||
<AppNumberInput
|
<AppNumberInput
|
||||||
v-model="createForm.discount"
|
v-model="createForm.discount"
|
||||||
:min="0"
|
:min="0"
|
||||||
:step="1"
|
:step="1"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
prefix="$"
|
:prefix="getCurrencySymbol()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,7 +339,7 @@ import { useInvoicesStore, type Invoice } from '../stores/invoices'
|
|||||||
import { useClientsStore } from '../stores/clients'
|
import { useClientsStore } from '../stores/clients'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import { generateInvoicePdf } from '../utils/invoicePdf'
|
import { generateInvoicePdf } from '../utils/invoicePdf'
|
||||||
import { formatDate, formatCurrency } from '../utils/locale'
|
import { formatDate, formatCurrency, getCurrencySymbol } from '../utils/locale'
|
||||||
|
|
||||||
const invoicesStore = useInvoicesStore()
|
const invoicesStore = useInvoicesStore()
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
|
|||||||
@@ -104,13 +104,13 @@
|
|||||||
|
|
||||||
<!-- Hourly Rate -->
|
<!-- Hourly Rate -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Hourly Rate ($)</label>
|
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Hourly Rate ({{ getCurrencySymbol() }})</label>
|
||||||
<AppNumberInput
|
<AppNumberInput
|
||||||
v-model="formData.hourly_rate"
|
v-model="formData.hourly_rate"
|
||||||
:min="0"
|
:min="0"
|
||||||
:step="1"
|
:step="1"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
prefix="$"
|
:prefix="getCurrencySymbol()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ import AppSelect from '../components/AppSelect.vue'
|
|||||||
import { useProjectsStore, type Project } from '../stores/projects'
|
import { useProjectsStore, type Project } from '../stores/projects'
|
||||||
import { useClientsStore } from '../stores/clients'
|
import { useClientsStore } from '../stores/clients'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { formatCurrency } from '../utils/locale'
|
import { formatCurrency, getCurrencySymbol } from '../utils/locale'
|
||||||
|
|
||||||
const colorPresets = ['#D97706', '#3B82F6', '#8B5CF6', '#EC4899', '#10B981', '#EF4444', '#06B6D4', '#6B7280']
|
const colorPresets = ['#D97706', '#3B82F6', '#8B5CF6', '#EC4899', '#10B981', '#EF4444', '#06B6D4', '#6B7280']
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
:min="0"
|
:min="0"
|
||||||
:step="1"
|
:step="1"
|
||||||
:precision="2"
|
:precision="2"
|
||||||
prefix="$"
|
:prefix="getCurrencySymbol()"
|
||||||
@update:model-value="saveSettings"
|
@update:model-value="saveSettings"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +241,7 @@ import { useSettingsStore } from '../stores/settings'
|
|||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import AppNumberInput from '../components/AppNumberInput.vue'
|
import AppNumberInput from '../components/AppNumberInput.vue'
|
||||||
import AppSelect from '../components/AppSelect.vue'
|
import AppSelect from '../components/AppSelect.vue'
|
||||||
import { LOCALES, getCurrencies } from '../utils/locale'
|
import { LOCALES, getCurrencies, getCurrencySymbol } from '../utils/locale'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user