// Partial early repayment screen — Komek Lombard v2
// Mirrors the "Условия пролонгации" screen pattern, but with an editable amount.
const PR_TEAL = '#1B7F70';
const PR_TEAL_DEEP = '#176A5D';
const PR_BORDER = '#E2E2E5';
const PR_LABEL = '#0B0B0C';
const PR_BODY = '#3A3A3D';
const PR_HINT = '#9A9AA0';
// ─── icons ─────────────────────────────────────────────────────────
const PRIcon = {
arrowLeft: (
),
chevron: (
),
wallet: (
),
tenge: (
),
check: (
),
};
// ─── top bar ───────────────────────────────────────────────────────
function TopBar({ title, onBack }) {
return (
{PRIcon.arrowLeft}
{title}
);
}
// ─── field ─────────────────────────────────────────────────────────
function FieldLabel({ children }) {
return (
{children}
);
}
function MethodSelector({ value, onClick }) {
return (
{PRIcon.wallet}
{value || 'Выбрать метод оплаты'}
{PRIcon.chevron}
);
}
function AmountInput({ value, onChange, max }) {
const display = value === '' ? '' : Number(value).toLocaleString('ru-RU');
return (
{PRIcon.tenge}
{
const digits = e.target.value.replace(/\D/g, '');
if (digits === '') { onChange(''); return; }
const n = parseInt(digits, 10);
onChange(Math.min(n, max));
}}
placeholder="0"
style={{
flex: 1, border: 'none', outline: 'none', background: 'transparent',
fontSize: 17, fontWeight: 700, color: PR_LABEL,
fontFamily: 'inherit', minWidth: 0,
}}
/>
₸
);
}
function HelperRow({ left, right }) {
return (
{left}
{right}
);
}
function Checkbox({ checked, onChange, children }) {
return (
{checked && PRIcon.check}
onChange(e.target.checked)}
style={{ position: 'absolute', opacity: 0, pointerEvents: 'none' }}
/>
{children}
);
}
function PayButton({ enabled, children, onClick }) {
return (
{children}
);
}
// ─── screen ────────────────────────────────────────────────────────
// Default pledged items for a multi-item ticket. minRemainder is the BP-2 min.
// balance for each item; a single repayment must keep at least that much in debt.
const DEFAULT_ITEMS = [
{ id: 'i1', name: 'Часы Rolex Datejust', balance: 18000, minRemainder: 5000 },
{ id: 'i2', name: 'Кольцо золотое 585°', balance: 14000, minRemainder: 5000 },
{ id: 'i3', name: 'Серьги серебряные', balance: 8000, minRemainder: 5000 },
];
function itemMaxRepayable(it) {
return Math.max(0, it.balance - it.minRemainder);
}
function distributeAllocations({ items, modes, manualValues, total }) {
// total = budget that goes specifically to main debt (already excludes interest)
const result = {};
let manualSum = 0;
items.forEach(it => {
if (modes[it.id] === 'manual') {
const v = Math.max(0, Math.min(manualValues[it.id] || 0, itemMaxRepayable(it)));
result[it.id] = v;
manualSum += v;
}
});
const autoItems = items.filter(it => modes[it.id] === 'auto');
const autoBudget = Math.max(0, total - manualSum);
const autoBase = autoItems.reduce((s, it) => s + itemMaxRepayable(it), 0);
let assigned = 0;
autoItems.forEach((it, idx) => {
const cap = itemMaxRepayable(it);
if (autoBase === 0 || autoBudget === 0) {
result[it.id] = 0;
} else if (idx === autoItems.length - 1) {
result[it.id] = Math.max(0, Math.min(cap, autoBudget - assigned));
} else {
const v = Math.min(cap, Math.round((cap / autoBase) * autoBudget));
result[it.id] = v;
assigned += v;
}
});
return { result, manualSum };
}
function ToggleSwitch({ checked, onChange }) {
return (
onChange(!checked)} aria-pressed={checked} style={{
width: 42, height: 26, borderRadius: 999, border: 'none', padding: 0, cursor: 'pointer',
background: checked ? PR_TEAL : '#D5D6DA',
transition: 'background 150ms ease', position: 'relative',
flexShrink: 0,
}}>
);
}
function ItemAllocationRow({ item, mode, value, onMode, onChange, isFirst, isLast }) {
const cap = itemMaxRepayable(item);
const editing = mode === 'manual';
const atCap = editing && (value || 0) >= cap && cap > 0;
const valueColor = editing ? PR_LABEL : '#5C5C60';
return (
{/* header: title + toggle */}
{item.name}
Изменить
onMode(c ? 'manual' : 'auto')}/>
{/* Amount field — same shape as main AmountInput; disabled when auto */}
{PRIcon.tenge}
{
if (!editing) return;
const digits = e.target.value.replace(/\D/g, '');
onChange(digits === '' ? 0 : parseInt(digits, 10));
}}
style={{
flex: 1, minWidth: 0, border: 'none', outline: 'none', background: 'transparent',
fontFamily: 'inherit', fontSize: 17, fontWeight: 700,
color: valueColor,
cursor: editing ? 'text' : 'default',
}}
/>
₸
{/* Info below the field */}
{atCap
? `Достигнут максимум: ${cap.toLocaleString('ru-RU')} ₸`
: `Долг ${item.balance.toLocaleString('ru-RU')} ₸ · мин. остаток ${item.minRemainder.toLocaleString('ru-RU')} ₸`
}
);
}
// ─── screen ─────────────────────────────────────────────────────
function PartialRepaymentScreen({ onBack, loanBalance = 40000, accruedInterest = 1234, minAmount = 5000, maxAmount = 35000, items = DEFAULT_ITEMS, initialAmount = 0, initialMethod = '', initialAgreed = false, initialAllocations }) {
const [method, setMethod] = React.useState(initialMethod);
const [amount, setAmount] = React.useState(initialAmount);
const [agreed, setAgreed] = React.useState(initialAgreed);
// per-item mode + manual values
const [modes, setModes] = React.useState(() => Object.fromEntries(
items.map(it => [it.id, (initialAllocations && initialAllocations[it.id] && initialAllocations[it.id].mode) || 'auto'])
));
const [manualValues, setManualValues] = React.useState(() => Object.fromEntries(
items.map(it => [it.id, (initialAllocations && initialAllocations[it.id] && initialAllocations[it.id].value) || 0])
));
const debtBudget = Math.max(0, Number(amount || 0) - accruedInterest);
const { result: alloc, manualSum } = distributeAllocations({
items, modes, manualValues, total: debtBudget,
});
const manualOver = manualSum > debtBudget;
const max = maxAmount;
const valid = method !== '' && Number(amount) >= minAmount && Number(amount) <= max && agreed && !manualOver;
const setItemMode = (id, m) => {
setModes(prev => ({ ...prev, [id]: m }));
if (m === 'manual') {
// when switching to manual, seed the field with the current auto allocation
setManualValues(prev => ({ ...prev, [id]: alloc[id] || 0 }));
}
};
const setItemManual = (id, v) => {
const item = items.find(it => it.id === id);
const cap = itemMaxRepayable(item);
setManualValues(prev => ({ ...prev, [id]: Math.min(cap, Math.max(0, v)) }));
};
return (
{/* scrollable middle */}
{/* description */}
Укажите сумму частичного погашения — она спишется в счёт начисленных процентов, а остаток уменьшит основной долг.
Проценты пересчитаются автоматически.
{/* method */}
Метод оплаты
setMethod('Mastercard •••• 4783')}/>
{/* amount */}
Частично погашаю сейчас
Максимальная сумма к погашению {max.toLocaleString('ru-RU')} ₸
{/* secondary info — less important than the payment summary */}
Сумма кредита до частичного погашения
{loanBalance.toLocaleString('ru-RU')} ₸
Сумма кредита после частичного погашения
{Math.max(0, loanBalance - Math.max(0, Number(amount || 0) - accruedInterest)).toLocaleString('ru-RU')} ₸
{/* summary preview */}
Сумма процентов
{Math.min(Number(amount || 0), accruedInterest).toLocaleString('ru-RU')} ₸
Сумма погашения основного долга
{Math.max(0, Number(amount || 0) - accruedInterest).toLocaleString('ru-RU')} ₸
Будет выполнен платёж
{Number(amount || 0).toLocaleString('ru-RU')} ₸
{/* Items — per-item allocation (placed right before the footer) */}
Распределение по предметам
{items.length} предмета
{items.map((it, idx) => (
setItemMode(it.id, m)}
onChange={(v) => setItemManual(it.id, v)}
/>
))}
{manualOver && (
Сумма ручных распределений ({manualSum.toLocaleString('ru-RU')} ₸) превышает сумму к распределению по основному долгу ({debtBudget.toLocaleString('ru-RU')} ₸). Уменьшите значения или увеличьте общую сумму платежа.
)}
{/* fixed footer */}
Согласен(-на) с условиями частичного погашения
Оплатить
);
}
Object.assign(window, { PartialRepaymentScreen });