feat: replace all hardcoded en-US and $ formatting with locale-aware helpers
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
|
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { getLocaleCode } from '../utils/locale'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
@@ -29,7 +30,7 @@ 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('en-US', {
|
return date.toLocaleDateString(getLocaleCode(), {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -38,7 +39,7 @@ const displayText = computed(() => {
|
|||||||
|
|
||||||
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('en-US', { month: 'long', year: 'numeric' })
|
return date.toLocaleDateString(getLocaleCode(), { month: 'long', year: 'numeric' })
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Today helpers ───────────────────────────────────────────────────
|
// ── Today helpers ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ import {
|
|||||||
import { Clock } from 'lucide-vue-next'
|
import { Clock } from 'lucide-vue-next'
|
||||||
import { useEntriesStore } from '../stores/entries'
|
import { useEntriesStore } from '../stores/entries'
|
||||||
import { useProjectsStore } from '../stores/projects'
|
import { useProjectsStore } from '../stores/projects'
|
||||||
|
import { formatDateLong } from '../utils/locale'
|
||||||
import type { TimeEntry } from '../stores/entries'
|
import type { TimeEntry } from '../stores/entries'
|
||||||
|
|
||||||
// Register Chart.js components
|
// Register Chart.js components
|
||||||
@@ -117,12 +118,7 @@ const greeting = computed(() => {
|
|||||||
|
|
||||||
// Formatted date
|
// Formatted date
|
||||||
const formattedDate = computed(() => {
|
const formattedDate = computed(() => {
|
||||||
return new Date().toLocaleDateString('en-US', {
|
return formatDateLong(new Date().toISOString())
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Empty state check
|
// Empty state check
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ import AppSelect from '../components/AppSelect.vue'
|
|||||||
import AppDatePicker from '../components/AppDatePicker.vue'
|
import AppDatePicker from '../components/AppDatePicker.vue'
|
||||||
import { useEntriesStore, type TimeEntry } from '../stores/entries'
|
import { useEntriesStore, type TimeEntry } from '../stores/entries'
|
||||||
import { useProjectsStore } from '../stores/projects'
|
import { useProjectsStore } from '../stores/projects'
|
||||||
|
import { formatDate } from '../utils/locale'
|
||||||
|
|
||||||
const entriesStore = useEntriesStore()
|
const entriesStore = useEntriesStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
@@ -316,16 +317,6 @@ function formatDuration(seconds: number): string {
|
|||||||
return `${minutes}m`
|
return `${minutes}m`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date
|
|
||||||
function formatDate(dateString: string): string {
|
|
||||||
const date = new Date(dateString)
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
{{ formatDate(invoice.date) }}
|
{{ formatDate(invoice.date) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
|
<td class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
|
||||||
${{ invoice.total.toFixed(2) }}
|
{{ formatCurrency(invoice.total) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-center">
|
<td class="px-4 py-3 text-center">
|
||||||
<span
|
<span
|
||||||
@@ -194,19 +194,19 @@
|
|||||||
<div class="bg-bg-inset rounded-lg p-4">
|
<div class="bg-bg-inset rounded-lg p-4">
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Subtotal:</span>
|
<span>Subtotal:</span>
|
||||||
<span class="font-mono">${{ calculateSubtotal().toFixed(2) }}</span>
|
<span class="font-mono">{{ formatCurrency(calculateSubtotal()) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Tax ({{ createForm.tax_rate }}%):</span>
|
<span>Tax ({{ createForm.tax_rate }}%):</span>
|
||||||
<span class="font-mono">${{ calculateTax().toFixed(2) }}</span>
|
<span class="font-mono">{{ formatCurrency(calculateTax()) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Discount:</span>
|
<span>Discount:</span>
|
||||||
<span class="font-mono">-${{ createForm.discount.toFixed(2) }}</span>
|
<span class="font-mono">-{{ formatCurrency(createForm.discount) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-text-primary font-medium text-[0.8125rem] pt-2 border-t border-border-subtle">
|
<div class="flex justify-between text-text-primary font-medium text-[0.8125rem] pt-2 border-t border-border-subtle">
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span class="font-mono text-accent-text">${{ calculateTotal().toFixed(2) }}</span>
|
<span class="font-mono text-accent-text">{{ formatCurrency(calculateTotal()) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -266,19 +266,19 @@
|
|||||||
<div class="border-t border-border-subtle pt-4">
|
<div class="border-t border-border-subtle pt-4">
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Subtotal:</span>
|
<span>Subtotal:</span>
|
||||||
<span class="font-mono">${{ (selectedInvoice?.subtotal || 0).toFixed(2) }}</span>
|
<span class="font-mono">{{ formatCurrency(selectedInvoice?.subtotal || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Tax ({{ selectedInvoice?.tax_rate || 0 }}%):</span>
|
<span>Tax ({{ selectedInvoice?.tax_rate || 0 }}%):</span>
|
||||||
<span class="font-mono">${{ (selectedInvoice?.tax_amount || 0).toFixed(2) }}</span>
|
<span class="font-mono">{{ formatCurrency(selectedInvoice?.tax_amount || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
<div class="flex justify-between text-[0.75rem] text-text-secondary mb-2">
|
||||||
<span>Discount:</span>
|
<span>Discount:</span>
|
||||||
<span class="font-mono">-${{ (selectedInvoice?.discount || 0).toFixed(2) }}</span>
|
<span class="font-mono">-{{ formatCurrency(selectedInvoice?.discount || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between text-text-primary font-medium text-[0.8125rem] pt-2 border-t border-border-subtle">
|
<div class="flex justify-between text-text-primary font-medium text-[0.8125rem] pt-2 border-t border-border-subtle">
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span class="font-mono text-accent-text">${{ (selectedInvoice?.total || 0).toFixed(2) }}</span>
|
<span class="font-mono text-accent-text">{{ formatCurrency(selectedInvoice?.total || 0) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -339,6 +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'
|
||||||
|
|
||||||
const invoicesStore = useInvoicesStore()
|
const invoicesStore = useInvoicesStore()
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
@@ -369,17 +370,6 @@ function getClientName(clientId: number): string {
|
|||||||
return client?.name || 'Unknown Client'
|
return client?.name || 'Unknown Client'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date
|
|
||||||
function formatDate(dateString: string): string {
|
|
||||||
if (!dateString) return '-'
|
|
||||||
const date = new Date(dateString)
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate subtotal (simplified - would need line items in a real app)
|
// Calculate subtotal (simplified - would need line items in a real app)
|
||||||
function calculateSubtotal(): number {
|
function calculateSubtotal(): number {
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-[0.8125rem] font-semibold text-text-primary">{{ project.name }}</h3>
|
<h3 class="text-[0.8125rem] font-semibold text-text-primary">{{ project.name }}</h3>
|
||||||
<p class="text-xs text-text-secondary mt-0.5">{{ getClientName(project.client_id) }} · ${{ project.hourly_rate.toFixed(2) }}/hr</p>
|
<p class="text-xs text-text-secondary mt-0.5">{{ getClientName(project.client_id) }} · {{ formatCurrency(project.hourly_rate) }}/hr</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
||||||
<button
|
<button
|
||||||
@@ -200,6 +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'
|
||||||
|
|
||||||
const colorPresets = ['#D97706', '#3B82F6', '#8B5CF6', '#EC4899', '#10B981', '#EF4444', '#06B6D4', '#6B7280']
|
const colorPresets = ['#D97706', '#3B82F6', '#8B5CF6', '#EC4899', '#10B981', '#EF4444', '#06B6D4', '#6B7280']
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Earnings</p>
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Earnings</p>
|
||||||
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">${{ reportData.totalEarnings?.toFixed(2) || '0.00' }}</p>
|
<p class="text-[1.25rem] font-[family-name:var(--font-heading)] text-accent-text font-medium">{{ formatCurrency(reportData.totalEarnings || 0) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Projects</p>
|
<p class="text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1">Projects</p>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span class="text-[0.75rem] font-mono text-accent-text">{{ formatHours(projectData.total_seconds) }}</span>
|
<span class="text-[0.75rem] font-mono text-accent-text">{{ formatHours(projectData.total_seconds) }}</span>
|
||||||
<span class="text-[0.75rem] font-mono text-text-secondary">${{ ((projectData.total_seconds / 3600) * getProjectRate(projectData.project_id)).toFixed(2) }}</span>
|
<span class="text-[0.75rem] font-mono text-text-secondary">{{ formatCurrency((projectData.total_seconds / 3600) * getProjectRate(projectData.project_id)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full bg-bg-elevated rounded-full h-[2px]">
|
<div class="w-full bg-bg-elevated rounded-full h-[2px]">
|
||||||
@@ -120,6 +120,7 @@ import {
|
|||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
import { useEntriesStore } from '../stores/entries'
|
import { useEntriesStore } from '../stores/entries'
|
||||||
import { useProjectsStore } from '../stores/projects'
|
import { useProjectsStore } from '../stores/projects'
|
||||||
|
import { formatCurrency } from '../utils/locale'
|
||||||
|
|
||||||
// Register Chart.js components
|
// Register Chart.js components
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
||||||
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDate(entry.start_time) }}</p>
|
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDateTime(entry.start_time) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,6 +110,7 @@ import { useEntriesStore } from '../stores/entries'
|
|||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import { Timer as TimerIcon } from 'lucide-vue-next'
|
import { Timer as TimerIcon } from 'lucide-vue-next'
|
||||||
import AppSelect from '../components/AppSelect.vue'
|
import AppSelect from '../components/AppSelect.vue'
|
||||||
|
import { formatDateTime } from '../utils/locale'
|
||||||
|
|
||||||
const timerStore = useTimerStore()
|
const timerStore = useTimerStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
@@ -200,17 +201,6 @@ function formatDuration(seconds: number): string {
|
|||||||
return `${minutes}m`
|
return `${minutes}m`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format date
|
|
||||||
function formatDate(dateString: string): string {
|
|
||||||
const date = new Date(dateString)
|
|
||||||
return date.toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load data on mount
|
// Load data on mount
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
Reference in New Issue
Block a user