Files
mkd/components/finance/PaymentCalendarEntryForm.tsx

318 lines
12 KiB
TypeScript
Raw Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect } from 'react';
import {
PaymentCalendarEntry,
PaymentDirection,
PaymentType,
PaymentProbability,
PaymentCategory
} from '../../types';
import { apiClient } from '../../services/apiClient';
import { X, Save, Calendar, Wallet, ArrowDownCircle, ArrowUpCircle, Banknote } from 'lucide-react';
interface PaymentCalendarEntryFormProps {
entry?: PaymentCalendarEntry | null;
currentUserId: string;
defaultDirection?: PaymentDirection;
onSave: (entry: Partial<PaymentCalendarEntry>) => Promise<void>;
onCancel: () => void;
}
const DIRECTION_OPTIONS: { value: PaymentDirection; label: string; icon: React.ReactNode }[] = [
{ value: 'outgoing', label: 'Расход', icon: <ArrowUpCircle className="w-4 h-4" /> },
{ value: 'incoming', label: 'Поступление', icon: <ArrowDownCircle className="w-4 h-4" /> }
];
const TYPE_OPTIONS: { value: PaymentType; label: string; icon: React.ReactNode }[] = [
{ value: 'manual', label: 'Без счета', icon: <Wallet className="w-4 h-4" /> },
{ value: 'cash', label: 'Наличные', icon: <Banknote className="w-4 h-4" /> },
{ value: 'invoice', label: 'По счету', icon: <Wallet className="w-4 h-4" /> }
];
const PROBABILITY_OPTIONS: { value: PaymentProbability; label: string }[] = [
{ value: 'confirmed', label: '100% (подтверждено)' },
{ value: 'high', label: '80%' },
{ value: 'medium', label: '50%' },
{ value: 'low', label: '30%' }
];
const CURRENCIES = [{ value: 'RUB', label: '₽ RUB' }, { value: 'USD', label: '$ USD' }, { value: 'EUR', label: '€ EUR' }];
export const PaymentCalendarEntryForm: React.FC<PaymentCalendarEntryFormProps> = ({
entry,
currentUserId,
defaultDirection = 'outgoing',
onSave,
onCancel
}) => {
const [direction, setDirection] = useState<PaymentDirection>(entry?.direction ?? defaultDirection);
const [type, setType] = useState<PaymentType>(entry?.type ?? 'manual');
const [category, setCategory] = useState(entry?.category ?? '');
const [contractorName, setContractorName] = useState(entry?.contractorName ?? '');
const [description, setDescription] = useState(entry?.description ?? '');
const [amount, setAmount] = useState(entry?.amount ?? 0);
const [scheduledDate, setScheduledDate] = useState(
entry?.scheduledDate ?? new Date().toISOString().split('T')[0]
);
const [probability, setProbability] = useState<PaymentProbability>(entry?.probability ?? 'confirmed');
const [currency, setCurrency] = useState(entry?.currency ?? 'RUB');
const [isCash, setIsCash] = useState(entry?.isCash ?? false);
const [notes, setNotes] = useState(entry?.notes ?? '');
const [categories, setCategories] = useState<PaymentCategory[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const load = async () => {
try {
const list = await apiClient.get<PaymentCategory[]>(
`/finance/payment-calendar/categories?direction=${direction}`
);
setCategories(Array.isArray(list) ? list : []);
} catch {
setCategories([]);
}
};
load();
}, [direction]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (amount <= 0) {
setError('Укажите сумму больше нуля');
return;
}
setLoading(true);
try {
if (entry?.id) {
await apiClient.put(`/finance/payment-calendar/entries/${entry.id}`, {
direction,
type,
category,
description,
amount,
scheduledDate,
probability,
currency,
isCash: type === 'cash' || isCash,
contractorName,
notes
});
} else {
await apiClient.post('/finance/payment-calendar/entries', {
createdBy: currentUserId,
direction,
type,
category,
description,
amount,
scheduledDate,
probability,
currency,
isCash: type === 'cash' || isCash,
contractorName,
notes
});
}
await onSave({} as Partial<PaymentCalendarEntry>);
} catch (err: any) {
setError(err?.message || 'Ошибка сохранения');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div className="bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
<div className="p-4 border-b border-slate-200 flex justify-between items-center sticky top-0 bg-white">
<h3 className="font-bold text-slate-800">
{entry?.id ? 'Редактировать запись' : 'Добавить запись в календарь'}
</h3>
<button type="button" onClick={onCancel} className="p-2 rounded-lg hover:bg-slate-100 text-slate-500">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{error && (
<div className="p-3 rounded-lg bg-red-50 text-red-700 text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Направление</label>
<div className="flex gap-2">
{DIRECTION_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setDirection(opt.value)}
className={`flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium border transition-colors ${
direction === opt.value
? opt.value === 'incoming'
? 'bg-emerald-50 border-emerald-200 text-emerald-700'
: 'bg-slate-900 text-white border-slate-900'
: 'bg-slate-50 border-slate-200 text-slate-600 hover:bg-slate-100'
}`}
>
{opt.icon}
{opt.label}
</button>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Тип</label>
<div className="flex flex-wrap gap-2">
{TYPE_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => {
setType(opt.value);
if (opt.value === 'cash') setIsCash(true);
else if (type === 'cash') setIsCash(false);
}}
className={`flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium border transition-colors ${
type === opt.value ? 'bg-primary-50 border-primary-200 text-primary-700' : 'bg-slate-50 border-slate-200 text-slate-600 hover:bg-slate-100'
}`}
>
{opt.icon}
{opt.label}
</button>
))}
</div>
</div>
{categories.length > 0 && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Статья</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
>
<option value=""> не выбрано </option>
{categories.map((c) => (
<option key={c.id} value={c.name}>
{c.name}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Контрагент *</label>
<input
type="text"
value={contractorName}
onChange={(e) => setContractorName(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
placeholder="Название контрагента"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Описание</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
placeholder="Краткое описание"
/>
</div>
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-slate-700 mb-1">Сумма *</label>
<input
type="number"
min="0"
step="0.01"
value={amount || ''}
onChange={(e) => setAmount(parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
/>
</div>
<div className="w-28">
<label className="block text-sm font-medium text-slate-700 mb-1">Валюта</label>
<select
value={currency}
onChange={(e) => setCurrency(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
>
{CURRENCIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Дата в графике *</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="date"
value={scheduledDate}
onChange={(e) => setScheduledDate(e.target.value)}
className="w-full pl-10 pr-3 py-2 border border-slate-200 rounded-xl text-sm"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Вероятность</label>
<select
value={probability}
onChange={(e) => setProbability(e.target.value as PaymentProbability)}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm"
>
{PROBABILITY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Примечания</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
className="w-full px-3 py-2 border border-slate-200 rounded-xl text-sm resize-none"
placeholder="Необязательно"
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="submit"
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-xl font-medium text-sm hover:bg-slate-800 disabled:opacity-50"
>
<Save className="w-4 h-4" />
{entry?.id ? 'Сохранить' : 'Добавить'}
</button>
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-slate-200 rounded-xl font-medium text-sm text-slate-600 hover:bg-slate-50"
>
Отмена
</button>
</div>
</form>
</div>
</div>
);
};