This commit is contained in:
phaichayon
2026-06-23 22:13:08 +07:00
parent 99a4087099
commit c1ecd5ea50
32 changed files with 3503 additions and 150 deletions

View File

@@ -0,0 +1,217 @@
'use client';
import * as React from 'react';
import { Calendar } from '@/components/ui/calendar';
import { Icons } from '@/components/icons';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
InputGroup,
InputGroupAddon,
InputGroupInput
} from '@/components/ui/input-group';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { formatCrmDate, formatCrmNumber, sanitizeDecimalInput } from '../shared/formats';
export const CRM_TEXTAREA_SIZE_CLASS = {
sm: 'min-h-20',
md: 'min-h-28',
lg: 'min-h-40'
} as const;
function parseDateValue(value: string | null | undefined) {
if (!value) {
return undefined;
}
const [year, month, day] = value.split('-').map(Number);
if (!year || !month || !day) {
return undefined;
}
return new Date(year, month - 1, day);
}
function toDateValue(date?: Date) {
if (!date) {
return '';
}
return [
String(date.getFullYear()),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0')
].join('-');
}
export function CrmDateInput({
value,
onChange,
placeholder = 'DD/MM/YYYY',
className
}: {
value?: string | null;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}) {
const selectedDate = parseDateValue(value);
return (
<Popover>
<PopoverTrigger asChild>
<Button
type='button'
variant='outline'
className={cn(
'w-full justify-start overflow-hidden text-left font-normal',
!value && 'text-muted-foreground',
className
)}
>
<Icons.calendar className='mr-2 h-4 w-4 shrink-0' />
<span className='truncate'>{value ? formatCrmDate(value) : placeholder}</span>
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto p-0' align='start'>
<Calendar
mode='single'
selected={selectedDate}
onSelect={(date) => onChange(toDateValue(date))}
initialFocus
/>
</PopoverContent>
</Popover>
);
}
function useFormattedNumericValue(value: string | number | null | undefined, options?: Intl.NumberFormatOptions) {
const [isFocused, setIsFocused] = React.useState(false);
const displayValue = React.useMemo(() => {
if (value === '' || value === null || value === undefined) {
return '';
}
return isFocused ? String(value).replaceAll(',', '') : formatCrmNumber(value, options);
}, [isFocused, options, value]);
return {
displayValue,
isFocused,
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false)
};
}
export function CrmNumberInput({
value,
onChange,
min,
max,
step,
placeholder,
className
}: {
value: string;
onChange: (value: string) => void;
min?: number;
max?: number;
step?: number | string;
placeholder?: string;
className?: string;
}) {
return (
<Input
type='number'
inputMode='decimal'
value={value}
min={min}
max={max}
step={step}
placeholder={placeholder}
className={cn('w-full', className)}
onChange={(event) => onChange(event.target.value)}
/>
);
}
export function CrmCurrencyInput({
value,
onChange,
currencyLabel = 'THB',
placeholder = '0.00',
className
}: {
value: string;
onChange: (value: string) => void;
currencyLabel?: string;
placeholder?: string;
className?: string;
}) {
const { displayValue, onBlur, onFocus } = useFormattedNumericValue(value, {
minimumFractionDigits: value ? 2 : 0,
maximumFractionDigits: 2
});
return (
<InputGroup className={className}>
<InputGroupAddon>{currencyLabel}</InputGroupAddon>
<InputGroupInput
type='text'
inputMode='decimal'
placeholder={placeholder}
value={displayValue}
onFocus={onFocus}
onBlur={onBlur}
onChange={(event) => onChange(sanitizeDecimalInput(event.target.value))}
/>
</InputGroup>
);
}
export function CrmPercentageInput({
value,
onChange,
placeholder = '0',
className
}: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}) {
return (
<InputGroup className={className}>
<InputGroupInput
type='number'
inputMode='decimal'
min={0}
max={100}
step='0.01'
placeholder={placeholder}
value={value}
onChange={(event) => onChange(event.target.value)}
/>
<InputGroupAddon align='inline-end'>%</InputGroupAddon>
</InputGroup>
);
}
export function CrmTextarea({
size = 'md',
className,
...props
}: React.ComponentProps<typeof Textarea> & {
size?: keyof typeof CRM_TEXTAREA_SIZE_CLASS;
}) {
return (
<Textarea
className={cn(CRM_TEXTAREA_SIZE_CLASS[size], 'resize-y', className)}
{...props}
/>
);
}