Translate UI and user-facing messages to Ukrainian (#5)
- All frontend pages, labels, notices and errors; html lang=uk, uk-UA money format - Brand "Assistant System" in the top bar and page title - Backend error details returned to the UI (auth, orders, receipts, cash registers, Checkbox/CRM/NP errors) and CLI output - Tests updated for the new messages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,16 +14,16 @@ import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types'
|
||||
import type { ReceiptRequestItem } from '@/features/receipts/types'
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = {
|
||||
admin: 'Администратор',
|
||||
cashier: 'Кассир',
|
||||
viewer: 'Наблюдатель',
|
||||
admin: 'Адміністратор',
|
||||
cashier: 'Касир',
|
||||
viewer: 'Спостерігач',
|
||||
}
|
||||
|
||||
const TABS: { key: OrderTab; label: string }[] = [
|
||||
{ key: 'no_receipt', label: 'Без чека' },
|
||||
{ key: 'has_receipt', label: 'Выписаны чеки' },
|
||||
{ key: 'received', label: 'Полученные' },
|
||||
{ key: 'refused', label: 'Отказы' },
|
||||
{ key: 'has_receipt', label: 'Виписані чеки' },
|
||||
{ key: 'received', label: 'Отримані' },
|
||||
{ key: 'refused', label: 'Відмови' },
|
||||
]
|
||||
|
||||
function npStatusTone(order: Order): 'delivered' | 'processing' | 'danger' | 'new' {
|
||||
@@ -45,7 +45,7 @@ function paymentBadge(order: Order): { label: string; tone: 'delivered' | 'proce
|
||||
return null
|
||||
}
|
||||
|
||||
const MONEY_FORMAT = new Intl.NumberFormat('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
const MONEY_FORMAT = new Intl.NumberFormat('uk-UA', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
function formatMoney(amount: string): string {
|
||||
return MONEY_FORMAT.format(Number(amount))
|
||||
@@ -80,7 +80,7 @@ export function DashboardPage() {
|
||||
for (const order of targets) {
|
||||
const prepayment = toKopecks(prepaymentOf(order))
|
||||
if (prepayment === null) {
|
||||
localErrors.push(`${order.id}: некорректная сумма предоплаты`)
|
||||
localErrors.push(`${order.id}: некоректна сума передоплати`)
|
||||
continue
|
||||
}
|
||||
items.push({ order_id: order.id, prepayment_kopecks: prepayment })
|
||||
@@ -95,14 +95,14 @@ export function DashboardPage() {
|
||||
const result = await createReceipts(items)
|
||||
const errors = [...localErrors, ...Object.entries(result.errors).map(([id, msg]) => `${id}: ${msg}`)]
|
||||
const lines = [
|
||||
...(result.created.length ? [`Отправлено в Checkbox: ${result.created.length}`] : []),
|
||||
...(result.created.length ? [`Надіслано в Checkbox: ${result.created.length}`] : []),
|
||||
...errors,
|
||||
]
|
||||
setNotice({ tone: errors.length ? 'error' : 'ok', lines })
|
||||
setSelected(new Set())
|
||||
await queryClient.invalidateQueries({ queryKey: ['orders'] })
|
||||
} catch (err) {
|
||||
setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не удалось создать чеки'] })
|
||||
setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не вдалося створити чеки'] })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -111,19 +111,19 @@ export function DashboardPage() {
|
||||
async function handleCancel(order: Order) {
|
||||
if (!order.receipt_id) return
|
||||
const outcome = {
|
||||
no_receipt: 'Заказ вернётся в очередь.',
|
||||
has_receipt: 'Заказ вернётся в очередь.',
|
||||
received: 'Заказ останется в полученных.',
|
||||
refused: 'Заказ останется в отказах.',
|
||||
no_receipt: 'Замовлення повернеться в чергу.',
|
||||
has_receipt: 'Замовлення повернеться в чергу.',
|
||||
received: 'Замовлення залишиться в отриманих.',
|
||||
refused: 'Замовлення залишиться у відмовах.',
|
||||
}[tab]
|
||||
if (!window.confirm(`Отменить ЕТТН-чек по заказу ${order.id}? ${outcome}`)) return
|
||||
if (!window.confirm(`Скасувати ЕТТН-чек за замовленням ${order.id}? ${outcome}`)) return
|
||||
setCancellingId(order.id)
|
||||
try {
|
||||
await cancelReceipt(order.receipt_id)
|
||||
setNotice({ tone: 'ok', lines: [`Чек по заказу ${order.id} отменён`] })
|
||||
setNotice({ tone: 'ok', lines: [`Чек за замовленням ${order.id} скасовано`] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['orders'] })
|
||||
} catch (err) {
|
||||
setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не удалось отменить чек'] })
|
||||
setNotice({ tone: 'error', lines: [err instanceof Error ? err.message : 'Не вдалося скасувати чек'] })
|
||||
} finally {
|
||||
setCancellingId(null)
|
||||
}
|
||||
@@ -174,23 +174,23 @@ export function DashboardPage() {
|
||||
return (
|
||||
<div className="dashboard-shell">
|
||||
<header className="dashboard-topbar">
|
||||
<span className="dashboard-brand">lux_fiscal</span>
|
||||
<span className="dashboard-brand">Assistant System</span>
|
||||
<div className="dashboard-user">
|
||||
<span>{user?.full_name}</span>
|
||||
<span className="dashboard-role">{user ? (ROLE_LABEL[user.role] ?? user.role) : ''}</span>
|
||||
<button type="button" className="dashboard-logout" onClick={() => void logout()}>
|
||||
Выйти
|
||||
Вийти
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="orders-body">
|
||||
<div className="orders-toolbar">
|
||||
<h2>Заказы</h2>
|
||||
<h2>Замовлення</h2>
|
||||
<div className="orders-toolbar-actions">
|
||||
{user?.role === 'admin' && (
|
||||
<Link to="/cash-registers" className="orders-link-btn">
|
||||
Кассы
|
||||
Каси
|
||||
</Link>
|
||||
)}
|
||||
{tab === 'no_receipt' && canFiscalize && (
|
||||
@@ -200,20 +200,20 @@ export function DashboardPage() {
|
||||
disabled={selected.size === 0 || submitting}
|
||||
onClick={() => void submitReceipts(filtered.filter((order) => selected.has(order.id)))}
|
||||
>
|
||||
Создать чеки по выбранным ({selected.size})
|
||||
Створити чеки за вибраними ({selected.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="orders-summary">
|
||||
<div className="orders-summary-card" title="Посылки с наложкой, которые ещё не забрали и по которым нет отказа">
|
||||
<span className="orders-summary-label">Наложка в пути</span>
|
||||
<div className="orders-summary-card" title="Посилки з післяплатою, які ще не забрали і за якими немає відмови">
|
||||
<span className="orders-summary-label">Післяплата в дорозі</span>
|
||||
<span className="orders-summary-value">
|
||||
{summary ? `${formatMoney(summary.cod_in_transit_amount)} ₴` : '—'}
|
||||
</span>
|
||||
<span className="orders-summary-hint">
|
||||
{summary ? `${summary.cod_in_transit_count} посылок` : ' '}
|
||||
{summary ? `${summary.cod_in_transit_count} посилок` : ' '}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,7 +225,7 @@ export function DashboardPage() {
|
||||
<li key={line}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button type="button" className="orders-notice-close" onClick={() => setNotice(null)} aria-label="Скрыть">
|
||||
<button type="button" className="orders-notice-close" onClick={() => setNotice(null)} aria-label="Сховати">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@@ -248,16 +248,16 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="orders-filterbar">
|
||||
<span className="orders-selected-count">Выбрано: {selected.size} заказов</span>
|
||||
<span className="orders-selected-count">Вибрано: {selected.size} замовлень</span>
|
||||
<input
|
||||
type="search"
|
||||
className="orders-search"
|
||||
placeholder="Поиск по ID, ТТН, клиенту"
|
||||
placeholder="Пошук за ID, ТТН, клієнтом"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<button type="button" className="orders-filter-btn">
|
||||
Фильтр
|
||||
Фільтр
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -266,31 +266,31 @@ export function DashboardPage() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="orders-checkbox-col">
|
||||
<input type="checkbox" checked={allSelected} onChange={toggleAll} aria-label="Выбрать всё" />
|
||||
<input type="checkbox" checked={allSelected} onChange={toggleAll} aria-label="Вибрати все" />
|
||||
</th>
|
||||
<th>ID заказа</th>
|
||||
<th>ID замовлення</th>
|
||||
<th>Номер ТТН</th>
|
||||
<th>Клиент</th>
|
||||
<th>Сумма</th>
|
||||
<th>Наложка</th>
|
||||
<th>Клієнт</th>
|
||||
<th>Сума</th>
|
||||
<th>Післяплата</th>
|
||||
<th>Оплачено</th>
|
||||
<th>Предоплата, ₴</th>
|
||||
<th>Передоплата, ₴</th>
|
||||
<th>Статус ТТН</th>
|
||||
<th>Действия</th>
|
||||
<th>Дії</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading && (
|
||||
<tr>
|
||||
<td colSpan={10} className="orders-empty">
|
||||
Загрузка…
|
||||
Завантаження…
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{isError && (
|
||||
<tr>
|
||||
<td colSpan={10} className="orders-empty">
|
||||
Не удалось загрузить заказы из CRM
|
||||
Не вдалося завантажити замовлення з CRM
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -305,7 +305,7 @@ export function DashboardPage() {
|
||||
type="checkbox"
|
||||
checked={selected.has(order.id)}
|
||||
onChange={() => toggleOne(order.id)}
|
||||
aria-label={`Выбрать заказ ${order.id}`}
|
||||
aria-label={`Вибрати замовлення ${order.id}`}
|
||||
/>
|
||||
</td>
|
||||
<td>{order.id}</td>
|
||||
@@ -334,7 +334,7 @@ export function DashboardPage() {
|
||||
placeholder="0.00"
|
||||
value={prepaymentOf(order)}
|
||||
disabled={!canFiscalize}
|
||||
title="Сумма заказа − предоплата должна равняться наложке"
|
||||
title="Сума замовлення − передоплата має дорівнювати післяплаті"
|
||||
onChange={(e) => setPrepayments((prev) => ({ ...prev, [order.id]: e.target.value }))}
|
||||
/>
|
||||
) : (
|
||||
@@ -343,19 +343,19 @@ export function DashboardPage() {
|
||||
</td>
|
||||
<td>
|
||||
<span className={`orders-status orders-status--${npStatusTone(order)}`}>
|
||||
{order.np_status || 'Нет данных'}
|
||||
{order.np_status || 'Немає даних'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="orders-actions-cell">
|
||||
<button type="button" className="orders-view-btn" onClick={() => setViewingOrder(order)}>
|
||||
Просмотр
|
||||
Перегляд
|
||||
</button>
|
||||
{tab === 'no_receipt' && canFiscalize && (
|
||||
<button
|
||||
type="button"
|
||||
className="orders-receipt-btn"
|
||||
disabled={!canCreateReceipt(order) || submitting}
|
||||
title={canCreateReceipt(order) ? 'Создать ЕТТН-чек в Checkbox' : 'Нет ТТН или наложки'}
|
||||
title={canCreateReceipt(order) ? 'Створити ЕТТН-чек у Checkbox' : 'Немає ТТН або післяплати'}
|
||||
onClick={() => void submitReceipts([order])}
|
||||
>
|
||||
Чек
|
||||
@@ -376,7 +376,7 @@ export function DashboardPage() {
|
||||
disabled={cancellingId === order.id}
|
||||
onClick={() => void handleCancel(order)}
|
||||
>
|
||||
Отменить
|
||||
Скасувати
|
||||
</button>
|
||||
)}
|
||||
{canDelete && (tab === 'no_receipt' || ((tab === 'refused' || tab === 'received') && !order.has_receipt)) && (
|
||||
@@ -386,7 +386,7 @@ export function DashboardPage() {
|
||||
disabled={deletingId === order.id}
|
||||
onClick={() => void handleDelete(order)}
|
||||
>
|
||||
Удалить
|
||||
Видалити
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
@@ -396,7 +396,7 @@ export function DashboardPage() {
|
||||
{!isLoading && !isError && filtered.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10} className="orders-empty">
|
||||
Ничего не найдено
|
||||
Нічого не знайдено
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user