Files
iistwin/_tablefield_orig.txt
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

142 lines
5.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

> function TableFieldSelect({ formField, value, onChange }: {
formField: FormFieldData;
value: any;
onChange: (value: string) => void;
}) {
const options = formField.options && !Array.isArray(formField.options) ? formField.options : {};
const tableId = options.tableId;
const displayColumn = options.displayColumn || '';
const { data: tableData, isLoading, error } = useQuery<{ table: { rows: Array<{ id: number; values: string[] }>; co
lumns: Array<{ name: string }> } }>({
queryKey: ['/api/directories', tableId],
enabled: !!tableId,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
retry: false,
});
const [searchTerm, setSearchTerm] = useState('');
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
if (!tableId) {
return <div className="text-sm text-muted-foreground">Таблица не настроена</div>;
}
if (isLoading) {
return <Input disabled className="h-8" placeholder="Загрузка..." />;
}
if (error) {
return <div className="text-sm text-destructive">Нет доступа к таблице</div>;
}
const table = tableData?.table;
if (!table || !table.rows?.length) {
return <div className="text-sm text-muted-foreground">Таблица пуста</div>;
}
// Находим индекс столбца для отображения, с fallback на первый столбец
let displayColumnIndex = table.columns?.findIndex(c => c.name === displayColumn);
if (displayColumnIndex === undefined || displayColumnIndex < 0) {
displayColumnIndex = 0;
}
const getDisplayValue = (rowId: string | number) => {
const row = table.rows?.find(r => r.id.toString() === rowId?.toString());
return row?.values[displayColumnIndex!] || row?.values[0] || '';
};
const selectedRow = table.rows?.find(r => r.id.toString() === value?.toString());
const selectedDisplay = selectedRow ? (selectedRow.values[displayColumnIndex!] || selectedRow.values[0] || '') : ''
;
const sortedRows = useMemo(() => {
return [...table.rows].sort((a, b) => {
const av = a.values[displayColumnIndex!] || a.values[0] || '';
const bv = b.values[displayColumnIndex!] || b.values[0] || '';
return av.localeCompare(bv, 'ru');
});
}, [table.rows, displayColumnIndex]);
const filtered = useMemo(() => {
if (!searchTerm.trim()) return sortedRows;
const term = searchTerm.toLowerCase();
return sortedRows.filter(r => {
const display = r.values[displayColumnIndex!] || r.values[0] || '';
return display.toLowerCase().includes(term);
});
}, [sortedRows, searchTerm, displayColumnIndex]);
const handleSelect = (rowId: string) => {
onChange(rowId);
setSearchTerm('');
setIsOpen(false);
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverAnchor asChild>
<div className="relative w-full">
<Input
ref={inputRef}
className="h-8"
placeholder={formField.placeholder || 'Выберите значение...'}
value={isOpen ? searchTerm : (selectedDisplay || '')}
onChange={(e) => {
setSearchTerm(e.target.value);
setIsOpen(true);
}}
onFocus={() => {
setSearchTerm('');
setIsOpen(true);
}}
data-testid={`select-table-${formField.code}`}
/>
</div>
</PopoverAnchor>
<PopoverContent
className="p-0"
style={{ width: 'var(--radix-popper-anchor-width)' }}
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(e) => {
if (inputRef.current && inputRef.current.contains(e.target as Node)) {
e.preventDefault();
}
}}
>
<ul className="max-h-60 overflow-auto py-1">
<li
className="cursor-pointer px-3 py-2 hover:bg-accent text-sm text-muted-foreground"
onMouseDown={(e) => { e.preventDefault(); handleSelect(''); }}
>
—
</li>
{filtered.map(row => (
<li
key={row.id}
className="cursor-pointer px-3 py-2 hover:bg-accent"
onMouseDown={(e) => { e.preventDefault(); handleSelect(row.id.toString()); }}
>
<div className="flex items-center gap-2 text-sm">
<Table className="w-3 h-3 text-muted-foreground" />
{row.values[displayColumnIndex!] || row.values[0]}
</div>
</li>
))}
{filtered.length === 0 && (
<li className="px-3 py-2 text-sm text-muted-foreground">Нет совпадений</li>
)}
</ul>
</PopoverContent>
</Popover>
);
}
// Компонент выбора связанной задачи из другой формы
function TaskFieldSelect({ formField, value, onChange }: {