16535ad7f3
- Girişler gün gün, gün toplamıyla; satırda kategori, grup, ödeme türü, açıklama - Düzenleme aynı formda (sürüm kontrolü, pasif kategori korunur) - Silmede "Geri al" bildirimi; grup bazlı bütçe / harcanan / kalan özeti Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
4.3 KiB
TypeScript
132 lines
4.3 KiB
TypeScript
/**
|
||
* Tüm sayı / para / tarih biçimlendirmesi buradan yapılır (KAKEIBO_PROMPT §9).
|
||
*
|
||
* - Para: backend'den **kuruş cinsinden tamsayı** gelir → `1.250,00 ₺`
|
||
* (para birimi rakamın sağında, arada bir boşluk)
|
||
* - Miktar (altın, döviz): milyonda bir birim (`_e6`) → `12,50 gr`
|
||
* - Tarih: `GG.AA.YYYY`
|
||
*/
|
||
|
||
export type CurrencyCode = 'TRY' | 'USD' | 'EUR' | 'XAU_GRAM'
|
||
|
||
const CURRENCY_SUFFIX: Record<CurrencyCode, string> = {
|
||
TRY: '₺',
|
||
USD: '$',
|
||
EUR: '€',
|
||
XAU_GRAM: 'gr',
|
||
}
|
||
|
||
const numberFormats = new Map<number, Intl.NumberFormat>()
|
||
function numberFormat(decimals: number): Intl.NumberFormat {
|
||
let f = numberFormats.get(decimals)
|
||
if (!f) {
|
||
f = new Intl.NumberFormat('tr-TR', {
|
||
minimumFractionDigits: decimals,
|
||
maximumFractionDigits: decimals,
|
||
})
|
||
numberFormats.set(decimals, f)
|
||
}
|
||
return f
|
||
}
|
||
|
||
/** Ondalıklı sayıyı Türkçe biçimler: `1250.5` → `1.250,50` */
|
||
export function formatNumber(value: number, decimals = 2): string {
|
||
return numberFormat(decimals).format(value)
|
||
}
|
||
|
||
/** Kuruş → `1.250,00 ₺`. `signed: true` ise pozitiflere `+` eklenir. */
|
||
export function formatMoney(
|
||
kurus: number,
|
||
currency: CurrencyCode = 'TRY',
|
||
options: { signed?: boolean } = {},
|
||
): string {
|
||
const text = `${formatNumber(kurus / 100)} ${CURRENCY_SUFFIX[currency]}`
|
||
return options.signed && kurus > 0 ? `+${text}` : text
|
||
}
|
||
|
||
/** Milyonda bir birim → `12,50 gr` (altın, döviz miktarı) */
|
||
export function formatQuantity(e6: number, unit: string, decimals = 2): string {
|
||
return `${formatNumber(e6 / 1_000_000, decimals)} ${unit}`
|
||
}
|
||
|
||
/** Yüzde: `0.123` → `%12,3` (Türkçede % işareti başta) */
|
||
export function formatPercent(ratio: number, decimals = 1): string {
|
||
return `%${formatNumber(ratio * 100, decimals)}`
|
||
}
|
||
|
||
/**
|
||
* Kullanıcının yazdığı tutarı kuruşa çevirir. Geçersizse `null`.
|
||
*
|
||
* Kabul edilenler: `1250` · `1.250` · `1250,5` · `1.250,50` · `1250.50` (telefon klavyesi)
|
||
* Kural: virgül varsa ondalık ayırıcıdır, noktalar binliktir.
|
||
* Virgül yoksa ve son noktadan sonra 1–2 rakam varsa o nokta ondalıktır.
|
||
* Float hatası olmasın diye hesap metin üzerinden yapılır.
|
||
*/
|
||
export function parseMoney(input: string): number | null {
|
||
let s = input.replace(/[\s₺]/g, '')
|
||
if (!s) return null
|
||
|
||
let negative = false
|
||
if (s.startsWith('-')) {
|
||
negative = true
|
||
s = s.slice(1)
|
||
}
|
||
|
||
let intPart: string
|
||
let fracPart = ''
|
||
if (s.includes(',')) {
|
||
const parts = s.split(',')
|
||
if (parts.length !== 2) return null
|
||
intPart = parts[0].replace(/\./g, '')
|
||
fracPart = parts[1]
|
||
} else {
|
||
const m = /^(.*)\.(\d{1,2})$/.exec(s)
|
||
if (m && !m[1].includes('.')) {
|
||
intPart = m[1]
|
||
fracPart = m[2]
|
||
} else {
|
||
intPart = s.replace(/\./g, '')
|
||
}
|
||
}
|
||
|
||
if (intPart === '') intPart = '0'
|
||
if (!/^\d+$/.test(intPart) || !/^\d{0,2}$/.test(fracPart)) return null
|
||
|
||
const kurus = Number(intPart) * 100 + Number(fracPart.padEnd(2, '0'))
|
||
if (!Number.isSafeInteger(kurus)) return null
|
||
return negative ? -kurus : kurus
|
||
}
|
||
|
||
/** `"2026-12-09"` veya ISO zaman damgası → `09.12.2026` */
|
||
export function formatDate(value: string | Date): string {
|
||
if (typeof value === 'string') {
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
||
if (m) return `${m[3]}.${m[2]}.${m[1]}`
|
||
value = new Date(value)
|
||
}
|
||
const dd = String(value.getDate()).padStart(2, '0')
|
||
const mm = String(value.getMonth() + 1).padStart(2, '0')
|
||
return `${dd}.${mm}.${value.getFullYear()}`
|
||
}
|
||
|
||
/** Yerel tarih → `"2026-12-09"` (backend'in takvim tarihi biçimi) */
|
||
export function toIsoDate(date: Date): string {
|
||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||
const dd = String(date.getDate()).padStart(2, '0')
|
||
return `${date.getFullYear()}-${mm}-${dd}`
|
||
}
|
||
|
||
/** `"2026-12-07"` → `07.12` (hafta aralıkları gibi kısa gösterimler) */
|
||
export function formatDayMonth(iso: string): string {
|
||
const m = /^\d{4}-(\d{2})-(\d{2})$/.exec(iso)
|
||
return m ? `${m[2]}.${m[1]}` : iso
|
||
}
|
||
|
||
const weekdayFormat = new Intl.DateTimeFormat('tr-TR', { weekday: 'long', timeZone: 'UTC' })
|
||
|
||
/** `"2026-11-12"` → `Perşembe` (UTC üzerinden: saat dilimi günü kaydırmaz) */
|
||
export function formatWeekday(iso: string): string {
|
||
const [y, m, d] = iso.split('-').map(Number)
|
||
return weekdayFormat.format(new Date(Date.UTC(y, m - 1, d)))
|
||
}
|