' +
+ '
' +
'
' +
escapeHtml(title) +
"
" +
+ '
' +
+ '
' +
+ ICON_NO_DUTY +
+ "" +
'
' +
escapeHtml(noDuty) +
"
" +
+ "
" +
'
" +
@@ -78,15 +101,22 @@ export function renderCurrentDutyContent(duty, lang) {
" " +
endTime;
const shiftLabel = t(lang, "current_duty.shift");
+ const { hours: remHours, minutes: remMinutes } = getRemainingTime(duty.end_at);
+ const remainingStr = t(lang, "current_duty.remaining", {
+ hours: String(remHours),
+ minutes: String(remMinutes)
+ });
const contactHtml = buildContactLinksHtml(lang, duty.phone, duty.username, {
classPrefix: "current-duty-contact",
showLabels: true,
- separator: " "
+ separator: " ",
+ layout: "block"
});
return (
'
' +
'
' +
+ ' ' +
escapeHtml(title) +
"
" +
'
' +
@@ -97,6 +127,9 @@ export function renderCurrentDutyContent(duty, lang) {
": " +
escapeHtml(shiftStr) +
"
" +
+ '
' +
+ escapeHtml(remainingStr) +
+ "
" +
contactHtml +
'
";
});
+ describe("getRemainingTime", () => {
+ it("returns hours and minutes until end from now", () => {
+ const endAt = "2025-03-02T17:30:00.000Z";
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2025-03-02T12:00:00.000Z"));
+ const { hours, minutes } = getRemainingTime(endAt);
+ vi.useRealTimers();
+ expect(hours).toBe(5);
+ expect(minutes).toBe(30);
+ });
+
+ it("returns 0 when end is in the past", () => {
+ const endAt = "2025-03-02T09:00:00.000Z";
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2025-03-02T12:00:00.000Z"));
+ const { hours, minutes } = getRemainingTime(endAt);
+ vi.useRealTimers();
+ expect(hours).toBe(0);
+ expect(minutes).toBe(0);
+ });
+ });
+
describe("findCurrentDuty", () => {
it("returns duty when now is between start_at and end_at", () => {
const now = new Date();
@@ -67,13 +90,18 @@ describe("currentDuty", () => {
it("renders no-duty message and back button when duty is null", () => {
const html = renderCurrentDutyContent(null, "en");
expect(html).toContain("current-duty-card");
+ expect(html).toContain("current-duty-card--no-duty");
expect(html).toContain("Current Duty");
+ expect(html).toContain("current-duty-no-duty-wrap");
+ expect(html).toContain("current-duty-no-duty-icon");
+ expect(html).toContain("current-duty-no-duty");
expect(html).toContain("No one is on duty right now");
expect(html).toContain("Back to calendar");
expect(html).toContain('data-action="back"');
+ expect(html).not.toContain("current-duty-live-dot");
});
- it("renders duty card with name, shift, and back button when duty has no contacts", () => {
+ it("renders duty card with name, shift, remaining time, and back button when duty has no contacts", () => {
const duty = {
event_type: "duty",
full_name: "Иванов Иван",
@@ -81,9 +109,12 @@ describe("currentDuty", () => {
end_at: "2025-03-03T06:00:00.000Z"
};
const html = renderCurrentDutyContent(duty, "ru");
+ expect(html).toContain("current-duty-live-dot");
expect(html).toContain("Текущее дежурство");
expect(html).toContain("Иванов Иван");
expect(html).toContain("Смена");
+ expect(html).toContain("current-duty-remaining");
+ expect(html).toMatch(/Осталось:\s*\d+ч\s*\d+мин/);
expect(html).toContain("Назад к календарю");
expect(html).toContain('data-action="back"');
});
@@ -99,7 +130,11 @@ describe("currentDuty", () => {
};
const html = renderCurrentDutyContent(duty, "en");
expect(html).toContain("Alice");
+ expect(html).toContain("current-duty-remaining");
+ expect(html).toMatch(/Remaining:\s*\d+h\s*\d+min/);
expect(html).toContain("current-duty-contact-row");
+ expect(html).toContain("current-duty-contact-row--blocks");
+ expect(html).toContain("current-duty-contact-block");
expect(html).toContain('href="tel:');
expect(html).toContain("+7 900 123-45-67");
expect(html).toContain("https://t.me/");
diff --git a/webapp/js/i18n.js b/webapp/js/i18n.js
index 90abbbb..7f245ed 100644
--- a/webapp/js/i18n.js
+++ b/webapp/js/i18n.js
@@ -58,6 +58,7 @@ export const MESSAGES = {
"current_duty.title": "Current Duty",
"current_duty.no_duty": "No one is on duty right now",
"current_duty.shift": "Shift",
+ "current_duty.remaining": "Remaining: {hours}h {minutes}min",
"current_duty.back": "Back to calendar"
},
ru: {
@@ -112,6 +113,7 @@ export const MESSAGES = {
"current_duty.title": "Текущее дежурство",
"current_duty.no_duty": "Сейчас никто не дежурит",
"current_duty.shift": "Смена",
+ "current_duty.remaining": "Осталось: {hours}ч {minutes}мин",
"current_duty.back": "Назад к календарю"
}
};