Scope manual sync buttons to current view dates only
Sync rota and pull timesheets now operate on the 7-day view window rather than rolling default windows — reduces API calls and allows backdating. Cron job still uses rolling defaults via runRotaSync()/runTimesheetSync(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9bfd14a082
commit
ffefa1914f
9 changed files with 81 additions and 60 deletions
|
|
@ -5,25 +5,37 @@ function fmtDate(d) {
|
||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runRotaSync() {
|
function defaultRotaWindow() {
|
||||||
|
const today = new Date()
|
||||||
|
const from = new Date(today); from.setDate(from.getDate() - 7)
|
||||||
|
const to = new Date(today); to.setDate(to.getDate() + 28)
|
||||||
|
return { from: fmtDate(from), to: fmtDate(to) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultTimesheetWindow() {
|
||||||
|
const today = new Date()
|
||||||
|
const from = new Date(today); from.setDate(from.getDate() - 7)
|
||||||
|
return { from: fmtDate(from), to: fmtDate(today) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// from/to: 'YYYY-MM-DD' strings — defaults to rolling window if omitted
|
||||||
|
export async function runRotaSync(from, to) {
|
||||||
const deptIds = (await getConfig('workforce_departments', [])) || []
|
const deptIds = (await getConfig('workforce_departments', [])) || []
|
||||||
if (!deptIds.length) throw new Error('No HK departments selected — configure in Category Settings')
|
if (!deptIds.length) throw new Error('No HK departments selected — configure in Category Settings')
|
||||||
|
|
||||||
const today = new Date()
|
if (!from || !to) ({ from, to } = defaultRotaWindow())
|
||||||
const fromDate = new Date(today); fromDate.setDate(fromDate.getDate() - 7)
|
|
||||||
const toDate = new Date(today); toDate.setDate(toDate.getDate() + 28)
|
const fromDate = new Date(from + 'T00:00:00')
|
||||||
const from = fmtDate(fromDate)
|
const toDate = new Date(to + 'T00:00:00')
|
||||||
const to = fmtDate(toDate)
|
|
||||||
|
|
||||||
// API limit: max 7 days per request — split into weekly chunks
|
// API limit: max 7 days per request — split into weekly chunks
|
||||||
const chunks = []
|
const chunks = []
|
||||||
const cur = new Date(fromDate)
|
const cur = new Date(fromDate)
|
||||||
while (cur <= toDate) {
|
while (cur <= toDate) {
|
||||||
const chunkFrom = fmtDate(cur)
|
const chunkFrom = fmtDate(cur)
|
||||||
const chunkToDate = new Date(cur)
|
const chunkEnd = new Date(cur); chunkEnd.setDate(chunkEnd.getDate() + 6)
|
||||||
chunkToDate.setDate(chunkToDate.getDate() + 6)
|
if (chunkEnd > toDate) chunkEnd.setTime(toDate.getTime())
|
||||||
if (chunkToDate > toDate) chunkToDate.setTime(toDate.getTime())
|
chunks.push({ from: chunkFrom, to: fmtDate(chunkEnd) })
|
||||||
chunks.push({ from: chunkFrom, to: fmtDate(chunkToDate) })
|
|
||||||
cur.setDate(cur.getDate() + 7)
|
cur.setDate(cur.getDate() + 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,14 +74,15 @@ export async function runRotaSync() {
|
||||||
return { ok: true, from, to, dates_synced: dailyRows.length }
|
return { ok: true, from, to, dates_synced: dailyRows.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runTimesheetSync(days = 7) {
|
// from/to: 'YYYY-MM-DD' strings — defaults to last 7 days if omitted
|
||||||
|
export async function runTimesheetSync(from, to) {
|
||||||
const deptIds = (await getConfig('workforce_departments', [])) || []
|
const deptIds = (await getConfig('workforce_departments', [])) || []
|
||||||
if (!deptIds.length) throw new Error('No HK departments selected — configure in Category Settings')
|
if (!deptIds.length) throw new Error('No HK departments selected — configure in Category Settings')
|
||||||
|
|
||||||
const today = new Date()
|
if (!from || !to) ({ from, to } = defaultTimesheetWindow())
|
||||||
const fromDate = new Date(today); fromDate.setDate(fromDate.getDate() - days)
|
|
||||||
const from = fmtDate(fromDate)
|
const fromDate = new Date(from + 'T00:00:00')
|
||||||
const to = fmtDate(today)
|
const toDate = new Date(to + 'T00:00:00')
|
||||||
|
|
||||||
const staffList = await fetchTimesheetShifts(from, to, deptIds)
|
const staffList = await fetchTimesheetShifts(from, to, deptIds)
|
||||||
|
|
||||||
|
|
@ -83,7 +96,7 @@ export async function runTimesheetSync(days = 7) {
|
||||||
|
|
||||||
const dailyRows = []
|
const dailyRows = []
|
||||||
const day = new Date(fromDate)
|
const day = new Date(fromDate)
|
||||||
while (day <= today) {
|
while (day <= toDate) {
|
||||||
const d = fmtDate(day)
|
const d = fmtDate(day)
|
||||||
dailyRows.push({ date: d, staff: byDate[d] || [], source: 'timesheet' })
|
dailyRows.push({ date: d, staff: byDate[d] || [], source: 'timesheet' })
|
||||||
day.setDate(day.getDate() + 1)
|
day.setDate(day.getDate() + 1)
|
||||||
|
|
|
||||||
|
|
@ -23,19 +23,21 @@ export async function workforceRoutes(app) {
|
||||||
// ── POST /api/workforce/sync — rolling rota window: today-7 → today+28 ──────
|
// ── POST /api/workforce/sync — rolling rota window: today-7 → today+28 ──────
|
||||||
|
|
||||||
app.post('/api/workforce/sync', { preHandler: requireCap('planner') }, async (req, reply) => {
|
app.post('/api/workforce/sync', { preHandler: requireCap('planner') }, async (req, reply) => {
|
||||||
|
const { start, end } = req.query
|
||||||
try {
|
try {
|
||||||
return await runRotaSync()
|
return await runRotaSync(start, end)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.log.error({ err: err.message }, 'rota sync failed')
|
req.log.error({ err: err.message }, 'rota sync failed')
|
||||||
return reply.status(errStatus(err.message)).send({ error: err.message })
|
return reply.status(errStatus(err.message)).send({ error: err.message })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── POST /api/workforce/sync-timesheets — last 7 days of actual hours ────────
|
// ── POST /api/workforce/sync-timesheets?start=&end= ──────────────────────────
|
||||||
|
|
||||||
app.post('/api/workforce/sync-timesheets', { preHandler: requireCap('planner') }, async (req, reply) => {
|
app.post('/api/workforce/sync-timesheets', { preHandler: requireCap('planner') }, async (req, reply) => {
|
||||||
|
const { start, end } = req.query
|
||||||
try {
|
try {
|
||||||
return await runTimesheetSync()
|
return await runTimesheetSync(start, end)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
req.log.error({ err: err.message }, 'timesheet sync failed')
|
req.log.error({ err: err.message }, 'timesheet sync failed')
|
||||||
return reply.status(errStatus(err.message)).send({ error: err.message })
|
return reply.status(errStatus(err.message)).send({ error: err.message })
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
|
|
@ -5,7 +5,7 @@
|
||||||
<meta name="viewport" content="width=1280" />
|
<meta name="viewport" content="width=1280" />
|
||||||
<meta name="theme-color" content="#2d6a4f" />
|
<meta name="theme-color" content="#2d6a4f" />
|
||||||
<title>HK Planner</title>
|
<title>HK Planner</title>
|
||||||
<script type="module" crossorigin src="/hk-planner/assets/index-C9TVzZ7i.js"></script>
|
<script type="module" crossorigin src="/hk-planner/assets/index-Bp4tnr99.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/hk-planner/assets/index-B7_UXJgZ.css">
|
<link rel="stylesheet" crossorigin href="/hk-planner/assets/index-B7_UXJgZ.css">
|
||||||
<link rel="manifest" href="/hk-planner/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/hk-planner/registerSW.js"></script></head>
|
<link rel="manifest" href="/hk-planner/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/hk-planner/registerSW.js"></script></head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
2
frontend/dist/sw.js
vendored
2
frontend/dist/sw.js
vendored
|
|
@ -1 +1 @@
|
||||||
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(i[o])return;let t={};const c=e=>n(e,o),d={module:{uri:o},exports:t,require:c};i[o]=Promise.all(s.map(e=>d[e]||c(e))).then(e=>(r(...e),t))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"a2c395d8c225f1b3ea12388f15189bce"},{url:"index.html",revision:"3b1c49874f107c5a3d6697cd6a89320e"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-C9TVzZ7i.js",revision:null},{url:"assets/index-B7_UXJgZ.css",revision:null},{url:"manifest.webmanifest",revision:"c2510de876adb84db0c4b300b71216fa"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/hk-planner/index.html"),{denylist:[/\/api\//]}))});
|
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(i[t])return;let o={};const c=e=>n(e,t),d={module:{uri:t},exports:o,require:c};i[t]=Promise.all(s.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"a2c395d8c225f1b3ea12388f15189bce"},{url:"index.html",revision:"455924148ee524d7363268ccabc71ea8"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-Bp4tnr99.js",revision:null},{url:"assets/index-B7_UXJgZ.css",revision:null},{url:"manifest.webmanifest",revision:"c2510de876adb84db0c4b300b71216fa"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/hk-planner/index.html"),{denylist:[/\/api\//]}))});
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,11 @@ export function putWorkforceDepartments(dept_ids) {
|
||||||
body: JSON.stringify({ dept_ids }),
|
body: JSON.stringify({ dept_ids }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export function syncWorkforce() {
|
export function syncWorkforce(start, end) {
|
||||||
return request('/workforce/sync', { method: 'POST' });
|
return request(`/workforce/sync?start=${start}&end=${end}`, { method: 'POST' });
|
||||||
}
|
}
|
||||||
export function syncTimesheets() {
|
export function syncTimesheets(start, end) {
|
||||||
return request('/workforce/sync-timesheets', { method: 'POST' });
|
return request(`/workforce/sync-timesheets?start=${start}&end=${end}`, { method: 'POST' });
|
||||||
}
|
}
|
||||||
export function getWorkforceShifts(start, end) {
|
export function getWorkforceShifts(start, end) {
|
||||||
return request(`/workforce/shifts?start=${start}&end=${end}`);
|
return request(`/workforce/shifts?start=${start}&end=${end}`);
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,12 @@ export function putWorkforceDepartments(dept_ids: string[]): Promise<{ ok: boole
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncWorkforce(): Promise<{ ok: boolean; from: string; to: string; dates_synced: number }> {
|
export function syncWorkforce(start: string, end: string): Promise<{ ok: boolean; from: string; to: string; dates_synced: number }> {
|
||||||
return request('/workforce/sync', { method: 'POST' })
|
return request(`/workforce/sync?start=${start}&end=${end}`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syncTimesheets(): Promise<{ ok: boolean; from: string; to: string; dates_synced: number }> {
|
export function syncTimesheets(start: string, end: string): Promise<{ ok: boolean; from: string; to: string; dates_synced: number }> {
|
||||||
return request('/workforce/sync-timesheets', { method: 'POST' })
|
return request(`/workforce/sync-timesheets?start=${start}&end=${end}`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkforceShifts(start: string, end: string): Promise<Record<string, WfDayData>> {
|
export function getWorkforceShifts(start: string, end: string): Promise<Record<string, WfDayData>> {
|
||||||
|
|
|
||||||
|
|
@ -250,13 +250,15 @@ export function Planner() {
|
||||||
}
|
}
|
||||||
// ── Workforce sync ───────────────────────────────────────────────────────────
|
// ── Workforce sync ───────────────────────────────────────────────────────────
|
||||||
async function syncRota() {
|
async function syncRota() {
|
||||||
|
if (!bookings)
|
||||||
|
return;
|
||||||
|
const start = bookings.dates[0];
|
||||||
|
const end = bookings.dates[bookings.dates.length - 1];
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
try {
|
try {
|
||||||
await syncWorkforce();
|
await syncWorkforce(start, end);
|
||||||
if (bookings) {
|
const shifts = await getWorkforceShifts(start, end);
|
||||||
const shifts = await getWorkforceShifts(bookings.dates[0], bookings.dates[bookings.dates.length - 1]);
|
|
||||||
setWfShifts(shifts);
|
setWfShifts(shifts);
|
||||||
}
|
|
||||||
flash('Rota synced from Workforce');
|
flash('Rota synced from Workforce');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|
@ -267,13 +269,15 @@ export function Planner() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function syncTimesheetData() {
|
async function syncTimesheetData() {
|
||||||
|
if (!bookings)
|
||||||
|
return;
|
||||||
|
const start = bookings.dates[0];
|
||||||
|
const end = bookings.dates[bookings.dates.length - 1];
|
||||||
setTimesheetSyncing(true);
|
setTimesheetSyncing(true);
|
||||||
try {
|
try {
|
||||||
await syncTimesheets();
|
await syncTimesheets(start, end);
|
||||||
if (bookings) {
|
const shifts = await getWorkforceShifts(start, end);
|
||||||
const shifts = await getWorkforceShifts(bookings.dates[0], bookings.dates[bookings.dates.length - 1]);
|
|
||||||
setWfShifts(shifts);
|
setWfShifts(shifts);
|
||||||
}
|
|
||||||
flash('Timesheets pulled from Workforce');
|
flash('Timesheets pulled from Workforce');
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -298,13 +298,14 @@ export function Planner() {
|
||||||
// ── Workforce sync ───────────────────────────────────────────────────────────
|
// ── Workforce sync ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function syncRota() {
|
async function syncRota() {
|
||||||
|
if (!bookings) return
|
||||||
|
const start = bookings.dates[0]
|
||||||
|
const end = bookings.dates[bookings.dates.length - 1]
|
||||||
setSyncing(true)
|
setSyncing(true)
|
||||||
try {
|
try {
|
||||||
await syncWorkforce()
|
await syncWorkforce(start, end)
|
||||||
if (bookings) {
|
const shifts = await getWorkforceShifts(start, end)
|
||||||
const shifts = await getWorkforceShifts(bookings.dates[0], bookings.dates[bookings.dates.length - 1])
|
|
||||||
setWfShifts(shifts)
|
setWfShifts(shifts)
|
||||||
}
|
|
||||||
flash('Rota synced from Workforce')
|
flash('Rota synced from Workforce')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
flash(e instanceof Error ? e.message : 'Sync failed', true)
|
flash(e instanceof Error ? e.message : 'Sync failed', true)
|
||||||
|
|
@ -314,13 +315,14 @@ export function Planner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncTimesheetData() {
|
async function syncTimesheetData() {
|
||||||
|
if (!bookings) return
|
||||||
|
const start = bookings.dates[0]
|
||||||
|
const end = bookings.dates[bookings.dates.length - 1]
|
||||||
setTimesheetSyncing(true)
|
setTimesheetSyncing(true)
|
||||||
try {
|
try {
|
||||||
await syncTimesheets()
|
await syncTimesheets(start, end)
|
||||||
if (bookings) {
|
const shifts = await getWorkforceShifts(start, end)
|
||||||
const shifts = await getWorkforceShifts(bookings.dates[0], bookings.dates[bookings.dates.length - 1])
|
|
||||||
setWfShifts(shifts)
|
setWfShifts(shifts)
|
||||||
}
|
|
||||||
flash('Timesheets pulled from Workforce')
|
flash('Timesheets pulled from Workforce')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
flash(e instanceof Error ? e.message : 'Timesheet sync failed', true)
|
flash(e instanceof Error ? e.message : 'Timesheet sync failed', true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue