Files
iistwin/client/src/components/ContractNumberField.tsx
Ильяс Султанов 509eb05c0b fix(contract-number): input шифра заказчика снова редактируемый
- Убрано локальное состояние customerValue, теперь значение берётся напрямую из parsed.value

- Убран лишний useEffect, который мог сбрасывать ввод при глобальном редактировании

- Состояние mode продолжает синхронизироваться с value prop
2026-07-16 15:21:50 +03:00

209 lines
7.0 KiB
TypeScript
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.

import { useState, useEffect, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { apiRequest } from "@/lib/queryClient";
import { useQuery } from "@tanstack/react-query";
import { cn } from "@/lib/utils";
import { FileSignature, Loader2 } from "lucide-react";
export interface ContractNumberValue {
mode: 'mine' | 'customer';
value: string;
legalEntityTaskId?: number;
}
export interface ContractNumberConfig {
legalEntityFormId: number;
prefixFieldCode: string;
startNumber: number;
sequencePadding: number;
}
interface ContractNumberFieldProps {
value?: ContractNumberValue | string;
onChange: (value: ContractNumberValue) => void;
disabled?: boolean;
config?: ContractNumberConfig;
formId?: number;
fieldId?: number;
compact?: boolean;
}
function parseValue(val: ContractNumberValue | string | undefined): ContractNumberValue {
if (!val) return { mode: 'mine', value: '' };
if (typeof val === 'string') {
try {
return JSON.parse(val) as ContractNumberValue;
} catch {
return { mode: 'customer', value: val };
}
}
return val;
}
export default function ContractNumberField({
value,
onChange,
disabled = false,
config,
formId,
fieldId,
compact = false,
}: ContractNumberFieldProps) {
const parsed = parseValue(value);
const [mode, setMode] = useState<'mine' | 'customer'>(parsed.mode);
const customerValue = parsed.mode === 'customer' ? parsed.value : '';
const [preview, setPreview] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>('');
// Синхронизируем локальное состояние mode с value prop, чтобы при повторном открытии
// глобального редактирования переключатель соответствовал сохранённому значению.
useEffect(() => {
setMode(parsed.mode);
}, [parsed.mode]);
const legalEntityFormId = config?.legalEntityFormId;
const legalEntityTaskId = parsed.legalEntityTaskId;
// Fetch tasks from legal entity form (always, regardless of mode)
const { data: tasksData } = useQuery<{ tasks: Array<{ id: number; title: string }> }>({
queryKey: ['/api/forms', legalEntityFormId, 'tasks'],
enabled: !!legalEntityFormId,
});
// Fetch preview when mode=mine and legalEntityTaskId changes
const fetchPreview = useCallback(async () => {
if (mode !== 'mine' || !config || !legalEntityTaskId || !formId || !fieldId) {
setPreview('');
return;
}
setLoading(true);
setError('');
try {
const response = await apiRequest('POST', '/api/tasks/preview-contract-number', {
formId,
legalEntityTaskId,
fieldId,
});
const data = await response.json();
if (data.success) {
setPreview(data.preview);
} else {
setPreview('');
setError(data.error || '');
}
} catch (err) {
setPreview('');
setError('Ошибка загрузки preview');
} finally {
setLoading(false);
}
}, [mode, config, legalEntityTaskId, formId, fieldId]);
useEffect(() => {
fetchPreview();
}, [fetchPreview]);
const handleModeChange = (newMode: 'mine' | 'customer') => {
setMode(newMode);
if (newMode === 'mine') {
onChange({ mode: 'mine', value: '', legalEntityTaskId: parsed.legalEntityTaskId });
} else {
onChange({ mode: 'customer', value: parsed.value || '', legalEntityTaskId: parsed.legalEntityTaskId });
}
};
const handleCustomerChange = (val: string) => {
onChange({ mode: 'customer', value: val, legalEntityTaskId: parsed.legalEntityTaskId });
};
const handleLegalEntityChange = (taskId: string) => {
const num = taskId ? parseInt(taskId) : undefined;
onChange({ mode, value: mode === 'mine' ? (parsed.value || '') : customerValue, legalEntityTaskId: num });
};
const isGenerated = !!(parsed.mode === 'mine' && parsed.value && parsed.value !== '');
return (
<div className={cn(compact ? 'flex items-center gap-2 flex-wrap' : 'space-y-2')}>
<div className="flex items-center space-x-3 shrink-0">
<Switch
id={`contract-number-mode-${fieldId}`}
checked={mode === 'mine'}
onCheckedChange={(checked: boolean) => handleModeChange(checked ? 'mine' : 'customer')}
disabled={disabled || isGenerated}
/>
<Label htmlFor={`contract-number-mode-${fieldId}`} className="cursor-pointer">
{mode === 'mine' ? 'Мой номер' : 'Номер заказчика'}
</Label>
</div>
<div className={cn('flex-1 min-w-[160px]', compact ? 'max-w-[200px]' : 'w-full')}>
<Select
value={legalEntityTaskId?.toString() || ''}
onValueChange={handleLegalEntityChange}
disabled={disabled || isGenerated}
>
<SelectTrigger>
<SelectValue placeholder="Выберите юрлицо" />
</SelectTrigger>
<SelectContent>
{tasksData?.tasks?.map((task) => (
<SelectItem key={task.id} value={task.id.toString()}>
#{task.id} {task.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{mode === 'customer' ? (
<Input
type="text"
value={customerValue}
onChange={(e) => handleCustomerChange(e.target.value)}
placeholder="Введите шифр заказчика"
disabled={disabled}
className={cn('flex-1 min-w-[160px]', compact && 'h-7 leading-6 px-2 max-w-[220px]')}
/>
) : (
<div className={cn('relative flex-1 min-w-[160px]', compact && 'max-w-[220px]')}>
<div className={cn(
'flex items-center space-x-2 rounded-md border border-input bg-muted/30',
compact ? 'px-2 h-7' : 'px-3 py-2'
)}>
<FileSignature className="h-4 w-4 text-muted-foreground shrink-0" />
{isGenerated ? (
<span className="text-sm font-medium">{parsed.value}</span>
) : loading ? (
<div className="flex items-center space-x-2">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">Загрузка...</span>
</div>
) : preview ? (
<span className="text-sm font-medium text-primary">{preview}</span>
) : (
<span className="text-sm text-muted-foreground">
{legalEntityTaskId ? 'Не удалось загрузить номер' : 'Выберите юрлицо'}
</span>
)}
</div>
{error && (
<p className="text-xs text-destructive mt-1">{error}</p>
)}
</div>
)}
</div>
);
}