Disable push quick replies and clarify payroll component labels

This commit is contained in:
lizerk 2026-09-04 19:00:31 +03:00
parent 29d8b05393
commit 2277d58967
6 changed files with 58 additions and 111 deletions

View File

@ -24,11 +24,11 @@ class VacationPayInstructionStatus(models.TextChoices):
class PayrollComponentKind(models.TextChoices):
EARNING = "earning", "Начисление"
ADJUSTMENT_CREDIT = "adjustment_credit", "Коррекция +"
ADJUSTMENT_DEBIT = "adjustment_debit", "Коррекция −"
DEDUCTION = "deduction", "Удержание"
PAYMENT = "payment", "Выплата"
EARNING = "earning", "Начислить сотруднику"
ADJUSTMENT_CREDIT = "adjustment_credit", "Положительная корректировка"
ADJUSTMENT_DEBIT = "adjustment_debit", "Отрицательная корректировка"
DEDUCTION = "deduction", "Удержать"
PAYMENT = "payment", "Уже выплачено"
class PayrollComponentPeriodScope(models.TextChoices):

View File

@ -0,0 +1,38 @@
from django.db import migrations, models
KIND_CHOICES = [
("earning", "Начислить сотруднику"),
("adjustment_credit", "Положительная корректировка"),
("adjustment_debit", "Отрицательная корректировка"),
("deduction", "Удержать"),
("payment", "Уже выплачено"),
]
class Migration(migrations.Migration):
dependencies = [
("finance", "0029_alter_payrollcustomcomponent_name"),
]
operations = [
migrations.AlterField(
model_name="payrollcustomcomponent",
name="kind",
field=models.CharField(
choices=KIND_CHOICES,
max_length=24,
verbose_name="Влияние на расчёт",
),
),
migrations.AlterField(
model_name="payrollstatementline",
name="kind",
field=models.CharField(
choices=KIND_CHOICES,
max_length=24,
verbose_name="Тип",
),
),
]

View File

@ -18,7 +18,6 @@ from notifications.chat_suppression import suppress_for_active_chat
from notifications.push_actions import (
build_push_avatar_token,
build_push_read_token,
build_push_reply_token,
)
@ -154,7 +153,6 @@ class PushNotificationSender(BaseNotificationSender):
"message_id": message_id,
"chat_name": source_data.get("chat_name"),
"chat_type": source_data.get("chat_type"),
"can_reply": source_data.get("can_reply", True),
"actor_name": actor_name,
},
}
@ -171,40 +169,19 @@ class PushNotificationSender(BaseNotificationSender):
chat_id=int(chat_id),
message_id=int(message_id),
)
reply_token = build_push_reply_token(
notification_id=notification.id,
user_id=notification.recipient_id,
chat_id=int(chat_id),
message_id=int(message_id),
)
except (TypeError, ValueError):
read_token = ""
reply_token = ""
if read_token and reply_token:
actions = []
if show_preview and source_data.get("can_reply", True):
actions.append({
"action": "reply",
"type": "text",
"title": "Ответить",
"placeholder": "Введите ответ…",
})
actions.append({
if read_token:
message_data["actions"] = [{
"action": "mark-read",
"type": "button",
"title": "Прочитано",
})
message_data["actions"] = actions
}]
message_data["data"].update({
"mark_read_url": "/api/v1/notifications/push/action/read/",
"mark_read_token": read_token,
})
if show_preview and source_data.get("can_reply", True):
message_data["data"].update({
"reply_url": "/api/v1/notifications/push/action/reply/",
"reply_token": reply_token,
})
actor_icon = (
self._get_actor_avatar_url(notification)

View File

@ -99,21 +99,15 @@ class TestPushNotificationSenderPayload:
assert payload["data"]["actor_name"] == "Рубцова Олеся"
assert payload["data"]["mark_read_url"].endswith("/push/action/read/")
assert payload["data"]["mark_read_token"]
assert payload["data"]["reply_url"].endswith("/push/action/reply/")
assert payload["data"]["reply_token"]
assert payload["actions"] == [
{
"action": "reply",
"type": "text",
"title": "Ответить",
"placeholder": "Введите ответ…",
},
{
"action": "mark-read",
"type": "button",
"title": "Прочитано",
},
]
assert "reply_url" not in payload["data"]
assert "reply_token" not in payload["data"]
assert payload["tag"] == "chat-123"
assert "icon" not in payload
assert "chat_new_message" not in json.dumps(payload)

View File

@ -6,7 +6,7 @@
* нативные уведомления даже когда страница закрыта.
*/
const SW_VERSION = '2.4.0';
const SW_VERSION = '2.4.1';
const SW_APP_KIND = self.CORP_SW_APP_KIND || 'main';
const STATIC_CACHE = `eusrr-static-${SW_VERSION}`;
const PRECACHE_URLS = [
@ -30,13 +30,6 @@ const DEFAULT_NOTIFICATION_OPTIONS = {
renotify: true,
};
function supportsNativeInlineReply() {
const userAgent = self.navigator?.userAgent || '';
const isChromium = /(?:Chrome|Chromium|Edg|OPR)\//.test(userAgent);
const isSupportedPlatform = /(?:Windows NT|Android|CrOS)/.test(userAgent);
return isChromium && isSupportedPlatform;
}
function buildNotificationData(payload = {}) {
const notificationData = {
title: payload.title || payload.head || 'Новое уведомление',
@ -58,7 +51,7 @@ function buildNotificationData(payload = {}) {
if (payload.image) notificationData.image = payload.image;
if (payload.actions) {
notificationData.actions = payload.actions.filter(
(action) => action.type !== 'text' || supportsNativeInlineReply(),
(action) => action.action !== 'reply',
);
}
return notificationData;
@ -139,48 +132,6 @@ async function markPushNotificationRead(notification) {
}));
}
async function sendPushQuickReply(notification, reply) {
const data = notification.data || {};
const normalizedReply = typeof reply === 'string' ? reply.trim() : '';
if (!normalizedReply) {
throw new Error('Native notification did not provide reply text');
}
if (!data.reply_url || !data.reply_token) {
throw new Error('Push notification has no signed reply action');
}
const response = await fetch(
new URL(data.reply_url, self.location.origin).toString(),
{
method: 'POST',
credentials: 'omit',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: data.reply_token,
reply: normalizedReply,
}),
},
);
if (!response.ok) {
throw new Error(`Push reply failed with HTTP ${response.status}`);
}
const result = await response.json();
const notifications = await self.registration.getNotifications({ includeTriggered: true });
notifications
.filter((item) => sameIdentity(item.data?.chat_id, data.chat_id))
.forEach((item) => item.close());
const windowClients = await clients.matchAll({ type: 'window', includeUncontrolled: true });
windowClients.forEach((client) => client.postMessage({
type: 'PUSH_CHAT_QUICK_REPLIED',
chatId: data.chat_id,
messageId: result.message_id,
replyToId: data.message_id,
notificationId: data.notification_id,
}));
}
/**
* Событие установки Service Worker
*/
@ -329,19 +280,6 @@ self.addEventListener('message', (event) => {
self.addEventListener('notificationclick', (event) => {
console.log('[SW] Notification clicked:', event.notification.tag);
if (event.action === 'reply') {
if (typeof event.reply !== 'string' || !event.reply.trim()) {
console.warn('[SW] Native inline reply is unavailable on this platform');
return;
}
event.waitUntil(
sendPushQuickReply(event.notification, event.reply).catch((error) => {
console.error('[SW] Failed to send push reply:', error);
}),
);
return;
}
if (event.action === 'mark-read') {
event.waitUntil(
markPushNotificationRead(event.notification).catch((error) => {

View File

@ -48,11 +48,11 @@ const statusMeta: Record<PayrollPeriodTableStatus, { label: string; className: s
};
const kindLabels = {
earning: "Начисление",
adjustment_credit: "Корректировка +",
adjustment_debit: "Корректировка −",
deduction: "Удержание",
payment: "Выплата",
earning: "Начислить сотруднику",
adjustment_credit: "Положительная корректировка",
adjustment_debit: "Отрицательная корректировка",
deduction: "Удержать",
payment: "Уже выплачено",
};
const kindHelpItems: Array<{
@ -708,7 +708,7 @@ function calculationHelps(pointPolicy: PayrollPeriodTable["calculation_rules"]["
},
deductions: {
title: "Удержано",
logic: "Сумма всех действующих строк с типом «Удержание» за выбранный период.",
logic: "Сумма всех действующих строк с типом «Удержать» за выбранный период.",
formula: "Удержано = Σ удержаний",
manual: "Нет. Итог меняется через строки удержаний.",
},
@ -720,8 +720,8 @@ function calculationHelps(pointPolicy: PayrollPeriodTable["calculation_rules"]["
},
payments: {
title: "Выплачено",
logic: "Деньги, которые уже фактически выдали сотруднику в этом периоде. Сюда попадают «Аванс», «Фактическая выплата» и другие операции с типом «Выплата». Начисления, премии, корректировки и удержания сюда не входят.",
formula: "Выплачено = Аванс + Фактическая выплата + Σ прочих операций типа «Выплата»",
logic: "Деньги, которые уже фактически выдали сотруднику в этом периоде. Сюда попадают «Аванс», «Фактическая выплата» и другие операции с типом «Уже выплачено». Начисления, премии, корректировки и удержания сюда не входят.",
formula: "Выплачено = Аванс + Фактическая выплата + Σ прочих операций типа «Уже выплачено»",
manual: "Сам итог не редактируется. Введите сумму в ячейке «Аванс» или «Фактическая выплата» — она уменьшит остаток к выплате.",
},
payable: {