Add shift planning, month closure + payroll, and a dark modern redesign
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.
This commit is contained in:
55
gscript/NewsService.js
Normal file
55
gscript/NewsService.js
Normal file
@@ -0,0 +1,55 @@
|
||||
function getNewsForUser_(user, emp) {
|
||||
const now = now_();
|
||||
const memberships = rows_(CFG.SHEETS.USER_COMPANIES)
|
||||
.filter(x => String(x.user_id) === String(user.user_id))
|
||||
.map(x => String(x.company_id));
|
||||
|
||||
return rows_(CFG.SHEETS.NEWS)
|
||||
.filter(n => n.active === true || String(n.active).toUpperCase() === 'TRUE')
|
||||
.filter(n => !n.published_from || new Date(n.published_from) <= now)
|
||||
.filter(n => !n.published_to || new Date(n.published_to) >= now)
|
||||
.filter(n => !n.target_role || String(n.target_role) === 'ALL' || String(n.target_role) === String(user.role))
|
||||
.filter(n => !n.company_id || memberships.includes(String(n.company_id)) || (emp && String(emp.company_id) === String(n.company_id)))
|
||||
.filter(n => !n.location_id || (emp && String(emp.location_id) === String(n.location_id)))
|
||||
.sort((a,b)=>new Date(b.created_at)-new Date(a.created_at))
|
||||
.map(n => ({
|
||||
news_id:n.news_id,title:n.title,content:n.content,category:n.category,
|
||||
require_confirmation:n.require_confirmation,created_at:n.created_at
|
||||
}));
|
||||
}
|
||||
|
||||
function publishNews(token, data) {
|
||||
const user = requireUser_(token,['ADMIN','MANAGER']);
|
||||
const n = {
|
||||
news_id:uuid_('NEWS'),
|
||||
title:String(data.title || '').trim(),
|
||||
content:String(data.content || '').trim(),
|
||||
category:String(data.category || 'INFO'),
|
||||
company_id:String(data.company_id || ''),
|
||||
location_id:String(data.location_id || ''),
|
||||
target_role:String(data.target_role || 'ALL'),
|
||||
published_from:data.published_from || now_(),
|
||||
published_to:data.published_to || '',
|
||||
require_confirmation:!!data.require_confirmation,
|
||||
active:true,
|
||||
created_by:user.user_id,
|
||||
created_at:now_()
|
||||
};
|
||||
if (!n.title) throw new Error('Titulek je povinný.');
|
||||
append_(CFG.SHEETS.NEWS,n);
|
||||
audit_(user.user_id,'NEWS_PUBLISHED','NEWS',n.news_id,'',n);
|
||||
return {ok:true,news_id:n.news_id};
|
||||
}
|
||||
|
||||
function confirmNews(token, newsId) {
|
||||
const user = requireUser_(token);
|
||||
let r = findOne_(CFG.SHEETS.NEWS_READS, x => String(x.news_id)===String(newsId) && String(x.user_id)===String(user.user_id));
|
||||
if (r) {
|
||||
updateBy_(CFG.SHEETS.NEWS_READS,'news_read_id',r.news_read_id,{read_at:r.read_at || now_(),confirmed_at:now_()});
|
||||
} else {
|
||||
append_(CFG.SHEETS.NEWS_READS,{
|
||||
news_read_id:uuid_('NREAD'),news_id:newsId,user_id:user.user_id,read_at:now_(),confirmed_at:now_()
|
||||
});
|
||||
}
|
||||
return {ok:true};
|
||||
}
|
||||
Reference in New Issue
Block a user