124 lines
5.6 KiB
React
124 lines
5.6 KiB
React
import { useState } from 'react'
|
|
import * as api from '../lib/api'
|
|
import { useAuth } from '../auth/AuthContext'
|
|
import { useClientLoyalty } from './useClientLoyalty'
|
|
import { inputStyle } from '../orders/formStyles'
|
|
|
|
const TYPE_LABELS = { accrual: 'Начисление', redemption: 'Списание', adjustment: 'Корректировка' }
|
|
const TYPE_COLORS = { accrual: 'var(--accent-green)', redemption: 'var(--accent-red)', adjustment: 'var(--text-muted)' }
|
|
|
|
function RedeemForm({ clientId, balance, onDone }) {
|
|
const [points, setPoints] = useState('')
|
|
const [note, setNote] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const submit = async () => {
|
|
const n = Number(points)
|
|
if (!n || n <= 0) { setError('Укажите количество баллов'); return }
|
|
setBusy(true)
|
|
setError('')
|
|
try {
|
|
await api.loyalty.redeem(clientId, { points: n, note })
|
|
setPoints('')
|
|
setNote('')
|
|
onDone()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '6px' }}>
|
|
<input type="number" min="1" max={balance} value={points} onChange={(e) => setPoints(e.target.value)} placeholder="Баллов" style={{ ...inputStyle, width: '90px', padding: '6px 8px' }} />
|
|
<input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Заметка (необязательно)" style={{ ...inputStyle, flex: 1, minWidth: '140px', padding: '6px 8px' }} />
|
|
<button type="button" onClick={submit} disabled={busy} className="btn-outline" style={{ padding: '6px 12px', fontSize: '0.78rem' }}>Списать</button>
|
|
{error && <span style={{ fontSize: '0.76rem', color: 'var(--accent-red)', width: '100%' }}>{error}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AdjustForm({ clientId, onDone }) {
|
|
const [points, setPoints] = useState('')
|
|
const [note, setNote] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const submit = async () => {
|
|
const n = Number(points)
|
|
if (!n) { setError('Укажите количество баллов (можно отрицательное)'); return }
|
|
if (!note.trim()) { setError('Заметка обязательна для корректировки'); return }
|
|
setBusy(true)
|
|
setError('')
|
|
try {
|
|
await api.loyalty.adjust(clientId, { points: n, note })
|
|
setPoints('')
|
|
setNote('')
|
|
onDone()
|
|
} catch (err) {
|
|
setError(err.message)
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', alignItems: 'center', marginBottom: '10px' }}>
|
|
<input type="number" value={points} onChange={(e) => setPoints(e.target.value)} placeholder="±Баллов" style={{ ...inputStyle, width: '90px', padding: '6px 8px' }} />
|
|
<input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Причина (обязательно)" style={{ ...inputStyle, flex: 1, minWidth: '140px', padding: '6px 8px' }} />
|
|
<button type="button" onClick={submit} disabled={busy} className="btn-outline" style={{ padding: '6px 12px', fontSize: '0.78rem' }}>Скорректировать</button>
|
|
{error && <span style={{ fontSize: '0.76rem', color: 'var(--accent-red)', width: '100%' }}>{error}</span>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Embedded in ClientsPage's ClientDetail modal, right after
|
|
// ClientNotificationsSection — balance, manual redeem/adjust (owner/manager
|
|
// only, matching the backend's cashOnly gate), and the ledger history.
|
|
const ClientLoyaltySection = ({ clientId }) => {
|
|
const { staff } = useAuth()
|
|
const canManage = staff.role === 'owner' || staff.role === 'manager'
|
|
const { balance, history, loading, reload } = useClientLoyalty(clientId)
|
|
|
|
return (
|
|
<div style={{ marginTop: '18px', paddingTop: '18px', borderTop: '1px solid var(--border-light)' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '10px' }}>
|
|
<h3 style={{ fontSize: '0.9rem' }}>Бонусные баллы</h3>
|
|
<span style={{ fontSize: '1.1rem', fontWeight: 590, color: 'var(--accent-orange)' }}>{balance}</span>
|
|
</div>
|
|
|
|
{canManage && (
|
|
<>
|
|
<RedeemForm clientId={clientId} balance={balance} onDone={reload} />
|
|
<AdjustForm clientId={clientId} onDone={reload} />
|
|
</>
|
|
)}
|
|
|
|
<h4 style={{ fontSize: '0.82rem', color: 'var(--text-muted)', marginBottom: '6px' }}>История</h4>
|
|
{loading ? (
|
|
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Загрузка...</p>
|
|
) : history.length === 0 ? (
|
|
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Пока нет операций</p>
|
|
) : (
|
|
history.map((t) => (
|
|
<div key={t.id} style={{ padding: '6px 0', borderBottom: '1px solid var(--border-light)', fontSize: '0.78rem' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
<span>{TYPE_LABELS[t.type] || t.type}{t.note ? ` — ${t.note}` : ''}</span>
|
|
<span style={{ fontWeight: 590, color: TYPE_COLORS[t.type], flexShrink: 0 }}>
|
|
{t.points > 0 ? '+' : ''}{t.points}
|
|
</span>
|
|
</div>
|
|
<p style={{ color: 'var(--text-muted)', marginTop: '2px' }}>
|
|
{new Date(t.created_at).toLocaleString('ru-RU')} · {t.staff_name}
|
|
</p>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ClientLoyaltySection
|