Ports two modules from the friend's Google Apps Script build (kept as reference in gscript/) onto the TypeScript stack, rewritten cleanly against this app's own data model rather than copied 1:1: - Shift planning: weekly template (Sun-Thu evening, Fri/Sat two slots), lazy idempotent generation per period (no cron needed), employee signup/cancel with collision + capacity checks, admin calendar view with slot editing and manual assignment. - Month closure + payroll: employee confirms the month (blocked while a shift is still open), admin locks and finalizes pay (base hours * rate + tips/bonus/other), reopen to undo a premature lock, mark paid. Pay rates are versioned by date, defaulting the first-ever rate to apply retroactively to the employee's whole history. - A shift left open more than 12h (forgotten clock-out) is auto-closed at clock_in + 12h, checked lazily on read instead of a background job. - Full dark, sharp-edged modern restyle (theme.css replaces tui.css) with an amber accent, keeping every existing class name so no component logic needed to change. Backend test coverage (jest) for all three workflows: shift planning, closure/payroll, and the forgotten-clock-out auto-close.
1576 lines
20 KiB
JavaScript
1576 lines
20 KiB
JavaScript
/* ============================================================
|
||
EATME PORTÁL – NEWS SERVICE
|
||
|
||
Role:
|
||
- EMPLOYEE: čte ALL + EMPLOYEE
|
||
- MANAGER: čte ALL + MANAGER, může spravovat aktuality
|
||
- ACCOUNTANT: čte ALL + ACCOUNTANT
|
||
- ADMIN: čte všechny aktuality, může je spravovat
|
||
|
||
Využívá existující:
|
||
- NEWS
|
||
- NEWS_READS
|
||
============================================================ */
|
||
|
||
|
||
/* ============================================================
|
||
SETUP
|
||
============================================================ */
|
||
|
||
function setupNews() {
|
||
|
||
ensureNewsSheet_(
|
||
CFG.SHEETS.NEWS,
|
||
HEADERS.NEWS
|
||
);
|
||
|
||
|
||
ensureNewsSheet_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
HEADERS.NEWS_READS
|
||
);
|
||
|
||
|
||
Logger.log(
|
||
'News setup OK'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
AKTUALITY PRO PŘIHLÁŠENÉHO UŽIVATELE
|
||
============================================================ */
|
||
|
||
function getMyNews(token) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token
|
||
);
|
||
|
||
|
||
const role =
|
||
String(
|
||
user.role ||
|
||
''
|
||
).toUpperCase();
|
||
|
||
|
||
const now =
|
||
new Date();
|
||
|
||
|
||
const newsRows =
|
||
rows_(
|
||
CFG.SHEETS.NEWS
|
||
);
|
||
|
||
|
||
const readRows =
|
||
rows_(
|
||
CFG.SHEETS.NEWS_READS
|
||
)
|
||
.filter(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.user_id
|
||
) ===
|
||
String(
|
||
user.user_id
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const readsByNews =
|
||
{};
|
||
|
||
|
||
readRows.forEach(
|
||
function(row) {
|
||
|
||
readsByNews[
|
||
String(
|
||
row.news_id
|
||
)
|
||
] =
|
||
row;
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return newsRows
|
||
.filter(
|
||
function(row) {
|
||
|
||
if (
|
||
!newsBool_(
|
||
row.active
|
||
)
|
||
) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
const target =
|
||
String(
|
||
row.target_role ||
|
||
'ALL'
|
||
)
|
||
.trim()
|
||
.toUpperCase();
|
||
|
||
|
||
/*
|
||
* ADMIN vidí všechno.
|
||
* Ostatní role pouze ALL + svou roli.
|
||
*/
|
||
|
||
if (
|
||
role !==
|
||
'ADMIN' &&
|
||
target !==
|
||
'ALL' &&
|
||
target !==
|
||
role
|
||
) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
const from =
|
||
newsDate_(
|
||
row.published_from
|
||
);
|
||
|
||
|
||
const to =
|
||
newsDate_(
|
||
row.published_to
|
||
);
|
||
|
||
|
||
if (
|
||
from &&
|
||
from.getTime() >
|
||
now.getTime()
|
||
) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
to &&
|
||
to.getTime() <
|
||
now.getTime()
|
||
) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
return true;
|
||
|
||
}
|
||
)
|
||
.map(
|
||
function(row) {
|
||
|
||
const read =
|
||
readsByNews[
|
||
String(
|
||
row.news_id
|
||
)
|
||
] ||
|
||
null;
|
||
|
||
|
||
return {
|
||
|
||
news_id:
|
||
String(
|
||
row.news_id ||
|
||
''
|
||
),
|
||
|
||
title:
|
||
String(
|
||
row.title ||
|
||
''
|
||
),
|
||
|
||
content:
|
||
String(
|
||
row.content ||
|
||
''
|
||
),
|
||
|
||
category:
|
||
String(
|
||
row.category ||
|
||
''
|
||
),
|
||
|
||
target_role:
|
||
String(
|
||
row.target_role ||
|
||
'ALL'
|
||
),
|
||
|
||
published_from:
|
||
row.published_from ||
|
||
'',
|
||
|
||
published_to:
|
||
row.published_to ||
|
||
'',
|
||
|
||
require_confirmation:
|
||
newsBool_(
|
||
row.require_confirmation
|
||
),
|
||
|
||
read_at:
|
||
read
|
||
? read.read_at ||
|
||
''
|
||
: '',
|
||
|
||
confirmed_at:
|
||
read
|
||
? read.confirmed_at ||
|
||
''
|
||
: '',
|
||
|
||
created_at:
|
||
row.created_at ||
|
||
''
|
||
|
||
};
|
||
|
||
}
|
||
)
|
||
.sort(
|
||
function(a,b) {
|
||
|
||
const aImportant =
|
||
String(
|
||
a.category
|
||
).toUpperCase() ===
|
||
'DŮLEŽITÉ'
|
||
? 1
|
||
: 0;
|
||
|
||
|
||
const bImportant =
|
||
String(
|
||
b.category
|
||
).toUpperCase() ===
|
||
'DŮLEŽITÉ'
|
||
? 1
|
||
: 0;
|
||
|
||
|
||
if (
|
||
aImportant !==
|
||
bImportant
|
||
) {
|
||
|
||
return (
|
||
bImportant -
|
||
aImportant
|
||
);
|
||
|
||
}
|
||
|
||
|
||
return (
|
||
newsTime_(
|
||
b.created_at
|
||
) -
|
||
newsTime_(
|
||
a.created_at
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
PŘEČTENO / POTVRZENO
|
||
============================================================ */
|
||
|
||
function markNewsRead(
|
||
token,
|
||
newsId,
|
||
confirm
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token
|
||
);
|
||
|
||
|
||
const news =
|
||
findOne_(
|
||
CFG.SHEETS.NEWS,
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
String(
|
||
newsId
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!news
|
||
) {
|
||
|
||
throw new Error(
|
||
'Aktualita nebyla nalezena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const existing =
|
||
findOne_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
String(
|
||
newsId
|
||
) &&
|
||
|
||
String(
|
||
row.user_id
|
||
) ===
|
||
String(
|
||
user.user_id
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const stamp =
|
||
now_();
|
||
|
||
|
||
if (
|
||
existing
|
||
) {
|
||
|
||
const update =
|
||
{};
|
||
|
||
|
||
if (
|
||
!existing.read_at
|
||
) {
|
||
|
||
update.read_at =
|
||
stamp;
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
confirm
|
||
) {
|
||
|
||
update.confirmed_at =
|
||
stamp;
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
Object.keys(
|
||
update
|
||
).length
|
||
) {
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
'news_read_id',
|
||
existing.news_read_id,
|
||
update
|
||
);
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
append_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
{
|
||
|
||
news_read_id:
|
||
uuid_(
|
||
'NR'
|
||
),
|
||
|
||
news_id:
|
||
newsId,
|
||
|
||
user_id:
|
||
user.user_id,
|
||
|
||
read_at:
|
||
stamp,
|
||
|
||
confirmed_at:
|
||
confirm
|
||
? stamp
|
||
: ''
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
|
||
return {
|
||
ok:true
|
||
};
|
||
|
||
}
|
||
|
||
|
||
/* ============================================================
|
||
HROMADNÉ OZNAČENÍ AKTUALIT JAKO PŘEČTENÉ
|
||
|
||
Jeden request pro všechny zobrazené aktuality.
|
||
============================================================ */
|
||
|
||
function markNewsListRead(
|
||
token,
|
||
newsIds
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token
|
||
);
|
||
|
||
|
||
newsIds =
|
||
Array.isArray(
|
||
newsIds
|
||
)
|
||
? newsIds
|
||
: [];
|
||
|
||
|
||
const uniqueIds =
|
||
Array.from(
|
||
new Set(
|
||
newsIds
|
||
.map(
|
||
function(id) {
|
||
|
||
return String(
|
||
id ||
|
||
''
|
||
).trim();
|
||
|
||
}
|
||
)
|
||
.filter(
|
||
Boolean
|
||
)
|
||
)
|
||
);
|
||
|
||
|
||
if (
|
||
!uniqueIds.length
|
||
) {
|
||
|
||
return {
|
||
ok:true
|
||
};
|
||
|
||
}
|
||
|
||
|
||
const existingRows =
|
||
rows_(
|
||
CFG.SHEETS.NEWS_READS
|
||
)
|
||
.filter(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.user_id
|
||
) ===
|
||
String(
|
||
user.user_id
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const existingByNews =
|
||
{};
|
||
|
||
|
||
existingRows.forEach(
|
||
function(row) {
|
||
|
||
existingByNews[
|
||
String(
|
||
row.news_id
|
||
)
|
||
] =
|
||
row;
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const stamp =
|
||
now_();
|
||
|
||
|
||
uniqueIds.forEach(
|
||
function(newsId) {
|
||
|
||
const existing =
|
||
existingByNews[
|
||
newsId
|
||
];
|
||
|
||
|
||
if (
|
||
existing
|
||
) {
|
||
|
||
if (
|
||
!existing.read_at
|
||
) {
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
'news_read_id',
|
||
existing.news_read_id,
|
||
{
|
||
read_at:
|
||
stamp
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
append_(
|
||
CFG.SHEETS.NEWS_READS,
|
||
{
|
||
|
||
news_read_id:
|
||
uuid_(
|
||
'NR'
|
||
),
|
||
|
||
news_id:
|
||
newsId,
|
||
|
||
user_id:
|
||
user.user_id,
|
||
|
||
read_at:
|
||
stamp,
|
||
|
||
confirmed_at:
|
||
''
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return {
|
||
ok:true
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
/* ============================================================
|
||
ADMIN / MANAGER – SEZNAM AKTUALIT
|
||
============================================================ */
|
||
|
||
function getAdminNews(token) {
|
||
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const newsRows =
|
||
rows_(
|
||
CFG.SHEETS.NEWS
|
||
);
|
||
|
||
|
||
const readRows =
|
||
rows_(
|
||
CFG.SHEETS.NEWS_READS
|
||
);
|
||
|
||
|
||
const users =
|
||
rows_(
|
||
CFG.SHEETS.USERS
|
||
)
|
||
.filter(
|
||
function(user) {
|
||
|
||
return newsBool_(
|
||
user.active
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return newsRows
|
||
.map(
|
||
function(row) {
|
||
|
||
const newsId =
|
||
String(
|
||
row.news_id ||
|
||
''
|
||
);
|
||
|
||
|
||
const target =
|
||
String(
|
||
row.target_role ||
|
||
'ALL'
|
||
).toUpperCase();
|
||
|
||
|
||
const audience =
|
||
users.filter(
|
||
function(user) {
|
||
|
||
return (
|
||
target ===
|
||
'ALL' ||
|
||
target ===
|
||
String(
|
||
user.role ||
|
||
''
|
||
).toUpperCase()
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const reads =
|
||
readRows.filter(
|
||
function(read) {
|
||
|
||
return (
|
||
String(
|
||
read.news_id
|
||
) ===
|
||
newsId
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return {
|
||
|
||
news_id:
|
||
newsId,
|
||
|
||
title:
|
||
String(
|
||
row.title ||
|
||
''
|
||
),
|
||
|
||
content:
|
||
String(
|
||
row.content ||
|
||
''
|
||
),
|
||
|
||
category:
|
||
String(
|
||
row.category ||
|
||
''
|
||
),
|
||
|
||
target_role:
|
||
String(
|
||
row.target_role ||
|
||
'ALL'
|
||
),
|
||
|
||
published_from:
|
||
row.published_from ||
|
||
'',
|
||
|
||
published_to:
|
||
row.published_to ||
|
||
'',
|
||
|
||
require_confirmation:
|
||
newsBool_(
|
||
row.require_confirmation
|
||
),
|
||
|
||
active:
|
||
newsBool_(
|
||
row.active
|
||
),
|
||
|
||
created_at:
|
||
row.created_at ||
|
||
'',
|
||
|
||
audience_count:
|
||
audience.length,
|
||
|
||
read_count:
|
||
reads.filter(
|
||
function(read) {
|
||
|
||
return !!read.read_at;
|
||
|
||
}
|
||
).length,
|
||
|
||
confirmed_count:
|
||
reads.filter(
|
||
function(read) {
|
||
|
||
return !!read.confirmed_at;
|
||
|
||
}
|
||
).length
|
||
|
||
};
|
||
|
||
}
|
||
)
|
||
.sort(
|
||
function(a,b) {
|
||
|
||
return (
|
||
newsTime_(
|
||
b.created_at
|
||
) -
|
||
newsTime_(
|
||
a.created_at
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
ADMIN / MANAGER – ULOŽIT AKTUALITU
|
||
============================================================ */
|
||
|
||
function saveAdminNews(
|
||
token,
|
||
payload
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
payload =
|
||
payload ||
|
||
{};
|
||
|
||
|
||
const title =
|
||
String(
|
||
payload.title ||
|
||
''
|
||
).trim();
|
||
|
||
|
||
const content =
|
||
String(
|
||
payload.content ||
|
||
''
|
||
).trim();
|
||
|
||
|
||
if (
|
||
!title
|
||
) {
|
||
|
||
throw new Error(
|
||
'Vyplň nadpis aktuality.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
!content
|
||
) {
|
||
|
||
throw new Error(
|
||
'Vyplň text aktuality.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const allowedTargets =
|
||
[
|
||
'ALL',
|
||
'EMPLOYEE',
|
||
'MANAGER',
|
||
'ACCOUNTANT',
|
||
'ADMIN'
|
||
];
|
||
|
||
|
||
const targetRole =
|
||
String(
|
||
payload.target_role ||
|
||
'ALL'
|
||
).toUpperCase();
|
||
|
||
|
||
if (
|
||
!allowedTargets.includes(
|
||
targetRole
|
||
)
|
||
) {
|
||
|
||
throw new Error(
|
||
'Neplatná cílová role.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const newsId =
|
||
String(
|
||
payload.news_id ||
|
||
''
|
||
).trim();
|
||
|
||
|
||
const data =
|
||
{
|
||
|
||
title:
|
||
title,
|
||
|
||
content:
|
||
content,
|
||
|
||
category:
|
||
String(
|
||
payload.category ||
|
||
'Provoz'
|
||
),
|
||
|
||
company_id:
|
||
String(
|
||
payload.company_id ||
|
||
''
|
||
),
|
||
|
||
location_id:
|
||
String(
|
||
payload.location_id ||
|
||
''
|
||
),
|
||
|
||
target_role:
|
||
targetRole,
|
||
|
||
published_from:
|
||
payload.published_from ||
|
||
now_(),
|
||
|
||
published_to:
|
||
payload.published_to ||
|
||
'',
|
||
|
||
require_confirmation:
|
||
!!payload.require_confirmation,
|
||
|
||
active:
|
||
payload.active !==
|
||
false
|
||
|
||
};
|
||
|
||
|
||
if (
|
||
newsId
|
||
) {
|
||
|
||
const old =
|
||
findOne_(
|
||
CFG.SHEETS.NEWS,
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
newsId
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!old
|
||
) {
|
||
|
||
throw new Error(
|
||
'Aktualita nebyla nalezena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.NEWS,
|
||
'news_id',
|
||
newsId,
|
||
data
|
||
);
|
||
|
||
|
||
audit_(
|
||
user.user_id,
|
||
'NEWS_UPDATED',
|
||
'NEWS',
|
||
newsId,
|
||
old,
|
||
data
|
||
);
|
||
|
||
|
||
return {
|
||
ok:true,
|
||
news_id:newsId
|
||
};
|
||
|
||
}
|
||
|
||
|
||
const newId =
|
||
uuid_(
|
||
'NEWS'
|
||
);
|
||
|
||
|
||
append_(
|
||
CFG.SHEETS.NEWS,
|
||
{
|
||
|
||
news_id:
|
||
newId,
|
||
|
||
title:
|
||
data.title,
|
||
|
||
content:
|
||
data.content,
|
||
|
||
category:
|
||
data.category,
|
||
|
||
company_id:
|
||
data.company_id,
|
||
|
||
location_id:
|
||
data.location_id,
|
||
|
||
target_role:
|
||
data.target_role,
|
||
|
||
published_from:
|
||
data.published_from,
|
||
|
||
published_to:
|
||
data.published_to,
|
||
|
||
require_confirmation:
|
||
data.require_confirmation,
|
||
|
||
active:
|
||
data.active,
|
||
|
||
created_by:
|
||
user.user_id,
|
||
|
||
created_at:
|
||
now_()
|
||
|
||
}
|
||
);
|
||
|
||
|
||
audit_(
|
||
user.user_id,
|
||
'NEWS_CREATED',
|
||
'NEWS',
|
||
newId,
|
||
'',
|
||
data
|
||
);
|
||
|
||
|
||
return {
|
||
ok:true,
|
||
news_id:newId
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
SKRÝT / ZOBRAZIT
|
||
============================================================ */
|
||
|
||
function setAdminNewsActive(
|
||
token,
|
||
newsId,
|
||
active
|
||
) {
|
||
|
||
const user =
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const news =
|
||
findOne_(
|
||
CFG.SHEETS.NEWS,
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
String(
|
||
newsId
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!news
|
||
) {
|
||
|
||
throw new Error(
|
||
'Aktualita nebyla nalezena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
updateBy_(
|
||
CFG.SHEETS.NEWS,
|
||
'news_id',
|
||
newsId,
|
||
{
|
||
active:
|
||
!!active
|
||
}
|
||
);
|
||
|
||
|
||
audit_(
|
||
user.user_id,
|
||
active
|
||
? 'NEWS_SHOWN'
|
||
: 'NEWS_HIDDEN',
|
||
'NEWS',
|
||
newsId,
|
||
news.active,
|
||
!!active
|
||
);
|
||
|
||
|
||
return {
|
||
ok:true
|
||
};
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
ADMIN – KDO PŘEČETL / POTVRDIL
|
||
============================================================ */
|
||
|
||
function getAdminNewsReaders(
|
||
token,
|
||
newsId
|
||
) {
|
||
|
||
requireUser_(
|
||
token,
|
||
[
|
||
'ADMIN',
|
||
'MANAGER'
|
||
]
|
||
);
|
||
|
||
|
||
const news =
|
||
findOne_(
|
||
CFG.SHEETS.NEWS,
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
String(
|
||
newsId
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
if (
|
||
!news
|
||
) {
|
||
|
||
throw new Error(
|
||
'Aktualita nebyla nalezena.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
const target =
|
||
String(
|
||
news.target_role ||
|
||
'ALL'
|
||
).toUpperCase();
|
||
|
||
|
||
const users =
|
||
rows_(
|
||
CFG.SHEETS.USERS
|
||
)
|
||
.filter(
|
||
function(user) {
|
||
|
||
if (
|
||
!newsBool_(
|
||
user.active
|
||
)
|
||
) {
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
|
||
return (
|
||
target ===
|
||
'ALL' ||
|
||
target ===
|
||
String(
|
||
user.role ||
|
||
''
|
||
).toUpperCase()
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
const employees =
|
||
rows_(
|
||
CFG.SHEETS.EMPLOYEES
|
||
);
|
||
|
||
|
||
const reads =
|
||
rows_(
|
||
CFG.SHEETS.NEWS_READS
|
||
)
|
||
.filter(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.news_id
|
||
) ===
|
||
String(
|
||
newsId
|
||
)
|
||
);
|
||
|
||
}
|
||
);
|
||
|
||
|
||
return users
|
||
.map(
|
||
function(user) {
|
||
|
||
const read =
|
||
reads.find(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.user_id
|
||
) ===
|
||
String(
|
||
user.user_id
|
||
)
|
||
);
|
||
|
||
}
|
||
) ||
|
||
null;
|
||
|
||
|
||
const employee =
|
||
employees.find(
|
||
function(row) {
|
||
|
||
return (
|
||
String(
|
||
row.employee_id
|
||
) ===
|
||
String(
|
||
user.employee_id ||
|
||
''
|
||
)
|
||
);
|
||
|
||
}
|
||
) ||
|
||
null;
|
||
|
||
|
||
return {
|
||
|
||
user_id:
|
||
String(
|
||
user.user_id ||
|
||
''
|
||
),
|
||
|
||
name:
|
||
employee
|
||
? (
|
||
String(
|
||
employee.first_name ||
|
||
''
|
||
) +
|
||
' ' +
|
||
String(
|
||
employee.last_name ||
|
||
''
|
||
)
|
||
).trim()
|
||
: String(
|
||
user.email ||
|
||
''
|
||
),
|
||
|
||
email:
|
||
String(
|
||
user.email ||
|
||
''
|
||
),
|
||
|
||
role:
|
||
String(
|
||
user.role ||
|
||
''
|
||
),
|
||
|
||
read_at:
|
||
read
|
||
? read.read_at ||
|
||
''
|
||
: '',
|
||
|
||
confirmed_at:
|
||
read
|
||
? read.confirmed_at ||
|
||
''
|
||
: ''
|
||
|
||
};
|
||
|
||
}
|
||
);
|
||
|
||
}
|
||
|
||
|
||
|
||
/* ============================================================
|
||
HELPERS
|
||
============================================================ */
|
||
|
||
function newsBool_(value) {
|
||
|
||
return (
|
||
value ===
|
||
true ||
|
||
String(
|
||
value
|
||
).toUpperCase() ===
|
||
'TRUE' ||
|
||
String(
|
||
value
|
||
) ===
|
||
'1'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
function newsDate_(value) {
|
||
|
||
if (
|
||
!value
|
||
) {
|
||
|
||
return null;
|
||
|
||
}
|
||
|
||
|
||
const date =
|
||
value instanceof Date
|
||
? value
|
||
: new Date(
|
||
value
|
||
);
|
||
|
||
|
||
return isNaN(
|
||
date.getTime()
|
||
)
|
||
? null
|
||
: date;
|
||
|
||
}
|
||
|
||
|
||
function newsTime_(value) {
|
||
|
||
const date =
|
||
newsDate_(
|
||
value
|
||
);
|
||
|
||
|
||
return date
|
||
? date.getTime()
|
||
: 0;
|
||
|
||
}
|
||
|
||
|
||
function ensureNewsSheet_(
|
||
sheetName,
|
||
headers
|
||
) {
|
||
|
||
try {
|
||
|
||
const existing =
|
||
sh_(
|
||
sheetName
|
||
);
|
||
|
||
|
||
if (
|
||
existing
|
||
) {
|
||
|
||
return existing;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
catch(error) {
|
||
|
||
/*
|
||
* List ještě neexistuje.
|
||
*/
|
||
|
||
}
|
||
|
||
|
||
let spreadsheet =
|
||
null;
|
||
|
||
|
||
try {
|
||
|
||
spreadsheet =
|
||
sh_(
|
||
CFG.SHEETS.USERS
|
||
).getParent();
|
||
|
||
}
|
||
|
||
catch(error) {
|
||
|
||
/*
|
||
* Fallback níže.
|
||
*/
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
!spreadsheet
|
||
) {
|
||
|
||
spreadsheet =
|
||
SpreadsheetApp
|
||
.getActiveSpreadsheet();
|
||
|
||
}
|
||
|
||
|
||
if (
|
||
!spreadsheet
|
||
) {
|
||
|
||
throw new Error(
|
||
'Nepodařilo se otevřít databázový spreadsheet.'
|
||
);
|
||
|
||
}
|
||
|
||
|
||
let sheet =
|
||
spreadsheet.getSheetByName(
|
||
sheetName
|
||
);
|
||
|
||
|
||
if (
|
||
!sheet
|
||
) {
|
||
|
||
sheet =
|
||
spreadsheet.insertSheet(
|
||
sheetName
|
||
);
|
||
|
||
|
||
sheet
|
||
.getRange(
|
||
1,
|
||
1,
|
||
1,
|
||
headers.length
|
||
)
|
||
.setValues(
|
||
[
|
||
headers
|
||
]
|
||
);
|
||
|
||
|
||
sheet.setFrozenRows(
|
||
1
|
||
);
|
||
|
||
}
|
||
|
||
|
||
return sheet;
|
||
|
||
}
|