Backoffice stage 3: orders and providers, auto-assignment

- Auto-assignment: a new order is offered to the best free provider (same ranking as
  the dispatcher hint); a decline or timeout passes it to the next one; nobody is
  offered the same order twice; after N tries it waits for a dispatcher and the team
  is notified once. The worker retries waiting orders every minute. Settings
  assign.auto / assign.maxAutoOffers (toggle on the Orders page, checkbox in old admin).
- Orders: list by day with groups (need a provider, offered, assigned, in progress,
  done, cancelled), periods and search; order card with progress, provider or ranked
  candidates ("offer" / "pick automatically"), details, checklist and photos,
  history in plain words, client, money (superadmin), payment, status change,
  reschedule (slots or custom time), cancel with fee.
- New order for a client (from the client card or the Orders page): params form,
  address, free slots for 14 days, price with the client's discount.
- Providers: list with rating, orders, declines, low ratings, next order; card with
  quality, ratings, upcoming orders, schedule and districts, admission documents,
  block/unblock with reason, new bot link, money and payouts (superadmin).
- Payout gets paidAt/note/createdById; Provider gets blockedReason/blockedAt/adminNote
  and documents (ProviderDoc).
- Tests for auto-assignment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 02:25:41 +04:00
1 parent 2772837e29
commit e7de4f5893
33 files changed
+4521 -45

No files matched your search

+716 -2
View File
@@ -386,6 +386,242 @@ const ru = {
'chat.notLinked': 'Рабочий бот не открыт',
'chat.details': 'Подробнее',
'chat.more': 'Ещё действия',
// ───── заказы и исполнители (этап 3) ─────
'ord.title': 'Заказы',
'ord.new': 'Новый заказ',
'ord.search': '№, клиент, телефон, адрес или исполнитель',
'ord.period': 'Период',
'ord.period.all': 'Все даты',
'ord.period.today': 'Сегодня',
'ord.period.tomorrow': 'Завтра',
'ord.period.week': '7 дней',
'ord.period.past': 'Прошедшие',
'ord.group.active': 'В работе у команды',
'ord.group.new': 'Ждут исполнителя',
'ord.group.pending': 'Предложены',
'ord.group.assigned': 'Назначены',
'ord.group.in_progress': 'Выполняются',
'ord.group.done': 'Готово',
'ord.group.cancelled': 'Отменены',
'ord.today': 'Сегодня',
'ord.tomorrow': 'Завтра',
'ord.empty': 'Заказов с такими условиями нет.',
'ord.waitsAnswer': 'ждём ответа: {name}',
'ord.needsHuman': 'нужен диспетчер',
'ord.searching': 'система подбирает…',
'ord.overdue': 'время прошло, исполнителя нет',
'ord.needsDispatcher': 'Автораспределение не нашло исполнителя для заказов: {n}. Назначьте вручную.',
'ord.autoOn': 'Автораспределение включено',
'ord.autoOff': 'Автораспределение выключено',
'ord.autoHint': 'Новый заказ сам предлагается лучшему свободному исполнителю (язык клиента, район, загрузка, рейтинг). Нет ответа {minutes} мин или отказ — следующему; после {tries} попыток — диспетчеру.',
'ord.back': 'Все заказы',
'ord.progress': 'Ход заказа',
'ord.details': 'Детали',
'ord.when': 'Когда',
'ord.where': 'Где',
'ord.minutes': '{n} мин',
'ord.params': 'Параметры',
'ord.extras': 'Доп. услуги',
'ord.note': 'Комментарий клиента',
'ord.lang': 'Язык клиента',
'ord.created': 'Создан',
'ord.checklist': 'Чек-лист',
'ord.needsPhoto': 'нужно фото',
'ord.photo': 'Фото',
'ord.history': 'История',
'ord.client': 'Клиент',
'ord.writeClient': 'Написать клиенту',
'ord.writeProvider': 'Написать исполнителю',
'ord.money': 'Деньги',
'ord.base': 'Без скидки',
'ord.discount': 'Скидка {pct}%',
'ord.payout': 'Исполнителю',
'ord.cancelFee': 'Удержание за отмену',
'ord.payment': 'Оплата',
'ord.markPaid': 'Отметить оплату',
'ord.confirmPaid': 'Отметить, что клиент оплатил заказ?',
'ord.paidDone': 'Оплата отмечена',
'ord.actions': 'Действия',
'ord.setStatus': 'Статус: {status}',
'ord.confirmStatus': 'Перевести заказ в «{status}»? Клиент получит уведомление.',
'ord.statusChanged': 'Статус изменён',
'ord.reschedule': 'Перенести',
'ord.rescheduleTitle': 'Перенос заказа №{n}',
'ord.rescheduleHint': 'Если исполнитель занят в новое время, заказ вернётся в «Ждут исполнителя» и уйдёт в автораспределение.',
'ord.rescheduled': 'Заказ перенесён, исполнитель тот же',
'ord.rescheduledFree': 'Заказ перенесён; исполнитель занят — ищем нового',
'ord.comment': 'Комментарий',
'ord.commentPh': 'Например: клиент попросил по телефону',
'ord.cancel': 'Отменить заказ',
'ord.cancelTitle': 'Отмена заказа',
'ord.cancelReason': 'Причина отмены',
'ord.cancelReasonPh': 'Например: клиент передумал',
'ord.cancelFeeHint': 'Сколько удержать с клиента, ֏. Обычно 0.',
'ord.cancelHint': 'Клиент и исполнитель получат уведомление.',
'ord.cancelled': 'Заказ отменён',
'ord.rating': 'Оценка клиента',
'ord.noComment': 'Без комментария',
'ord.offers': 'Кому предлагали',
'ord.bySystem': 'система',
'ord.provider': 'Исполнитель',
'ord.unassign': 'Снять исполнителя',
'ord.revoke': 'Отозвать предложение',
'ord.confirmUnassign': 'Снять исполнителя? Заказ вернётся в «Ждут исполнителя».',
'ord.confirmRevoke': 'Отозвать предложение? Заказ снова будет ждать исполнителя.',
'ord.unassigned': 'Готово — заказ снова ждёт исполнителя',
'ord.pendingProvider': 'Предложен: {name}',
'ord.pendingCompany': 'Передан компании «{name}» — ждём, кого назначит диспетчер',
'ord.byAuto': 'предложила система',
'ord.minLeft': 'на ответ ≈ {n} мин',
'ord.pickHint': 'Свободные на это время — по порядку подбора. Или поручите выбор системе.',
'ord.autoPick': 'Подобрать автоматически',
'ord.autoOffered': 'Предложили: {name}',
'ord.autoNone': 'Свободных исполнителей не нашлось',
'ord.noFree': 'На это время все заняты. Перенесите заказ или договоритесь с исполнителем.',
'ord.busy': 'Заняты в это время: {n}',
'ord.company': 'компания',
'ord.tried': 'уже предлагали',
'ord.load': 'заказов в этот день: {n}',
'ord.offer': 'Предложить',
'ord.offered': 'Заказ отправлен исполнителю',
'ord.noProvider': 'Исполнитель не назначался.',
'ord.ev.created': 'Заказ создан',
'ord.ev.createdByStaff': 'Заказ оформила команда',
'ord.ev.offered': 'Предложен исполнителю',
'ord.ev.autoOffered': 'Автораспределение: предложен',
'ord.ev.autoNoCandidate': 'Автораспределение: свободных нет',
'ord.ev.autoGaveUp': 'Автораспределение остановлено — нужен диспетчер',
'no.title': 'Новый заказ',
'no.client': 'Клиент',
'no.clientPh': 'Имя, @username или телефон',
'no.noClients': 'Никого не нашли',
'no.changeClient': 'Другой клиент',
'no.service': 'Услуга',
'no.extras': 'Доп. услуги',
'no.address': 'Адрес',
'no.addressPh': 'Улица, дом, квартира',
'no.district': 'Район',
'no.when': 'Дата и время',
'no.day': 'День',
'no.noSlots': 'На этот день свободного времени нет — выберите другой день или «другое время».',
'no.otherTime': 'Другое время (договорились с исполнителем сами)',
'no.customTime': 'Время',
'no.customHint': 'По Еревану. Свободных исполнителей система не гарантирует.',
'no.note': 'Комментарий для исполнителя',
'no.notePh': 'Код домофона, животные…',
'no.payment': 'Оплата',
'no.hint': 'Цена — с учётом подписки клиента. Клиент получит уведомление в бота, заказ сразу уйдёт в автораспределение.',
'no.create': 'Оформить заказ',
'no.created': 'Заказ №{n} оформлен',
'prv.title': 'Исполнители',
'prv.add': 'Добавить',
'prv.search': 'Имя или телефон',
'prv.status.active': 'Работают',
'prv.status.blocked': 'Заблокированы',
'prv.status.all': 'Все',
'prv.empty': 'Никого не нашли.',
'prv.col.name': 'Исполнитель',
'prv.col.rating': 'Рейтинг',
'prv.col.done': 'Заказов',
'prv.col.refused': 'Отказы',
'prv.col.next': 'Ближайший заказ',
'prv.period30': 'Заказы, отказы и низкие оценки — за 30 дней.',
'prv.blocked': 'заблокирован',
'prv.onShift': 'на смене',
'prv.offShift': 'не на смене',
'prv.notLinked': 'не открыл бота',
'prv.noDocs': 'нет документов',
'prv.ratings': 'оценок: {n}',
'prv.low': 'низких: {n}',
'prv.today': 'сегодня: {n}',
'prv.back': 'Все исполнители',
'prv.write': 'Написать',
'prv.edit': 'Редактировать',
'prv.blockedSince': 'Заблокирован с {date}.',
'prv.unblock': 'Вернуть к работе',
'prv.inviteHint': 'Исполнитель ещё не открыл рабочий бот — отправьте ему ссылку.',
'prv.copyInvite': 'Скопировать ссылку',
'prv.quality': 'Качество',
'prv.days30': 'за 30 дней',
'prv.acceptRate': 'принял {a} из {n} предложений',
'prv.refusals': 'отказы: {d}, молчал: {e}',
'prv.lowRatings': 'оценок 3 и ниже',
'prv.upcoming': 'Ближайшие заказы',
'prv.noUpcoming': 'Назначенных заказов нет.',
'prv.recent': 'Последние выполненные',
'prv.balance': 'К выплате',
'prv.thisMonth': 'Этот месяц',
'prv.prevMonth': 'Прошлый',
'prv.paidTotal': 'Выплачено',
'prv.payout': 'Отметить выплату',
'prv.payoutTitle': 'Выплата исполнителю',
'prv.amount': 'Сумма, ֏',
'prv.balanceHint': 'Долг перед исполнителем: {sum}',
'prv.payoutNote': 'Как выплатили',
'prv.payoutNotePh': 'Наличные / перевод на карту…',
'prv.payoutConfirm': 'Выплачено',
'prv.payoutDone': 'Выплата отмечена',
'prv.rateCompany': 'ставка компании {v}',
'prv.rateOwn': 'ставка {v}',
'prv.ratePreset': 'по пресету «{name}»',
'prv.work': 'Работа',
'prv.languages': 'Языки',
'prv.districts': 'Районы',
'prv.anyDistrict': 'любые',
'prv.districtsHint': 'Подбор ставит выше тех, кто работает в районе заказа.',
'prv.docs': 'Документы для допуска',
'prv.noDocsHint': 'Паспорт, договор, справка — всё хранится в закрытом хранилище.',
'prv.docTitlePh': 'Название: паспорт, договор…',
'prv.upload': 'Загрузить',
'prv.docAdded': 'Документ добавлен',
'prv.removeDocConfirm': 'Удалить документ?',
'prv.access': 'Допуск',
'prv.block': 'Заблокировать',
'prv.blockTitle': 'Заблокировать исполнителя',
'prv.blockReason': 'Причина',
'prv.blockReasonPh': 'Например: жалобы клиентов, нет документов',
'prv.blockHint': 'Заказы ему больше не предлагаются, действующие предложения отзываются. Уже назначенные заказы остаются — переназначьте их вручную.',
'prv.resetInvite': 'Новая ссылка для входа в бот',
'prv.resetConfirm': 'Сделать новую ссылку? Текущая привязка к Telegram снимется — исполнителю нужно будет войти заново.',
'prv.since': 'С нами с {date}',
'prv.saved': 'Сохранено',
'prv.addTitle': 'Новый исполнитель',
'prv.editTitle': 'Профиль исполнителя',
'prv.name': 'Имя',
'prv.role': 'Роль',
'prv.company': 'Компания',
'prv.freelance': 'Частный исполнитель',
'prv.schedule': 'График',
'prv.rate': 'Ставка',
'prv.note': 'Заметка команды',
'err.REASON_REQUIRED': 'Укажите причину.',
'err.SLOT_UNAVAILABLE': 'Это время уже занято или прошло. Выберите другое или «другое время».',
'err.ALREADY_ASSIGNED': 'Заказ уже назначен или предложен — обновите страницу.',
'err.PROVIDER_BUSY': 'Исполнитель занят в это время.',
'err.TRANSITION_NOT_ALLOWED': 'Сейчас этот шаг для заказа недоступен.',
'err.CHECKLIST_INCOMPLETE': 'Сначала исполнитель должен закрыть чек-лист и приложить фото.',
'err.CONSENT_REQUIRED': 'Клиент ещё не дал согласие на обработку данных в боте — заказ оформить нельзя.',
'err.ADDRESS_REQUIRED': 'Укажите адрес и район.',
'err.INVALID_PARAMS': 'Проверьте параметры заказа.',
'err.OFFER_NOT_ACTIVE': 'Предложение уже неактуально.',
'err.BAD_FILE_TYPE_DOC': 'Можно фото или PDF.',
'audit.bo.orders.assign': 'предложил(а) заказ исполнителю',
'audit.bo.orders.auto': 'запустил(а) подбор исполнителя',
'audit.bo.orders.unassign': 'снял(а) исполнителя с заказа',
'audit.bo.orders.transition': 'сменил(а) статус заказа',
'audit.bo.orders.cancel': 'отменил(а) заказ',
'audit.bo.orders.reschedule': 'перенёс(ла) заказ',
'audit.bo.orders.markPaid': 'отметил(а) оплату заказа',
'audit.bo.orders.create': 'оформил(а) заказ за клиента',
'audit.bo.orders.setAutoSetting': 'изменил(а) автораспределение',
'audit.bo.providers.create': 'добавил(а) исполнителя',
'audit.bo.providers.update': 'изменил(а) профиль исполнителя',
'audit.bo.providers.setActive': 'изменил(а) допуск исполнителя',
'audit.bo.providers.resetInvite': 'выпустил(а) новую ссылку исполнителю',
'audit.bo.providers.removeDoc': 'удалил(а) документ исполнителя',
'audit.bo.providers.addDoc': 'загрузил(а) документ исполнителя',
'audit.bo.providers.payout': 'отметил(а) выплату исполнителю',
'chat.order': 'Заказ №{n}',
'chat.noOrder': 'Без заказа',
'chat.noOrderNow': 'Сейчас без заказа',
@@ -792,6 +1028,242 @@ const en: Dict = {
'chat.notLinked': 'Work bot not opened',
'chat.details': 'Details',
'chat.more': 'More actions',
// ───── заказы и исполнители (этап 3) ─────
'ord.title': 'Orders',
'ord.new': 'New order',
'ord.search': 'No., client, phone, address or provider',
'ord.period': 'Period',
'ord.period.all': 'All dates',
'ord.period.today': 'Today',
'ord.period.tomorrow': 'Tomorrow',
'ord.period.week': '7 days',
'ord.period.past': 'Past',
'ord.group.active': 'Active',
'ord.group.new': 'Need a provider',
'ord.group.pending': 'Offered',
'ord.group.assigned': 'Assigned',
'ord.group.in_progress': 'In progress',
'ord.group.done': 'Done',
'ord.group.cancelled': 'Cancelled',
'ord.today': 'Today',
'ord.tomorrow': 'Tomorrow',
'ord.empty': 'No orders match these filters.',
'ord.waitsAnswer': 'awaiting: {name}',
'ord.needsHuman': 'needs a dispatcher',
'ord.searching': 'auto-assigning…',
'ord.overdue': 'time passed, no provider',
'ord.needsDispatcher': 'Auto-assignment found no provider for {n} order(s). Assign manually.',
'ord.autoOn': 'Auto-assign on',
'ord.autoOff': 'Auto-assign off',
'ord.autoHint': 'A new order is offered to the best free provider (client language, district, load, rating). No answer in {minutes} min or a decline — the next one; after {tries} tries — to a dispatcher.',
'ord.back': 'All orders',
'ord.progress': 'Progress',
'ord.details': 'Details',
'ord.when': 'When',
'ord.where': 'Where',
'ord.minutes': '{n} min',
'ord.params': 'Details',
'ord.extras': 'Extras',
'ord.note': 'Client comment',
'ord.lang': 'Client language',
'ord.created': 'Created',
'ord.checklist': 'Checklist',
'ord.needsPhoto': 'photo needed',
'ord.photo': 'Photo',
'ord.history': 'History',
'ord.client': 'Client',
'ord.writeClient': 'Message the client',
'ord.writeProvider': 'Message the provider',
'ord.money': 'Money',
'ord.base': 'Before discount',
'ord.discount': 'Discount {pct}%',
'ord.payout': 'To the provider',
'ord.cancelFee': 'Cancellation fee',
'ord.payment': 'Payment',
'ord.markPaid': 'Mark as paid',
'ord.confirmPaid': 'Mark the order as paid?',
'ord.paidDone': 'Marked as paid',
'ord.actions': 'Actions',
'ord.setStatus': 'Status: {status}',
'ord.confirmStatus': 'Move the order to “{status}”? The client will be notified.',
'ord.statusChanged': 'Status changed',
'ord.reschedule': 'Reschedule',
'ord.rescheduleTitle': 'Reschedule order #{n}',
'ord.rescheduleHint': 'If the provider is busy at the new time, the order goes back to “Need a provider” and to auto-assignment.',
'ord.rescheduled': 'Rescheduled, same provider',
'ord.rescheduledFree': 'Rescheduled; the provider is busy — looking for a new one',
'ord.comment': 'Comment',
'ord.commentPh': 'E.g. the client asked by phone',
'ord.cancel': 'Cancel order',
'ord.cancelTitle': 'Cancel the order',
'ord.cancelReason': 'Reason',
'ord.cancelReasonPh': 'E.g. the client changed their mind',
'ord.cancelFeeHint': 'How much to charge the client, ֏. Usually 0.',
'ord.cancelHint': 'The client and the provider will be notified.',
'ord.cancelled': 'Order cancelled',
'ord.rating': 'Client rating',
'ord.noComment': 'No comment',
'ord.offers': 'Offers',
'ord.bySystem': 'system',
'ord.provider': 'Provider',
'ord.unassign': 'Remove provider',
'ord.revoke': 'Withdraw offer',
'ord.confirmUnassign': 'Remove the provider? The order goes back to “Need a provider”.',
'ord.confirmRevoke': 'Withdraw the offer? The order will wait for a provider again.',
'ord.unassigned': 'Done — the order needs a provider again',
'ord.pendingProvider': 'Offered to {name}',
'ord.pendingCompany': 'Passed to {name} — waiting for their dispatcher',
'ord.byAuto': 'offered automatically',
'ord.minLeft': '≈ {n} min to answer',
'ord.pickHint': 'Free at this time, best match first. Or let the system pick.',
'ord.autoPick': 'Pick automatically',
'ord.autoOffered': 'Offered to {name}',
'ord.autoNone': 'No free provider found',
'ord.noFree': 'Everyone is busy at this time. Reschedule or arrange with a provider.',
'ord.busy': 'Busy at this time: {n}',
'ord.company': 'company',
'ord.tried': 'already offered',
'ord.load': '{n} orders that day',
'ord.offer': 'Offer',
'ord.offered': 'Offer sent',
'ord.noProvider': 'No provider was assigned.',
'ord.ev.created': 'Order created',
'ord.ev.createdByStaff': 'Created by the team',
'ord.ev.offered': 'Offered to a provider',
'ord.ev.autoOffered': 'Auto-assign: offered',
'ord.ev.autoNoCandidate': 'Auto-assign: nobody free',
'ord.ev.autoGaveUp': 'Auto-assign stopped — needs a dispatcher',
'no.title': 'New order',
'no.client': 'Client',
'no.clientPh': 'Name, @username or phone',
'no.noClients': 'Nobody found',
'no.changeClient': 'Another client',
'no.service': 'Service',
'no.extras': 'Extras',
'no.address': 'Address',
'no.addressPh': 'Street, building, apartment',
'no.district': 'District',
'no.when': 'Date and time',
'no.day': 'Day',
'no.noSlots': 'No free time that day — pick another day or “other time”.',
'no.otherTime': 'Other time (arranged with a provider directly)',
'no.customTime': 'Time',
'no.customHint': 'Yerevan time. A free provider is not guaranteed.',
'no.note': 'Comment for the provider',
'no.notePh': 'Door code, pets…',
'no.payment': 'Payment',
'no.hint': 'The price includes the client’s subscription discount. The client is notified and the order goes to auto-assignment.',
'no.create': 'Create order',
'no.created': 'Order #{n} created',
'prv.title': 'Providers',
'prv.add': 'Add',
'prv.search': 'Name or phone',
'prv.status.active': 'Active',
'prv.status.blocked': 'Blocked',
'prv.status.all': 'All',
'prv.empty': 'Nobody found.',
'prv.col.name': 'Provider',
'prv.col.rating': 'Rating',
'prv.col.done': 'Orders',
'prv.col.refused': 'Declines',
'prv.col.next': 'Next order',
'prv.period30': 'Orders, declines and low ratings — last 30 days.',
'prv.blocked': 'blocked',
'prv.onShift': 'on shift',
'prv.offShift': 'off shift',
'prv.notLinked': 'bot not opened',
'prv.noDocs': 'no documents',
'prv.ratings': '{n} ratings',
'prv.low': '{n} low',
'prv.today': 'today: {n}',
'prv.back': 'All providers',
'prv.write': 'Message',
'prv.edit': 'Edit',
'prv.blockedSince': 'Blocked since {date}.',
'prv.unblock': 'Unblock',
'prv.inviteHint': 'The provider hasn’t opened the work bot yet — send them the link.',
'prv.copyInvite': 'Copy link',
'prv.quality': 'Quality',
'prv.days30': 'last 30 days',
'prv.acceptRate': 'accepted {a} of {n} offers',
'prv.refusals': 'declined: {d}, no answer: {e}',
'prv.lowRatings': 'ratings of 3 or less',
'prv.upcoming': 'Upcoming orders',
'prv.noUpcoming': 'No assigned orders.',
'prv.recent': 'Recently done',
'prv.balance': 'To pay',
'prv.thisMonth': 'This month',
'prv.prevMonth': 'Last month',
'prv.paidTotal': 'Paid',
'prv.payout': 'Record a payout',
'prv.payoutTitle': 'Payout to provider',
'prv.amount': 'Amount, ֏',
'prv.balanceHint': 'Owed to the provider: {sum}',
'prv.payoutNote': 'How it was paid',
'prv.payoutNotePh': 'Cash / card transfer…',
'prv.payoutConfirm': 'Paid',
'prv.payoutDone': 'Payout recorded',
'prv.rateCompany': 'company rate {v}',
'prv.rateOwn': 'rate {v}',
'prv.ratePreset': 'preset “{name}”',
'prv.work': 'Work',
'prv.languages': 'Languages',
'prv.districts': 'Districts',
'prv.anyDistrict': 'any',
'prv.districtsHint': 'Assignment favors providers working in the order’s district.',
'prv.docs': 'Admission documents',
'prv.noDocsHint': 'Passport, contract, certificate — kept in private storage.',
'prv.docTitlePh': 'Title: passport, contract…',
'prv.upload': 'Upload',
'prv.docAdded': 'Document added',
'prv.removeDocConfirm': 'Delete the document?',
'prv.access': 'Access',
'prv.block': 'Block',
'prv.blockTitle': 'Block the provider',
'prv.blockReason': 'Reason',
'prv.blockReasonPh': 'E.g. client complaints, missing documents',
'prv.blockHint': 'No new offers; pending offers are withdrawn. Already assigned orders stay — reassign them manually.',
'prv.resetInvite': 'New bot link',
'prv.resetConfirm': 'Create a new link? The current Telegram link is removed — the provider will need to log in again.',
'prv.since': 'With us since {date}',
'prv.saved': 'Saved',
'prv.addTitle': 'New provider',
'prv.editTitle': 'Provider profile',
'prv.name': 'Name',
'prv.role': 'Role',
'prv.company': 'Company',
'prv.freelance': 'Freelancer',
'prv.schedule': 'Schedule',
'prv.rate': 'Rate',
'prv.note': 'Team note',
'err.REASON_REQUIRED': 'Please give a reason.',
'err.SLOT_UNAVAILABLE': 'This time is taken or has passed. Pick another one.',
'err.ALREADY_ASSIGNED': 'The order is already assigned or offered — refresh the page.',
'err.PROVIDER_BUSY': 'The provider is busy at this time.',
'err.TRANSITION_NOT_ALLOWED': 'This step isn’t available for the order now.',
'err.CHECKLIST_INCOMPLETE': 'The provider must finish the checklist and add photos first.',
'err.CONSENT_REQUIRED': 'The client hasn’t given data-processing consent in the bot yet.',
'err.ADDRESS_REQUIRED': 'Please enter the address and district.',
'err.INVALID_PARAMS': 'Check the order details.',
'err.OFFER_NOT_ACTIVE': 'The offer is no longer active.',
'err.BAD_FILE_TYPE_DOC': 'Photo or PDF only.',
'audit.bo.orders.assign': 'offered an order',
'audit.bo.orders.auto': 'ran auto-assignment',
'audit.bo.orders.unassign': 'removed a provider from an order',
'audit.bo.orders.transition': 'changed an order status',
'audit.bo.orders.cancel': 'cancelled an order',
'audit.bo.orders.reschedule': 'rescheduled an order',
'audit.bo.orders.markPaid': 'marked an order as paid',
'audit.bo.orders.create': 'created an order for a client',
'audit.bo.orders.setAutoSetting': 'changed auto-assignment',
'audit.bo.providers.create': 'added a provider',
'audit.bo.providers.update': 'edited a provider',
'audit.bo.providers.setActive': 'changed provider access',
'audit.bo.providers.resetInvite': 'reissued a provider link',
'audit.bo.providers.removeDoc': 'deleted a provider document',
'audit.bo.providers.addDoc': 'uploaded a provider document',
'audit.bo.providers.payout': 'recorded a payout',
'chat.order': 'Order #{n}',
'chat.noOrder': 'No order',
'chat.noOrderNow': 'No current order',
@@ -1195,6 +1667,242 @@ const hy: Dict = {
'chat.notLinked': 'Աշխատանքային բոտը բացված չէ',
'chat.details': 'Մանրամասներ',
'chat.more': 'Այլ գործողություններ',
// ───── заказы и исполнители (этап 3) ─────
'ord.title': 'Պատվերներ',
'ord.new': 'Նոր պատվեր',
'ord.search': '№, հաճախորդ, հեռախոս, հասցե կամ կատարող',
'ord.period': 'Ժամանակահատված',
'ord.period.all': 'Բոլոր օրերը',
'ord.period.today': 'Այսօր',
'ord.period.tomorrow': 'Վաղը',
'ord.period.week': '7 օր',
'ord.period.past': 'Անցածները',
'ord.group.active': 'Ակտիվ',
'ord.group.new': 'Սպասում են կատարողի',
'ord.group.pending': 'Առաջարկված',
'ord.group.assigned': 'Նշանակված',
'ord.group.in_progress': 'Կատարվում են',
'ord.group.done': 'Պատրաստ',
'ord.group.cancelled': 'Չեղարկված',
'ord.today': 'Այսօր',
'ord.tomorrow': 'Վաղը',
'ord.empty': 'Այս պայմաններով պատվերներ չկան։',
'ord.waitsAnswer': 'սպասում ենք՝ {name}',
'ord.needsHuman': 'պետք է դիսպետչեր',
'ord.searching': 'համակարգը ընտրում է…',
'ord.overdue': 'ժամն անցել է, կատարող չկա',
'ord.needsDispatcher': 'Ավտոմատ բաշխումը կատարող չգտավ {n} պատվերի համար։ Նշանակեք ձեռքով։',
'ord.autoOn': 'Ավտոմատ բաշխումը միացված է',
'ord.autoOff': 'Ավտոմատ բաշխումն անջատված է',
'ord.autoHint': 'Նոր պատվերն ինքնաբերաբար առաջարկվում է լավագույն ազատ կատարողին։ {minutes} րոպեում պատասխան չկա կամ հրաժարվեց՝ հաջորդին, {tries} փորձից հետո՝ դիսպետչերին։',
'ord.back': 'Բոլոր պատվերները',
'ord.progress': 'Ընթացք',
'ord.details': 'Մանրամասներ',
'ord.when': 'Երբ',
'ord.where': 'Որտեղ',
'ord.minutes': '{n} ր',
'ord.params': 'Պարամետրեր',
'ord.extras': 'Լրացուցիչ',
'ord.note': 'Հաճախորդի մեկնաբանություն',
'ord.lang': 'Հաճախորդի լեզուն',
'ord.created': 'Ստեղծված',
'ord.checklist': 'Ստուգացանկ',
'ord.needsPhoto': 'լուսանկար է պետք',
'ord.photo': 'Լուսանկար',
'ord.history': 'Պատմություն',
'ord.client': 'Հաճախորդ',
'ord.writeClient': 'Գրել հաճախորդին',
'ord.writeProvider': 'Գրել կատարողին',
'ord.money': 'Գումար',
'ord.base': 'Առանց զեղչի',
'ord.discount': 'Զեղչ {pct}%',
'ord.payout': 'Կատարողին',
'ord.cancelFee': 'Չեղարկման պահում',
'ord.payment': 'Վճարում',
'ord.markPaid': 'Նշել վճարումը',
'ord.confirmPaid': 'Նշե՞լ, որ հաճախորդը վճարել է։',
'ord.paidDone': 'Վճարումը նշված է',
'ord.actions': 'Գործողություններ',
'ord.setStatus': 'Կարգավիճակ՝ {status}',
'ord.confirmStatus': 'Տեղափոխե՞լ «{status}»։ Հաճախորդը ծանուցում կստանա։',
'ord.statusChanged': 'Կարգավիճակը փոխված է',
'ord.reschedule': 'Տեղափոխել',
'ord.rescheduleTitle': 'Պատվեր №{n}-ի տեղափոխում',
'ord.rescheduleHint': 'Եթե կատարողը զբաղված է նոր ժամին, պատվերը կվերադառնա «Սպասում են կատարողի»։',
'ord.rescheduled': 'Տեղափոխված է, կատարողը նույնն է',
'ord.rescheduledFree': 'Տեղափոխված է, կատարողը զբաղված է՝ փնտրում ենք նորը',
'ord.comment': 'Մեկնաբանություն',
'ord.commentPh': 'Օրինակ՝ հաճախորդը խնդրեց հեռախոսով',
'ord.cancel': 'Չեղարկել պատվերը',
'ord.cancelTitle': 'Պատվերի չեղարկում',
'ord.cancelReason': 'Չեղարկման պատճառը',
'ord.cancelReasonPh': 'Օրինակ՝ հաճախորդը փոխեց միտքը',
'ord.cancelFeeHint': 'Որքան պահել հաճախորդից, ֏։ Սովորաբար 0։',
'ord.cancelHint': 'Հաճախորդը և կատարողը ծանուցում կստանան։',
'ord.cancelled': 'Պատվերը չեղարկված է',
'ord.rating': 'Հաճախորդի գնահատականը',
'ord.noComment': 'Առանց մեկնաբանության',
'ord.offers': 'Ում է առաջարկվել',
'ord.bySystem': 'համակարգ',
'ord.provider': 'Կատարող',
'ord.unassign': 'Հանել կատարողին',
'ord.revoke': 'Հետ կանչել առաջարկը',
'ord.confirmUnassign': 'Հանե՞լ կատարողին։ Պատվերը կվերադառնա։',
'ord.confirmRevoke': 'Հետ կանչե՞լ առաջարկը։',
'ord.unassigned': 'Պատվերը կրկին սպասում է կատարողի',
'ord.pendingProvider': 'Առաջարկված է՝ {name}',
'ord.pendingCompany': 'Հանձնված է «{name}» ընկերությանը',
'ord.byAuto': 'առաջարկել է համակարգը',
'ord.minLeft': 'պատասխանի համար ≈ {n} ր',
'ord.pickHint': 'Այս ժամին ազատներ՝ ըստ համապատասխանության։',
'ord.autoPick': 'Ընտրել ավտոմատ',
'ord.autoOffered': 'Առաջարկվեց՝ {name}',
'ord.autoNone': 'Ազատ կատարող չգտնվեց',
'ord.noFree': 'Այս ժամին բոլորը զբաղված են։',
'ord.busy': 'Զբաղված են՝ {n}',
'ord.company': 'ընկերություն',
'ord.tried': 'արդեն առաջարկվել է',
'ord.load': 'այդ օրը պատվերներ՝ {n}',
'ord.offer': 'Առաջարկել',
'ord.offered': 'Առաջարկն ուղարկված է',
'ord.noProvider': 'Կատարող չի նշանակվել։',
'ord.ev.created': 'Պատվերը ստեղծված է',
'ord.ev.createdByStaff': 'Ստեղծել է թիմը',
'ord.ev.offered': 'Առաջարկվել է կատարողին',
'ord.ev.autoOffered': 'Ավտոմատ բաշխում՝ առաջարկված',
'ord.ev.autoNoCandidate': 'Ավտոմատ բաշխում՝ ազատ չկա',
'ord.ev.autoGaveUp': 'Ավտոմատ բաշխումը կանգնեց',
'no.title': 'Նոր պատվեր',
'no.client': 'Հաճախորդ',
'no.clientPh': 'Անուն, @username կամ հեռախոս',
'no.noClients': 'Ոչ ոք չգտնվեց',
'no.changeClient': 'Այլ հաճախորդ',
'no.service': 'Ծառայություն',
'no.extras': 'Լրացուցիչ',
'no.address': 'Հասցե',
'no.addressPh': 'Փողոց, շենք, բնակարան',
'no.district': 'Վարչական շրջան',
'no.when': 'Օր և ժամ',
'no.day': 'Օր',
'no.noSlots': 'Այս օրը ազատ ժամ չկա։',
'no.otherTime': 'Այլ ժամ (պայմանավորվել ենք կատարողի հետ)',
'no.customTime': 'Ժամ',
'no.customHint': 'Երևանի ժամով։',
'no.note': 'Մեկնաբանություն կատարողի համար',
'no.notePh': 'Դոմոֆոնի կոդ, կենդանիներ…',
'no.payment': 'Վճարում',
'no.hint': 'Գինը՝ հաշվի առնելով բաժանորդագրությունը։ Հաճախորդը ծանուցում կստանա։',
'no.create': 'Ձևակերպել պատվերը',
'no.created': 'Պատվեր №{n}-ը ձևակերպված է',
'prv.title': 'Կատարողներ',
'prv.add': 'Ավելացնել',
'prv.search': 'Անուն կամ հեռախոս',
'prv.status.active': 'Աշխատում են',
'prv.status.blocked': 'Արգելափակված',
'prv.status.all': 'Բոլորը',
'prv.empty': 'Ոչ ոք չգտնվեց։',
'prv.col.name': 'Կատարող',
'prv.col.rating': 'Վարկանիշ',
'prv.col.done': 'Պատվերներ',
'prv.col.refused': 'Հրաժարումներ',
'prv.col.next': 'Մոտակա պատվերը',
'prv.period30': 'Պատվերներ, հրաժարումներ, ցածր գնահատականներ՝ 30 օրում։',
'prv.blocked': 'արգելափակված',
'prv.onShift': 'հերթափոխում',
'prv.offShift': 'հերթափոխում չէ',
'prv.notLinked': 'բոտը բացված չէ',
'prv.noDocs': 'փաստաթղթեր չկան',
'prv.ratings': 'գնահատականներ՝ {n}',
'prv.low': 'ցածր՝ {n}',
'prv.today': 'այսօր՝ {n}',
'prv.back': 'Բոլոր կատարողները',
'prv.write': 'Գրել',
'prv.edit': 'Խմբագրել',
'prv.blockedSince': 'Արգելափակված է {date}-ից։',
'prv.unblock': 'Վերադարձնել աշխատանքի',
'prv.inviteHint': 'Կատարողը դեռ չի բացել աշխատանքային բոտը՝ ուղարկեք հղումը։',
'prv.copyInvite': 'Պատճենել հղումը',
'prv.quality': 'Որակ',
'prv.days30': '30 օրում',
'prv.acceptRate': 'ընդունել է {a}-ը {n}-ից',
'prv.refusals': 'հրաժարվել՝ {d}, լռել՝ {e}',
'prv.lowRatings': 'գնահատական 3 և ցածր',
'prv.upcoming': 'Մոտակա պատվերներ',
'prv.noUpcoming': 'Նշանակված պատվերներ չկան։',
'prv.recent': 'Վերջին կատարվածները',
'prv.balance': 'Վճարման ենթակա',
'prv.thisMonth': 'Այս ամիս',
'prv.prevMonth': 'Անցյալ',
'prv.paidTotal': 'Վճարված',
'prv.payout': 'Նշել վճարումը',
'prv.payoutTitle': 'Վճարում կատարողին',
'prv.amount': 'Գումար, ֏',
'prv.balanceHint': 'Պարտք կատարողին՝ {sum}',
'prv.payoutNote': 'Ինչպես է վճարվել',
'prv.payoutNotePh': 'Կանխիկ / փոխանցում…',
'prv.payoutConfirm': 'Վճարված է',
'prv.payoutDone': 'Վճարումը նշված է',
'prv.rateCompany': 'ընկերության դրույք {v}',
'prv.rateOwn': 'դրույք {v}',
'prv.ratePreset': '«{name}» նախադրույքով',
'prv.work': 'Աշխատանք',
'prv.languages': 'Լեզուներ',
'prv.districts': 'Շրջաններ',
'prv.anyDistrict': 'ցանկացած',
'prv.districtsHint': 'Ընտրությունը նախապատվություն է տալիս շրջանում աշխատողներին։',
'prv.docs': 'Թույլտվության փաստաթղթեր',
'prv.noDocsHint': 'Անձնագիր, պայմանագիր, տեղեկանք՝ փակ պահոցում։',
'prv.docTitlePh': 'Անվանում՝ անձնագիր, պայմանագիր…',
'prv.upload': 'Վերբեռնել',
'prv.docAdded': 'Փաստաթուղթն ավելացված է',
'prv.removeDocConfirm': 'Ջնջե՞լ փաստաթուղթը։',
'prv.access': 'Թույլտվություն',
'prv.block': 'Արգելափակել',
'prv.blockTitle': 'Արգելափակել կատարողին',
'prv.blockReason': 'Պատճառը',
'prv.blockReasonPh': 'Օրինակ՝ բողոքներ, փաստաթղթեր չկան',
'prv.blockHint': 'Նոր առաջարկներ չեն լինի։ Նշանակված պատվերները վերանշանակեք ձեռքով։',
'prv.resetInvite': 'Նոր հղում բոտի համար',
'prv.resetConfirm': 'Ստեղծե՞լ նոր հղում։ Ընթացիկ կապը կհանվի։',
'prv.since': 'Մեզ հետ է {date}-ից',
'prv.saved': 'Պահպանված է',
'prv.addTitle': 'Նոր կատարող',
'prv.editTitle': 'Կատարողի պրոֆիլ',
'prv.name': 'Անուն',
'prv.role': 'Դեր',
'prv.company': 'Ընկերություն',
'prv.freelance': 'Անհատ կատարող',
'prv.schedule': 'Ժամանակացույց',
'prv.rate': 'Դրույք',
'prv.note': 'Թիմի նշում',
'err.REASON_REQUIRED': 'Նշեք պատճառը։',
'err.SLOT_UNAVAILABLE': 'Այս ժամը զբաղված է կամ անցել է։',
'err.ALREADY_ASSIGNED': 'Պատվերն արդեն նշանակված է։',
'err.PROVIDER_BUSY': 'Կատարողը զբաղված է այդ ժամին։',
'err.TRANSITION_NOT_ALLOWED': 'Այս քայլը հիմա հասանելի չէ։',
'err.CHECKLIST_INCOMPLETE': 'Կատարողը նախ պետք է ավարտի ստուգացանկը։',
'err.CONSENT_REQUIRED': 'Հաճախորդը դեռ համաձայնություն չի տվել։',
'err.ADDRESS_REQUIRED': 'Նշեք հասցեն և շրջանը։',
'err.INVALID_PARAMS': 'Ստուգեք պատվերի պարամետրերը։',
'err.OFFER_NOT_ACTIVE': 'Առաջարկն այլևս ակտիվ չէ։',
'err.BAD_FILE_TYPE_DOC': 'Միայն լուսանկար կամ PDF։',
'audit.bo.orders.assign': 'առաջարկեց պատվերը',
'audit.bo.orders.auto': 'գործարկեց ավտոմատ ընտրությունը',
'audit.bo.orders.unassign': 'հանեց կատարողին',
'audit.bo.orders.transition': 'փոխեց պատվերի կարգավիճակը',
'audit.bo.orders.cancel': 'չեղարկեց պատվերը',
'audit.bo.orders.reschedule': 'տեղափոխեց պատվերը',
'audit.bo.orders.markPaid': 'նշեց վճարումը',
'audit.bo.orders.create': 'ձևակերպեց պատվեր',
'audit.bo.orders.setAutoSetting': 'փոխեց ավտոմատ բաշխումը',
'audit.bo.providers.create': 'ավելացրեց կատարող',
'audit.bo.providers.update': 'փոխեց կատարողի պրոֆիլը',
'audit.bo.providers.setActive': 'փոխեց կատարողի թույլտվությունը',
'audit.bo.providers.resetInvite': 'նոր հղում տվեց կատարողին',
'audit.bo.providers.removeDoc': 'ջնջեց կատարողի փաստաթուղթը',
'audit.bo.providers.addDoc': 'վերբեռնեց կատարողի փաստաթուղթը',
'audit.bo.providers.payout': 'նշեց վճարում կատարողին',
'chat.order': 'Պատվեր №{n}',
'chat.noOrder': 'Առանց պատվերի',
'chat.noOrderNow': 'Այժմ պատվեր չկա',
@@ -1235,8 +1943,12 @@ interface Ctx {
locale: BoLocale;
setLocale: (l: BoLocale) => void;
t: T;
/** общие переводы проекта: services.<code>.name и т.п. */
shared: (key: string) => string;
/** общие переводы проекта: services.<code>.name, roles.<ROLE>, params.* и т.п. */
shared: (key: string, params?: Record<string, string | number>) => string;
/** время по Еревану: 14:00 */
time: (d: Date) => string;
/** общий t проекта целиком — для хелперов @barev/web (paramsSummary и т.п.) */
sharedT: ReturnType<typeof createSharedT>;
amd: (n: number) => string;
date: (d: Date) => string;
dateTime: (d: Date) => string;
@@ -1279,10 +1991,12 @@ export function BoI18nProvider({ children }: { children: ReactNode }) {
},
t: makeT(locale),
shared: sharedT,
sharedT,
amd: (n) => formatAmd(locale as SharedLocale, n),
amdShort: (n) => `${compact.format(n)} ֏`,
date: (d) => formatDateTime(locale as SharedLocale, d, { day: 'numeric', month: 'short', year: 'numeric' }),
dateTime: (d) => formatDateTime(locale as SharedLocale, d, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }),
time: (d) => formatDateTime(locale as SharedLocale, d, { hour: '2-digit', minute: '2-digit' }),
};
}, [locale]);
return <I18nCtx.Provider value={value}>{children}</I18nCtx.Provider>;