feat: deposit presets UI, back button in history, onFocus select on inputs
- DepositSettingsPage: deduction presets management (add/edit/delete inline) - DepositHistoryPage: "← Назад" link back to settings - Amount inputs: onFocus selects all text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Loader2, ShieldCheck, CreditCard, Banknote, RefreshCw } from 'lucide-react'
|
import { Loader2, ShieldCheck, CreditCard, Banknote, RefreshCw, ArrowLeft } from 'lucide-react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { api, type BookingDeposit } from '../lib/api'
|
import { api, type BookingDeposit } from '../lib/api'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { cn, formatCurrency } from '../lib/utils'
|
import { cn, formatCurrency } from '../lib/utils'
|
||||||
@@ -40,14 +41,17 @@ export function DepositHistoryPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-6 space-y-5 max-w-4xl">
|
<div className="p-4 md:p-6 space-y-5 max-w-4xl">
|
||||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Link to="/settings/deposit" className="flex items-center gap-1.5 text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
Назад
|
||||||
|
</Link>
|
||||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
||||||
<ShieldCheck size={22} className="text-brand-600" />
|
<ShieldCheck size={22} className="text-brand-600" />
|
||||||
История депозитов
|
История депозитов
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
</div>
|
||||||
onClick={load}
|
<button onClick={load} className="btn-secondary flex items-center gap-1.5 text-sm">
|
||||||
className="btn-secondary flex items-center gap-1.5 text-sm"
|
|
||||||
>
|
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
Обновить
|
Обновить
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Save, Eye, EyeOff, Loader2, ShieldCheck, Printer, History, Info } from 'lucide-react'
|
import { Save, Eye, EyeOff, Loader2, ShieldCheck, Printer, History, Info, Plus, Trash2, Check } from 'lucide-react'
|
||||||
import { QRCodeSVG } from 'qrcode.react'
|
import { QRCodeSVG } from 'qrcode.react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { api, type DepositSettings } from '../lib/api'
|
import { api, type DepositSettings, type DepositPreset } from '../lib/api'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { cn } from '../lib/utils'
|
import { cn, formatCurrency } from '../lib/utils'
|
||||||
|
|
||||||
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
|
||||||
return (
|
return (
|
||||||
@@ -33,24 +33,33 @@ export function DepositSettingsPage() {
|
|||||||
const [secretKey, setSecretKey] = useState('')
|
const [secretKey, setSecretKey] = useState('')
|
||||||
const [showSecret, setShowSecret] = useState(false)
|
const [showSecret, setShowSecret] = useState(false)
|
||||||
|
|
||||||
|
// Deduction presets
|
||||||
|
const [presets, setPresets] = useState<DepositPreset[]>([])
|
||||||
|
const [newPresetName, setNewPresetName] = useState('')
|
||||||
|
const [newPresetAmount, setNewPresetAmount] = useState('')
|
||||||
|
const [addingPreset, setAddingPreset] = useState(false)
|
||||||
|
const [editingPreset, setEditingPreset] = useState<string | null>(null)
|
||||||
|
const [editName, setEditName] = useState('')
|
||||||
|
const [editAmount, setEditAmount] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!slug) return
|
if (!slug) return
|
||||||
api.deposits.getSettings(slug)
|
Promise.all([
|
||||||
.then(s => {
|
api.deposits.getSettings(slug),
|
||||||
|
api.deposits.getPresets(slug).catch(() => []),
|
||||||
|
]).then(([s, p]) => {
|
||||||
setSettings(s)
|
setSettings(s)
|
||||||
setIsEnabled(s.isEnabled)
|
setIsEnabled(s.isEnabled)
|
||||||
setAmount(String(s.amount))
|
setAmount(String(s.amount))
|
||||||
setShopId(s.yookassaShopId ?? '')
|
setShopId(s.yookassaShopId ?? '')
|
||||||
setSecretKey(s.yookassaSecretKey ?? '')
|
setSecretKey(s.yookassaSecretKey ?? '')
|
||||||
})
|
setPresets(p)
|
||||||
.catch(() => {
|
}).catch(() => {
|
||||||
// No settings yet — defaults
|
|
||||||
setIsEnabled(false)
|
setIsEnabled(false)
|
||||||
setAmount('5000')
|
setAmount('5000')
|
||||||
setShopId('')
|
setShopId('')
|
||||||
setSecretKey('')
|
setSecretKey('')
|
||||||
})
|
}).finally(() => setLoading(false))
|
||||||
.finally(() => setLoading(false))
|
|
||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
@@ -75,6 +84,47 @@ export function DepositSettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddPreset = async () => {
|
||||||
|
const name = newPresetName.trim()
|
||||||
|
const amt = parseFloat(newPresetAmount)
|
||||||
|
if (!name || !amt || amt <= 0) return
|
||||||
|
setAddingPreset(true)
|
||||||
|
try {
|
||||||
|
const p = await api.deposits.createPreset(slug, name, amt)
|
||||||
|
setPresets(prev => [...prev, p])
|
||||||
|
setNewPresetName('')
|
||||||
|
setNewPresetAmount('')
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setAddingPreset(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePreset = async (id: string) => {
|
||||||
|
await api.deposits.deletePreset(slug, id).catch(() => {})
|
||||||
|
setPresets(prev => prev.filter(p => p.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEdit = (p: DepositPreset) => {
|
||||||
|
setEditingPreset(p.id)
|
||||||
|
setEditName(p.name)
|
||||||
|
setEditAmount(String(p.amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEdit = async (id: string) => {
|
||||||
|
const name = editName.trim()
|
||||||
|
const amt = parseFloat(editAmount)
|
||||||
|
if (!name || !amt) return
|
||||||
|
try {
|
||||||
|
const updated = await api.deposits.updatePreset(slug, id, { name, amount: amt })
|
||||||
|
setPresets(prev => prev.map(p => p.id === id ? updated : p))
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
setEditingPreset(null)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@@ -95,10 +145,7 @@ export function DepositSettingsPage() {
|
|||||||
Настройте предоплату или залог при заселении гостей.
|
Настройте предоплату или залог при заселении гостей.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link to="/settings/deposit/history" className="btn-secondary flex items-center gap-1.5 text-sm shrink-0">
|
||||||
to="/settings/deposit/history"
|
|
||||||
className="btn-secondary flex items-center gap-1.5 text-sm shrink-0"
|
|
||||||
>
|
|
||||||
<History size={15} />
|
<History size={15} />
|
||||||
История платежей
|
История платежей
|
||||||
</Link>
|
</Link>
|
||||||
@@ -110,8 +157,8 @@ export function DepositSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Main settings card */}
|
||||||
<div className="card p-5 space-y-5">
|
<div className="card p-5 space-y-5">
|
||||||
{/* Enable toggle */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-slate-900 dark:text-slate-100">Включить депозит</p>
|
<p className="font-semibold text-slate-900 dark:text-slate-100">Включить депозит</p>
|
||||||
@@ -125,66 +172,47 @@ export function DepositSettingsPage() {
|
|||||||
{isEnabled && (
|
{isEnabled && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-4 space-y-4">
|
<div className="border-t border-slate-100 dark:border-slate-700 pt-4 space-y-4">
|
||||||
{/* Amount */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Сумма депозита, руб.</label>
|
<label className="form-label">Сумма депозита, руб.</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number" min="0" step="100"
|
||||||
min="0"
|
value={amount} onChange={e => setAmount(e.target.value)}
|
||||||
step="100"
|
onFocus={e => e.target.select()}
|
||||||
value={amount}
|
|
||||||
onChange={e => setAmount(e.target.value)}
|
|
||||||
className="input w-48"
|
className="input w-48"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* YooKassa section */}
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm">ЮКасса (опционально)</p>
|
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm">ЮКасса (опционально)</p>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
Укажите реквизиты ЮКасса для создания холда (предавторизации) оплаты. Без этих данных доступен только наличный депозит.
|
Укажите реквизиты ЮКасса для создания холда (предавторизации) оплаты.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">ID магазина (shopId)</label>
|
<label className="form-label">ID магазина (shopId)</label>
|
||||||
<input
|
<input value={shopId} onChange={e => setShopId(e.target.value)} placeholder="123456" className="input w-full" />
|
||||||
value={shopId}
|
|
||||||
onChange={e => setShopId(e.target.value)}
|
|
||||||
placeholder="123456"
|
|
||||||
className="input w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="form-label">Секретный ключ</label>
|
<label className="form-label">Секретный ключ</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type={showSecret ? 'text' : 'password'}
|
type={showSecret ? 'text' : 'password'}
|
||||||
value={secretKey}
|
value={secretKey} onChange={e => setSecretKey(e.target.value)}
|
||||||
onChange={e => setSecretKey(e.target.value)}
|
|
||||||
placeholder={settings?.yookassaSecretKey ? '••••••••' : 'live_xxxx...'}
|
placeholder={settings?.yookassaSecretKey ? '••••••••' : 'live_xxxx...'}
|
||||||
className="input w-full pr-10"
|
className="input w-full pr-10"
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="button" onClick={() => setShowSecret(s => !s)} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
|
||||||
type="button"
|
|
||||||
onClick={() => setShowSecret(s => !s)}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
|
||||||
>
|
|
||||||
{showSecret ? <EyeOff size={15} /> : <Eye size={15} />}
|
{showSecret ? <EyeOff size={15} /> : <Eye size={15} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{settings?.yookassaSecretKey && (
|
{settings?.yookassaSecretKey && (
|
||||||
<p className="text-xs text-slate-400 mt-1">
|
<p className="text-xs text-slate-400 mt-1">Ключ уже сохранён. Оставьте поле пустым, чтобы не менять.</p>
|
||||||
Ключ уже сохранён. Оставьте поле пустым, чтобы не менять.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Webhook notice */}
|
|
||||||
{shopId && (
|
{shopId && (
|
||||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
|
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
|
||||||
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3 space-y-1.5">
|
<div className="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 p-3 space-y-1.5">
|
||||||
@@ -192,7 +220,7 @@ export function DepositSettingsPage() {
|
|||||||
<Info size={13} /> Настройте webhook в ЮКасса
|
<Info size={13} /> Настройте webhook в ЮКасса
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-blue-700 dark:text-blue-400">
|
<p className="text-xs text-blue-700 dark:text-blue-400">
|
||||||
Чтобы статус депозита обновлялся автоматически после оплаты, добавьте URL уведомлений в личном кабинете ЮКасса (Настройки → HTTP-уведомления):
|
Чтобы статус депозита обновлялся автоматически, добавьте URL в личном кабинете ЮКасса (Настройки → HTTP-уведомления):
|
||||||
</p>
|
</p>
|
||||||
<code className="block text-xs font-mono bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-200 rounded px-2 py-1 break-all select-all">
|
<code className="block text-xs font-mono bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-200 rounded px-2 py-1 break-all select-all">
|
||||||
https://api.hotelsync.ru/api/webhooks/yookassa
|
https://api.hotelsync.ru/api/webhooks/yookassa
|
||||||
@@ -204,29 +232,20 @@ export function DepositSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* QR code section — only when shopId is configured */}
|
|
||||||
{shopId && (
|
{shopId && (
|
||||||
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
|
<div className="border-t border-slate-100 dark:border-slate-700 pt-4">
|
||||||
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm mb-1">QR-код для гостей</p>
|
<p className="font-medium text-slate-800 dark:text-slate-200 text-sm mb-1">QR-код для гостей</p>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-4">
|
<p className="text-xs text-slate-500 dark:text-slate-400 mb-4">
|
||||||
Распечатайте и разместите на стойке ресепшена. Гость сканирует код и оплачивает депозит картой.
|
Распечатайте и разместите на стойке ресепшена.
|
||||||
</p>
|
</p>
|
||||||
<div className="print-qr-section flex flex-col items-center gap-4">
|
<div className="print-qr-section flex flex-col items-center gap-4">
|
||||||
<div className="p-4 bg-white rounded-xl border border-slate-200 dark:border-slate-600 inline-block">
|
<div className="p-4 bg-white rounded-xl border border-slate-200 dark:border-slate-600 inline-block">
|
||||||
<QRCodeSVG
|
<QRCodeSVG value={`https://app.hotelsync.ru/${slug}/pay`} size={160} level="M" />
|
||||||
value={`https://app.hotelsync.ru/${slug}/pay`}
|
|
||||||
size={160}
|
|
||||||
level="M"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-400 font-mono break-all text-center">
|
<p className="text-xs text-slate-400 font-mono break-all text-center">
|
||||||
https://app.hotelsync.ru/{slug}/pay
|
https://app.hotelsync.ru/{slug}/pay
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button type="button" onClick={() => window.print()} className="btn-secondary flex items-center gap-2">
|
||||||
type="button"
|
|
||||||
onClick={() => window.print()}
|
|
||||||
className="btn-secondary flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Printer size={15} />
|
<Printer size={15} />
|
||||||
Распечатать QR-код
|
Распечатать QR-код
|
||||||
</button>
|
</button>
|
||||||
@@ -234,16 +253,8 @@ export function DepositSettingsPage() {
|
|||||||
<style>{`
|
<style>{`
|
||||||
@media print {
|
@media print {
|
||||||
body > * { display: none !important; }
|
body > * { display: none !important; }
|
||||||
.print-qr-section,
|
.print-qr-section, .print-qr-section * { display: flex !important; }
|
||||||
.print-qr-section * { display: flex !important; }
|
.print-qr-section { position: fixed; inset: 0; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; }
|
||||||
.print-qr-section {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
button { display: none !important; }
|
button { display: none !important; }
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
@@ -254,18 +265,103 @@ export function DepositSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button onClick={handleSave} disabled={saving} className="btn-primary flex items-center gap-2">
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
className="btn-primary flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{saving ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
{saving ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
||||||
{saved ? 'Сохранено!' : 'Сохранить'}
|
{saved ? 'Сохранено!' : 'Сохранить'}
|
||||||
</button>
|
</button>
|
||||||
{saved && (
|
{saved && <span className="text-sm text-emerald-600 dark:text-emerald-400">Настройки сохранены</span>}
|
||||||
<span className="text-sm text-emerald-600 dark:text-emerald-400">Настройки сохранены</span>
|
</div>
|
||||||
|
|
||||||
|
{/* Deduction presets */}
|
||||||
|
{isEnabled && (
|
||||||
|
<div className="card p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-900 dark:text-slate-100">Позиции удержания</p>
|
||||||
|
<p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
|
Стандартные причины для списания из депозита. При возврате депозита менеджер выбирает из этого списка.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Existing presets */}
|
||||||
|
{presets.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{presets.map(p => (
|
||||||
|
<div key={p.id} className="flex items-center gap-2">
|
||||||
|
{editingPreset === p.id ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
value={editName} onChange={e => setEditName(e.target.value)}
|
||||||
|
className="input text-sm flex-1"
|
||||||
|
placeholder="Название"
|
||||||
|
onFocus={e => e.target.select()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={editAmount} onChange={e => setEditAmount(e.target.value)}
|
||||||
|
onFocus={e => e.target.select()}
|
||||||
|
type="number" min="0" step="100"
|
||||||
|
className="input text-sm w-28 text-right"
|
||||||
|
placeholder="Сумма"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">₽</span>
|
||||||
|
<button onClick={() => saveEdit(p.id)} className="p-1 rounded text-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 transition-colors">
|
||||||
|
<Check size={15} />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setEditingPreset(null)} className="p-1 rounded text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
||||||
|
<Trash2 size={13} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="flex-1 text-sm text-slate-700 dark:text-slate-200 cursor-pointer hover:text-brand-600 transition-colors"
|
||||||
|
onClick={() => startEdit(p)}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-slate-500 dark:text-slate-400 cursor-pointer" onClick={() => startEdit(p)}>
|
||||||
|
{formatCurrency(p.amount)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeletePreset(p.id)}
|
||||||
|
className="p-1 rounded text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 size={13} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add new preset */}
|
||||||
|
<div className="flex items-center gap-2 pt-1 border-t border-slate-100 dark:border-slate-700">
|
||||||
|
<input
|
||||||
|
value={newPresetName} onChange={e => setNewPresetName(e.target.value)}
|
||||||
|
className="input text-sm flex-1"
|
||||||
|
placeholder="Название (напр. Штраф за курение)"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleAddPreset()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={newPresetAmount} onChange={e => setNewPresetAmount(e.target.value)}
|
||||||
|
onFocus={e => e.target.select()}
|
||||||
|
type="number" min="0" step="100"
|
||||||
|
className="input text-sm w-28 text-right"
|
||||||
|
placeholder="Сумма"
|
||||||
|
onKeyDown={e => e.key === 'Enter' && handleAddPreset()}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400 shrink-0">₽</span>
|
||||||
|
<button
|
||||||
|
onClick={handleAddPreset}
|
||||||
|
disabled={addingPreset || !newPresetName.trim() || !newPresetAmount}
|
||||||
|
className="btn-primary flex items-center gap-1 text-sm px-3 py-1.5 shrink-0 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{addingPreset ? <Loader2 size={13} className="animate-spin" /> : <Plus size={13} />}
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user