Files
iistwin/client/src/components/ContractNumberField.tsx
Ильяс Султанов 2b3a9f768b fix(ui): плоский inline-оверлей и компактный редактор номера договора
- FieldOverlay: border-input, shadow-sm, убран overflow-hidden по умолчанию

- TaskDetail: input-редакторы h-7 leading-6, для contract-number fitOverlay=false

- ContractNumberField: добавлен compact-режим, подполя не обрезаются

- Создание задачи оставлено с вертикальным layout
2026-07-15 15:38:22 +03:00

206 lines
6.8 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, setCustomerValue] = useState(parsed.mode === 'customer' ? parsed.value : '');
const [preview, setPreview] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>('');
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: customerValue, legalEntityTaskId: parsed.legalEntityTaskId });
}
};
const handleCustomerChange = (val: string) => {
setCustomerValue(val);
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 flex-col gap-2' : 'space-y-2')}>
<div className={cn('flex items-center', compact ? 'gap-3 flex-wrap' : 'space-x-3')}>
<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-[180px]', compact && '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>
</div>
{mode === 'customer' ? (
<Input
type="text"
value={customerValue}
onChange={(e) => handleCustomerChange(e.target.value)}
placeholder="Введите шифр заказчика"
disabled={disabled}
className={cn(compact && 'h-7 leading-6 px-2')}
/>
) : (
<div className="relative">
<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>
);
}