| import React, { useEffect, useState } from 'react'; |
| import { cn } from '@/lib/utils'; |
|
|
| const baseInput = |
| 'w-full min-w-0 rounded-md border border-transparent bg-transparent px-1.5 py-0.5 text-sm text-slate-800 ' + |
| 'hover:border-slate-200 focus:border-violet-300 focus:bg-white focus:outline-none focus:ring-1 focus:ring-violet-200'; |
|
|
| |
| |
| |
| export function EditableCell({ |
| value, |
| onCommit, |
| className = '', |
| inputClassName = '', |
| type = 'text', |
| multiline = false, |
| disabled = false, |
| }) { |
| const str = value == null || value === undefined ? '' : String(value); |
| const [local, setLocal] = useState(str); |
|
|
| useEffect(() => { |
| setLocal(str); |
| }, [str]); |
|
|
| const commit = () => { |
| if (disabled) return; |
| const next = multiline ? local : local.trim(); |
| const prev = multiline ? str : str.trim(); |
| if (next !== prev) onCommit(next); |
| }; |
|
|
| const stop = (e) => e.stopPropagation(); |
|
|
| if (multiline) { |
| return ( |
| <textarea |
| className={cn(baseInput, 'resize-y min-h-[2.5rem] max-w-full', className, inputClassName)} |
| value={local} |
| onChange={(e) => setLocal(e.target.value)} |
| onBlur={commit} |
| onKeyDown={stop} |
| disabled={disabled} |
| rows={2} |
| /> |
| ); |
| } |
|
|
| return ( |
| <input |
| type={type} |
| className={cn(baseInput, className, inputClassName)} |
| value={local} |
| onChange={(e) => setLocal(e.target.value)} |
| onBlur={commit} |
| onKeyDown={(e) => { |
| stop(e); |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| e.currentTarget.blur(); |
| } |
| }} |
| disabled={disabled} |
| /> |
| ); |
| } |
|
|
| function usdWholeString(value) { |
| if (value == null || value === '') return ''; |
| const n = Math.round(Number(value)); |
| return Number.isFinite(n) ? String(n) : ''; |
| } |
|
|
| function formatUsdWholeDisplay(value) { |
| const s = usdWholeString(value); |
| if (s === '') return ''; |
| return new Intl.NumberFormat('en-US', { |
| style: 'currency', |
| currency: 'USD', |
| maximumFractionDigits: 0, |
| minimumFractionDigits: 0, |
| }).format(Number(s)); |
| } |
|
|
| function parseUsdWholeInput(raw) { |
| const cleaned = String(raw ?? '') |
| .replace(/[$,\s]/g, '') |
| .trim(); |
| if (cleaned === '') return null; |
| const n = Math.round(Number(cleaned)); |
| return Number.isFinite(n) ? n : NaN; |
| } |
|
|
| |
| |
| |
| |
| export function EditableCurrencyCell({ |
| value, |
| onCommit, |
| className = '', |
| inputClassName = '', |
| disabled = false, |
| }) { |
| const canonical = usdWholeString(value); |
| const [focused, setFocused] = useState(false); |
| const [local, setLocal] = useState(() => |
| canonical ? formatUsdWholeDisplay(canonical) : '' |
| ); |
|
|
| useEffect(() => { |
| if (focused) return; |
| setLocal(canonical ? formatUsdWholeDisplay(canonical) : ''); |
| }, [canonical, focused]); |
|
|
| const commit = () => { |
| if (disabled) return; |
| const parsed = parseUsdWholeInput(local); |
| if (parsed === null) { |
| if (canonical !== '') onCommit(''); |
| setLocal(''); |
| setFocused(false); |
| return; |
| } |
| if (Number.isNaN(parsed)) { |
| setLocal(canonical ? formatUsdWholeDisplay(canonical) : ''); |
| setFocused(false); |
| return; |
| } |
| const next = String(parsed); |
| if (next !== canonical) onCommit(next); |
| setLocal(formatUsdWholeDisplay(next)); |
| setFocused(false); |
| }; |
|
|
| const handleFocus = () => { |
| if (disabled) return; |
| setFocused(true); |
| setLocal(canonical); |
| }; |
|
|
| const handleBlur = () => { |
| commit(); |
| }; |
|
|
| return ( |
| <input |
| type="text" |
| inputMode="decimal" |
| autoComplete="off" |
| className={cn(baseInput, className, inputClassName)} |
| value={local} |
| onChange={(e) => setLocal(e.target.value)} |
| onFocus={handleFocus} |
| onBlur={handleBlur} |
| onKeyDown={(e) => { |
| e.stopPropagation(); |
| if (e.key === 'Enter') { |
| e.preventDefault(); |
| e.currentTarget.blur(); |
| } |
| }} |
| disabled={disabled} |
| /> |
| ); |
| } |
|
|
| function toDateInputValue(iso) { |
| if (!iso) return ''; |
| const d = new Date(iso); |
| if (Number.isNaN(d.getTime())) return ''; |
| return d.toISOString().slice(0, 10); |
| } |
|
|
| |
| |
| |
| export function EditableDateCell({ value, onCommit, className = '', disabled = false }) { |
| const [local, setLocal] = useState(() => toDateInputValue(value)); |
|
|
| useEffect(() => { |
| setLocal(toDateInputValue(value)); |
| }, [value]); |
|
|
| const commit = () => { |
| if (disabled) return; |
| const prev = toDateInputValue(value); |
| if (local !== prev) onCommit(local); |
| }; |
|
|
| return ( |
| <input |
| type="date" |
| className={cn(baseInput, 'max-w-[11rem]', className)} |
| value={local} |
| onChange={(e) => setLocal(e.target.value)} |
| onBlur={commit} |
| onKeyDown={(e) => e.stopPropagation()} |
| disabled={disabled} |
| /> |
| ); |
| } |
|
|