- FieldOverlay: border-input, shadow-sm, убран overflow-hidden по умолчанию - TaskDetail: input-редакторы h-7 leading-6, для contract-number fitOverlay=false - ContractNumberField: добавлен compact-режим, подполя не обрезаются - Создание задачи оставлено с вертикальным layout
206 lines
6.8 KiB
TypeScript
206 lines
6.8 KiB
TypeScript
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>
|
||
);
|
||
}
|