Fix today gap + future date visual drop after pull timesheets

runRotaSync: only protect today's timesheet row when it has real
clocked-out hours (hours > 0), so stale ?/0h rows and empty rows
are healed by the next rota sync without needing manual intervention.

runTimesheetSync: revert to SKIP for today when no completed shifts —
writing an empty row was overwriting the rota schedule with nothing.

Frontend: fetch wfShifts for the full week after a timesheet pull
(not just up to today), so future rota rows aren't dropped from state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-23 12:37:43 +00:00
parent cbc9db67bc
commit cae0f48d79
5 changed files with 24 additions and 17 deletions

View file

@ -56,12 +56,17 @@ export async function runRotaSync(from, to) {
}
}
// Skip past dates already replaced with timesheet actuals (future timesheet rows are allowed through)
// Skip dates already replaced with confirmed timesheet actuals
const today = fmtDate(new Date())
const existing = await getWorkforceShifts(from, to)
const timesheetDates = new Set(
Object.entries(existing)
.filter(([date, d]) => d.source === 'timesheet' && date <= today)
.filter(([date, d]) => {
if (d.source !== 'timesheet') return false
if (date > today) return false // never protect future dates
if (date === today) return d.staff?.some(s => s.hours > 0) // only protect today if real clocked-out hours exist
return true // always protect past timesheet dates
})
.map(([date]) => date)
)
@ -107,12 +112,13 @@ export async function runTimesheetSync(from, to) {
const d = fmtDate(day)
const staffForDate = byDate[d] || []
if (d === today && staffForDate.length === 0) {
// Nobody has clocked out yet — write a workforce-sourced row so this clears any
// stale timesheet entry and rota sync can overwrite with schedule data again
dailyRows.push({ date: d, staff: [], source: 'workforce' })
} else {
dailyRows.push({ date: d, staff: staffForDate, source: 'timesheet' })
// Nobody has clocked out yet — leave the existing DB row intact so rota schedule
// data stays visible. Rota sync will heal any stale timesheet row for today
// because runRotaSync only protects today when it has real clocked-out hours.
day.setDate(day.getDate() + 1)
continue
}
dailyRows.push({ date: d, staff: staffForDate, source: 'timesheet' })
day.setDate(day.getDate() + 1)
}

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@
<meta name="viewport" content="width=1280" />
<meta name="theme-color" content="#2d6a4f" />
<title>HK Planner</title>
<script type="module" crossorigin src="/hk-planner/assets/index-DCZDkbFd.js"></script>
<script type="module" crossorigin src="/hk-planner/assets/index-BdIiqds1.js"></script>
<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>
<body>

2
frontend/dist/sw.js vendored
View file

@ -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} didnt 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:"7b380a0736ef8c03d6f22034e9ce02f8"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-DCZDkbFd.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} didnt 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:"4f26c3459b65391beb1152aac0e652f9"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-BdIiqds1.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\//]}))});

View file

@ -328,7 +328,8 @@ export function Planner() {
setTimesheetSyncing(true)
try {
await syncTimesheets(start, end)
const shifts = await getWorkforceShifts(start, end)
const wsEnd = bookings.dates[bookings.dates.length - 1]
const shifts = await getWorkforceShifts(start, wsEnd)
setWfShifts(shifts)
flash('Timesheets pulled from Workforce')
} catch (e) {