diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec45330 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +frontend/node_modules/ +backend/node_modules/ diff --git a/backend/src/routes/workforce.js b/backend/src/routes/workforce.js index 8cff1b6..70962d1 100644 --- a/backend/src/routes/workforce.js +++ b/backend/src/routes/workforce.js @@ -58,6 +58,7 @@ export async function workforceRoutes(app) { await upsertWorkforceShifts(dailyRows) return { ok: true, from, to, dates_synced: dailyRows.length } } catch (err) { + req.log.error({ err: err.message }, 'workforce sync failed') const status = err.message.includes('not configured') ? 503 : 502 return reply.status(status).send({ error: err.message }) } diff --git a/frontend/dist/assets/index-BM62SqjA.js b/frontend/dist/assets/index-C1fxqzme.js similarity index 67% rename from frontend/dist/assets/index-BM62SqjA.js rename to frontend/dist/assets/index-C1fxqzme.js index f3a03cd..868480a 100644 --- a/frontend/dist/assets/index-BM62SqjA.js +++ b/frontend/dist/assets/index-C1fxqzme.js @@ -6,7 +6,7 @@ function Zf(i,u){for(var s=0;s>>1,R=W[y];if(0>>1;yp(le,F))oep(ae,le)?(W[y]=ae,W[oe]=F,y=oe):(W[y]=le,W[re]=F,y=re);else if(oep(ae,F))W[y]=ae,W[oe]=F,y=oe;else break e}}return X}function p(W,X){var F=W.sortIndex-X.sortIndex;return F!==0?F:W.id-X.id}if(typeof performance=="object"&&typeof performance.now=="function"){var h=performance;i.unstable_now=function(){return h.now()}}else{var g=Date,j=g.now();i.unstable_now=function(){return g.now()-j}}var w=[],_=[],P=1,N=null,M=3,H=!1,K=!1,T=!1,E=typeof setTimeout=="function"?setTimeout:null,A=typeof clearTimeout=="function"?clearTimeout:null,$=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function G(W){for(var X=s(_);X!==null;){if(X.callback===null)d(_);else if(X.startTime<=W)d(_),X.sortIndex=X.expirationTime,u(w,X);else break;X=s(_)}}function U(W){if(T=!1,G(W),!K)if(s(w)!==null)K=!0,Oe(ce);else{var X=s(_);X!==null&&ve(U,X.startTime-W)}}function ce(W,X){K=!1,T&&(T=!1,A(xe),xe=-1),H=!0;var F=M;try{for(G(X),N=s(w);N!==null&&(!(N.expirationTime>X)||W&&!Ke());){var y=N.callback;if(typeof y=="function"){N.callback=null,M=N.priorityLevel;var R=y(N.expirationTime<=X);X=i.unstable_now(),typeof R=="function"?N.callback=R:N===s(w)&&d(w),G(X)}else d(w);N=s(w)}if(N!==null)var ee=!0;else{var re=s(_);re!==null&&ve(U,re.startTime-X),ee=!1}return ee}finally{N=null,M=F,H=!1}}var de=!1,Ce=null,xe=-1,Ze=5,Ie=-1;function Ke(){return!(i.unstable_now()-IeW||125y?(W.sortIndex=F,u(_,W),s(w)===null&&W===s(_)&&(T?(A(xe),xe=-1):T=!0,ve(U,F-y))):(W.sortIndex=R,u(w,W),K||H||(K=!0,Oe(ce))),W},i.unstable_shouldYield=Ke,i.unstable_wrapCallback=function(W){var X=M;return function(){var F=M;M=X;try{return W.apply(this,arguments)}finally{M=F}}}})(ys)),ys}var wc;function op(){return wc||(wc=1,gs.exports=lp()),gs.exports}/** + */var xc;function lp(){return xc||(xc=1,(function(i){function u(W,X){var F=W.length;W.push(X);e:for(;0>>1,R=W[y];if(0>>1;yp(le,F))oep(ae,le)?(W[y]=ae,W[oe]=F,y=oe):(W[y]=le,W[re]=F,y=re);else if(oep(ae,F))W[y]=ae,W[oe]=F,y=oe;else break e}}return X}function p(W,X){var F=W.sortIndex-X.sortIndex;return F!==0?F:W.id-X.id}if(typeof performance=="object"&&typeof performance.now=="function"){var h=performance;i.unstable_now=function(){return h.now()}}else{var g=Date,j=g.now();i.unstable_now=function(){return g.now()-j}}var k=[],_=[],P=1,N=null,M=3,H=!1,K=!1,T=!1,E=typeof setTimeout=="function"?setTimeout:null,A=typeof clearTimeout=="function"?clearTimeout:null,$=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function J(W){for(var X=s(_);X!==null;){if(X.callback===null)d(_);else if(X.startTime<=W)d(_),X.sortIndex=X.expirationTime,u(k,X);else break;X=s(_)}}function U(W){if(T=!1,J(W),!K)if(s(k)!==null)K=!0,Oe(ce);else{var X=s(_);X!==null&&ve(U,X.startTime-W)}}function ce(W,X){K=!1,T&&(T=!1,A(xe),xe=-1),H=!0;var F=M;try{for(J(X),N=s(k);N!==null&&(!(N.expirationTime>X)||W&&!Ke());){var y=N.callback;if(typeof y=="function"){N.callback=null,M=N.priorityLevel;var R=y(N.expirationTime<=X);X=i.unstable_now(),typeof R=="function"?N.callback=R:N===s(k)&&d(k),J(X)}else d(k);N=s(k)}if(N!==null)var ee=!0;else{var re=s(_);re!==null&&ve(U,re.startTime-X),ee=!1}return ee}finally{N=null,M=F,H=!1}}var de=!1,Ce=null,xe=-1,qe=5,We=-1;function Ke(){return!(i.unstable_now()-WeW||125y?(W.sortIndex=F,u(_,W),s(k)===null&&W===s(_)&&(T?(A(xe),xe=-1):T=!0,ve(U,F-y))):(W.sortIndex=R,u(k,W),K||H||(K=!0,Oe(ce))),W},i.unstable_shouldYield=Ke,i.unstable_wrapCallback=function(W){var X=M;return function(){var F=M;M=X;try{return W.apply(this,arguments)}finally{M=F}}}})(ys)),ys}var kc;function op(){return kc||(kc=1,gs.exports=lp()),gs.exports}/** * @license React * react-dom.production.min.js * @@ -30,14 +30,14 @@ function Zf(i,u){for(var s=0;s"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),w=Object.prototype.hasOwnProperty,_=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,P={},N={};function M(e){return w.call(N,e)?!0:w.call(P,e)?!1:_.test(e)?N[e]=!0:(P[e]=!0,!1)}function H(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function K(e,t,n,r){if(t===null||typeof t>"u"||H(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function T(e,t,n,r,l,o,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=a}var E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){E[e]=new T(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];E[t]=new T(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){E[e]=new T(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){E[e]=new T(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){E[e]=new T(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){E[e]=new T(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){E[e]=new T(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){E[e]=new T(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){E[e]=new T(e,5,!1,e.toLowerCase(),null,!1,!1)});var A=/[\-:]([a-z])/g;function $(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){E[e]=new T(e,1,!1,e.toLowerCase(),null,!1,!1)}),E.xlinkHref=new T("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){E[e]=new T(e,1,!1,e.toLowerCase(),null,!0,!0)});function G(e,t,n,r){var l=E.hasOwnProperty(t)?E[t]:null;(l!==null?l.type!==0:r||!(2"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),k=Object.prototype.hasOwnProperty,_=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,P={},N={};function M(e){return k.call(N,e)?!0:k.call(P,e)?!1:_.test(e)?N[e]=!0:(P[e]=!0,!1)}function H(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function K(e,t,n,r){if(t===null||typeof t>"u"||H(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function T(e,t,n,r,l,o,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=a}var E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){E[e]=new T(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];E[t]=new T(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){E[e]=new T(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){E[e]=new T(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){E[e]=new T(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){E[e]=new T(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){E[e]=new T(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){E[e]=new T(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){E[e]=new T(e,5,!1,e.toLowerCase(),null,!1,!1)});var A=/[\-:]([a-z])/g;function $(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(A,$);E[t]=new T(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){E[e]=new T(e,1,!1,e.toLowerCase(),null,!1,!1)}),E.xlinkHref=new T("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){E[e]=new T(e,1,!1,e.toLowerCase(),null,!0,!0)});function J(e,t,n,r){var l=E.hasOwnProperty(t)?E[t]:null;(l!==null?l.type!==0:r||!(2f||l[a]!==o[f]){var m=` -`+l[a].replace(" at new "," at ");return e.displayName&&m.includes("")&&(m=m.replace("",e.displayName)),m}while(1<=a&&0<=f);break}}}finally{ee=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?R(e):""}function le(e){switch(e.tag){case 5:return R(e.type);case 16:return R("Lazy");case 13:return R("Suspense");case 19:return R("SuspenseList");case 0:case 2:case 15:return e=re(e.type,!1),e;case 11:return e=re(e.type.render,!1),e;case 1:return e=re(e.type,!0),e;default:return""}}function oe(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ce:return"Fragment";case de:return"Portal";case Ze:return"Profiler";case xe:return"StrictMode";case Te:return"Suspense";case Be:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ke:return(e.displayName||"Context")+".Consumer";case Ie:return(e._context.displayName||"Context")+".Provider";case Ee:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case qe:return t=e.displayName||null,t!==null?t:oe(e.type)||"Memo";case Oe:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function ae(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oe(t);case 8:return t===xe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function se(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function fe(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function We(e){var t=fe(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(a){r=""+a,o.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function At(e){e._valueTracker||(e._valueTracker=We(e))}function En(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=fe(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $t(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Vt(e,t){var n=t.checked;return F({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function _n(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=se(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function en(e,t){t=t.checked,t!=null&&G(e,"checked",t,!1)}function Ht(e,t){en(e,t);var n=se(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?tn(e,t.type,n):t.hasOwnProperty("defaultValue")&&tn(e,t.type,se(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Nn(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function tn(e,t,n){(t!=="number"||$t(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var _t=Array.isArray;function Dt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=pt.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function we(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var yt={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Tn=["Webkit","ms","Moz","O"];Object.keys(yt).forEach(function(e){Tn.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),yt[t]=yt[e]})});function Vn(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||yt.hasOwnProperty(e)&&yt[e]?(""+t).trim():t+"px"}function I(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Vn(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var te=F({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ie(e,t){if(t){if(te[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(s(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(s(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(s(61))}if(t.style!=null&&typeof t.style!="object")throw Error(s(62))}}function De(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var xt=null;function kr(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ro=null,Hn=null,Qn=null;function Ls(e){if(e=Vr(e)){if(typeof Ro!="function")throw Error(s(280));var t=e.stateNode;t&&(t=Ol(t),Ro(e.stateNode,e.type,t))}}function Os(e){Hn?Qn?Qn.push(e):Qn=[e]:Hn=e}function Ds(){if(Hn){var e=Hn,t=Qn;if(Qn=Hn=null,Ls(e),t)for(e=0;e>>=0,e===0?32:31-(fd(e)/pd|0)|0}var ml=64,vl=4194304;function Er(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function gl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,a=n&268435455;if(a!==0){var f=a&~l;f!==0?r=Er(f):(o&=a,o!==0&&(r=Er(o)))}else a=n&~l,a!==0?r=Er(a):o!==0&&(r=Er(o));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function _r(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Nt(t),e[t]=n}function gd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Dr),aa=" ",ua=!1;function ca(e,t){switch(e){case"keyup":return Qd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function da(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Jn=!1;function Yd(e,t){switch(e){case"compositionend":return da(t);case"keypress":return t.which!==32?null:(ua=!0,aa);case"textInput":return e=t.data,e===aa&&ua?null:e;default:return null}}function Jd(e,t){if(Jn)return e==="compositionend"||!Jo&&ca(e,t)?(e=na(),Sl=$o=sn=null,Jn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ya(n)}}function wa(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?wa(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function ka(){for(var e=window,t=$t();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$t(e.document)}return t}function Zo(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function rf(e){var t=ka(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&wa(n.ownerDocument.documentElement,n)){if(r!==null&&Zo(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=xa(n,o);var a=xa(n,r);l&&a&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Gn=null,qo=null,Fr=null,bo=!1;function Sa(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;bo||Gn==null||Gn!==$t(r)||(r=Gn,"selectionStart"in r&&Zo(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fr&&Wr(Fr,r)||(Fr=r,r=Rl(qo,"onSelect"),0er||(e.current=di[er],di[er]=null,er--)}function ye(e,t){er++,di[er]=e.current,e.current=t}var dn={},be=cn(dn),it=cn(!1),Ln=dn;function tr(e,t){var n=e.type.contextTypes;if(!n)return dn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function st(e){return e=e.childContextTypes,e!=null}function Dl(){Se(it),Se(be)}function Wa(e,t,n){if(be.current!==dn)throw Error(s(168));ye(be,t),ye(it,n)}function Fa(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(s(108,ae(e)||"Unknown",l));return F({},n,r)}function Ml(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||dn,Ln=be.current,ye(be,e),ye(it,it.current),!0}function Ua(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=Fa(e,t,Ln),r.__reactInternalMemoizedMergedChildContext=e,Se(it),Se(be),ye(be,e)):Se(it),ye(it,n)}var Kt=null,Il=!1,fi=!1;function Ba(e){Kt===null?Kt=[e]:Kt.push(e)}function vf(e){Il=!0,Ba(e)}function fn(){if(!fi&&Kt!==null){fi=!0;var e=0,t=ge;try{var n=Kt;for(ge=1;e>=a,l-=a,Yt=1<<32-Nt(t)+l|n<b?(Qe=q,q=null):Qe=q.sibling;var he=z(x,q,k[b],D);if(he===null){q===null&&(q=Qe);break}e&&q&&he.alternate===null&&t(x,q),v=o(he,v,b),Z===null?J=he:Z.sibling=he,Z=he,q=Qe}if(b===k.length)return n(x,q),je&&Dn(x,b),J;if(q===null){for(;bb?(Qe=q,q=null):Qe=q.sibling;var kn=z(x,q,he.value,D);if(kn===null){q===null&&(q=Qe);break}e&&q&&kn.alternate===null&&t(x,q),v=o(kn,v,b),Z===null?J=kn:Z.sibling=kn,Z=kn,q=Qe}if(he.done)return n(x,q),je&&Dn(x,b),J;if(q===null){for(;!he.done;b++,he=k.next())he=O(x,he.value,D),he!==null&&(v=o(he,v,b),Z===null?J=he:Z.sibling=he,Z=he);return je&&Dn(x,b),J}for(q=r(x,q);!he.done;b++,he=k.next())he=B(q,x,b,he.value,D),he!==null&&(e&&he.alternate!==null&&q.delete(he.key===null?b:he.key),v=o(he,v,b),Z===null?J=he:Z.sibling=he,Z=he);return e&&q.forEach(function(Xf){return t(x,Xf)}),je&&Dn(x,b),J}function Le(x,v,k,D){if(typeof k=="object"&&k!==null&&k.type===Ce&&k.key===null&&(k=k.props.children),typeof k=="object"&&k!==null){switch(k.$$typeof){case ce:e:{for(var J=k.key,Z=v;Z!==null;){if(Z.key===J){if(J=k.type,J===Ce){if(Z.tag===7){n(x,Z.sibling),v=l(Z,k.props.children),v.return=x,x=v;break e}}else if(Z.elementType===J||typeof J=="object"&&J!==null&&J.$$typeof===Oe&&Ka(J)===Z.type){n(x,Z.sibling),v=l(Z,k.props),v.ref=Hr(x,Z,k),v.return=x,x=v;break e}n(x,Z);break}else t(x,Z);Z=Z.sibling}k.type===Ce?(v=$n(k.props.children,x.mode,D,k.key),v.return=x,x=v):(D=co(k.type,k.key,k.props,null,x.mode,D),D.ref=Hr(x,v,k),D.return=x,x=D)}return a(x);case de:e:{for(Z=k.key;v!==null;){if(v.key===Z)if(v.tag===4&&v.stateNode.containerInfo===k.containerInfo&&v.stateNode.implementation===k.implementation){n(x,v.sibling),v=l(v,k.children||[]),v.return=x,x=v;break e}else{n(x,v);break}else t(x,v);v=v.sibling}v=us(k,x.mode,D),v.return=x,x=v}return a(x);case Oe:return Z=k._init,Le(x,v,Z(k._payload),D)}if(_t(k))return Q(x,v,k,D);if(X(k))return Y(x,v,k,D);Bl(x,k)}return typeof k=="string"&&k!==""||typeof k=="number"?(k=""+k,v!==null&&v.tag===6?(n(x,v.sibling),v=l(v,k),v.return=x,x=v):(n(x,v),v=as(k,x.mode,D),v.return=x,x=v),a(x)):n(x,v)}return Le}var or=Ya(!0),Ja=Ya(!1),Al=cn(null),$l=null,ir=null,yi=null;function xi(){yi=ir=$l=null}function wi(e){var t=Al.current;Se(Al),e._currentValue=t}function ki(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function sr(e,t){$l=e,yi=ir=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(at=!0),e.firstContext=null)}function St(e){var t=e._currentValue;if(yi!==e)if(e={context:e,memoizedValue:t,next:null},ir===null){if($l===null)throw Error(s(308));ir=e,$l.dependencies={lanes:0,firstContext:e}}else ir=ir.next=e;return t}var Mn=null;function Si(e){Mn===null?Mn=[e]:Mn.push(e)}function Ga(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Si(t)):(n.next=l.next,l.next=n),t.interleaved=n,Gt(e,r)}function Gt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var pn=!1;function ji(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function hn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(pe&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Gt(e,n)}return l=r.interleaved,l===null?(t.next=t,Si(r)):(t.next=l.next,l.next=t),r.interleaved=t,Gt(e,n)}function Vl(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Wo(e,n)}}function Za(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=a:o=o.next=a,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Hl(e,t,n,r){var l=e.updateQueue;pn=!1;var o=l.firstBaseUpdate,a=l.lastBaseUpdate,f=l.shared.pending;if(f!==null){l.shared.pending=null;var m=f,C=m.next;m.next=null,a===null?o=C:a.next=C,a=m;var L=e.alternate;L!==null&&(L=L.updateQueue,f=L.lastBaseUpdate,f!==a&&(f===null?L.firstBaseUpdate=C:f.next=C,L.lastBaseUpdate=m))}if(o!==null){var O=l.baseState;a=0,L=C=m=null,f=o;do{var z=f.lane,B=f.eventTime;if((r&z)===z){L!==null&&(L=L.next={eventTime:B,lane:0,tag:f.tag,payload:f.payload,callback:f.callback,next:null});e:{var Q=e,Y=f;switch(z=t,B=n,Y.tag){case 1:if(Q=Y.payload,typeof Q=="function"){O=Q.call(B,O,z);break e}O=Q;break e;case 3:Q.flags=Q.flags&-65537|128;case 0:if(Q=Y.payload,z=typeof Q=="function"?Q.call(B,O,z):Q,z==null)break e;O=F({},O,z);break e;case 2:pn=!0}}f.callback!==null&&f.lane!==0&&(e.flags|=64,z=l.effects,z===null?l.effects=[f]:z.push(f))}else B={eventTime:B,lane:z,tag:f.tag,payload:f.payload,callback:f.callback,next:null},L===null?(C=L=B,m=O):L=L.next=B,a|=z;if(f=f.next,f===null){if(f=l.shared.pending,f===null)break;z=f,f=z.next,z.next=null,l.lastBaseUpdate=z,l.shared.pending=null}}while(!0);if(L===null&&(m=O),l.baseState=m,l.firstBaseUpdate=C,l.lastBaseUpdate=L,t=l.shared.interleaved,t!==null){l=t;do a|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Fn|=a,e.lanes=a,e.memoizedState=O}}function qa(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Pi.transition;Pi.transition={};try{e(!1),t()}finally{ge=n,Pi.transition=r}}function gu(){return jt().memoizedState}function wf(e,t,n){var r=yn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},yu(e))xu(t,n);else if(n=Ga(e,t,n,r),n!==null){var l=ot();Ot(n,e,r,l),wu(n,t,r)}}function kf(e,t,n){var r=yn(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(yu(e))xu(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var a=t.lastRenderedState,f=o(a,n);if(l.hasEagerState=!0,l.eagerState=f,Pt(f,a)){var m=t.interleaved;m===null?(l.next=l,Si(t)):(l.next=m.next,m.next=l),t.interleaved=l;return}}catch{}finally{}n=Ga(e,t,l,r),n!==null&&(l=ot(),Ot(n,e,r,l),wu(n,t,r))}}function yu(e){var t=e.alternate;return e===Ne||t!==null&&t===Ne}function xu(e,t){Jr=Yl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function wu(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Wo(e,n)}}var Xl={readContext:St,useCallback:et,useContext:et,useEffect:et,useImperativeHandle:et,useInsertionEffect:et,useLayoutEffect:et,useMemo:et,useReducer:et,useRef:et,useState:et,useDebugValue:et,useDeferredValue:et,useTransition:et,useMutableSource:et,useSyncExternalStore:et,useId:et,unstable_isNewReconciler:!1},Sf={readContext:St,useCallback:function(e,t){return Ft().memoizedState=[e,t===void 0?null:t],e},useContext:St,useEffect:uu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Jl(4194308,4,fu.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Jl(4194308,4,e,t)},useInsertionEffect:function(e,t){return Jl(4,2,e,t)},useMemo:function(e,t){var n=Ft();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ft();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=wf.bind(null,Ne,e),[r.memoizedState,e]},useRef:function(e){var t=Ft();return e={current:e},t.memoizedState=e},useState:su,useDebugValue:Mi,useDeferredValue:function(e){return Ft().memoizedState=e},useTransition:function(){var e=su(!1),t=e[0];return e=xf.bind(null,e[1]),Ft().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Ne,l=Ft();if(je){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),He===null)throw Error(s(349));(Wn&30)!==0||nu(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,uu(lu.bind(null,r,o,e),[e]),r.flags|=2048,Zr(9,ru.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ft(),t=He.identifierPrefix;if(je){var n=Jt,r=Yt;n=(r&~(1<<32-Nt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Gr++,0")&&(m=m.replace("",e.displayName)),m}while(1<=a&&0<=f);break}}}finally{ee=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?R(e):""}function le(e){switch(e.tag){case 5:return R(e.type);case 16:return R("Lazy");case 13:return R("Suspense");case 19:return R("SuspenseList");case 0:case 2:case 15:return e=re(e.type,!1),e;case 11:return e=re(e.type.render,!1),e;case 1:return e=re(e.type,!0),e;default:return""}}function oe(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ce:return"Fragment";case de:return"Portal";case qe:return"Profiler";case xe:return"StrictMode";case Te:return"Suspense";case Ae:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ke:return(e.displayName||"Context")+".Consumer";case We:return(e._context.displayName||"Context")+".Provider";case Ee:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case be:return t=e.displayName||null,t!==null?t:oe(e.type)||"Memo";case Oe:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function ae(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oe(t);case 8:return t===xe?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function se(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function fe(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Fe(e){var t=fe(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(a){r=""+a,o.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function At(e){e._valueTracker||(e._valueTracker=Fe(e))}function En(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=fe(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function $t(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Vt(e,t){var n=t.checked;return F({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function _n(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=se(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function en(e,t){t=t.checked,t!=null&&J(e,"checked",t,!1)}function Ht(e,t){en(e,t);var n=se(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?tn(e,t.type,n):t.hasOwnProperty("defaultValue")&&tn(e,t.type,se(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Nn(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function tn(e,t,n){(t!=="number"||$t(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var _t=Array.isArray;function Dt(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=pt.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ke(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var yt={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Tn=["Webkit","ms","Moz","O"];Object.keys(yt).forEach(function(e){Tn.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),yt[t]=yt[e]})});function Vn(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||yt.hasOwnProperty(e)&&yt[e]?(""+t).trim():t+"px"}function I(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=Vn(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var ne=F({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ie(e,t){if(t){if(ne[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(s(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(s(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(s(61))}if(t.style!=null&&typeof t.style!="object")throw Error(s(62))}}function Me(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var xt=null;function wr(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ro=null,Hn=null,Qn=null;function Ls(e){if(e=Vr(e)){if(typeof Ro!="function")throw Error(s(280));var t=e.stateNode;t&&(t=Ol(t),Ro(e.stateNode,e.type,t))}}function Os(e){Hn?Qn?Qn.push(e):Qn=[e]:Hn=e}function Ds(){if(Hn){var e=Hn,t=Qn;if(Qn=Hn=null,Ls(e),t)for(e=0;e>>=0,e===0?32:31-(fd(e)/pd|0)|0}var ml=64,vl=4194304;function Er(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function gl(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,a=n&268435455;if(a!==0){var f=a&~l;f!==0?r=Er(f):(o&=a,o!==0&&(r=Er(o)))}else a=n&~l,a!==0?r=Er(a):o!==0&&(r=Er(o));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function _r(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Nt(t),e[t]=n}function gd(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Dr),aa=" ",ua=!1;function ca(e,t){switch(e){case"keyup":return Qd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function da(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Gn=!1;function Yd(e,t){switch(e){case"compositionend":return da(t);case"keypress":return t.which!==32?null:(ua=!0,aa);case"textInput":return e=t.data,e===aa&&ua?null:e;default:return null}}function Gd(e,t){if(Gn)return e==="compositionend"||!Go&&ca(e,t)?(e=na(),Sl=$o=sn=null,Gn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=ya(n)}}function ka(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ka(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wa(){for(var e=window,t=$t();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=$t(e.document)}return t}function Zo(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function rf(e){var t=wa(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&ka(n.ownerDocument.documentElement,n)){if(r!==null&&Zo(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=xa(n,o);var a=xa(n,r);l&&a&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Jn=null,qo=null,Fr=null,bo=!1;function Sa(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;bo||Jn==null||Jn!==$t(r)||(r=Jn,"selectionStart"in r&&Zo(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fr&&Wr(Fr,r)||(Fr=r,r=Rl(qo,"onSelect"),0er||(e.current=di[er],di[er]=null,er--)}function ye(e,t){er++,di[er]=e.current,e.current=t}var dn={},et=cn(dn),it=cn(!1),Ln=dn;function tr(e,t){var n=e.type.contextTypes;if(!n)return dn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function st(e){return e=e.childContextTypes,e!=null}function Dl(){Se(it),Se(et)}function Wa(e,t,n){if(et.current!==dn)throw Error(s(168));ye(et,t),ye(it,n)}function Fa(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(s(108,ae(e)||"Unknown",l));return F({},n,r)}function Ml(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||dn,Ln=et.current,ye(et,e),ye(it,it.current),!0}function Ua(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=Fa(e,t,Ln),r.__reactInternalMemoizedMergedChildContext=e,Se(it),Se(et),ye(et,e)):Se(it),ye(it,n)}var Kt=null,Il=!1,fi=!1;function Ba(e){Kt===null?Kt=[e]:Kt.push(e)}function vf(e){Il=!0,Ba(e)}function fn(){if(!fi&&Kt!==null){fi=!0;var e=0,t=ge;try{var n=Kt;for(ge=1;e>=a,l-=a,Yt=1<<32-Nt(t)+l|n<b?(Qe=q,q=null):Qe=q.sibling;var me=z(x,q,w[b],D);if(me===null){q===null&&(q=Qe);break}e&&q&&me.alternate===null&&t(x,q),v=o(me,v,b),Z===null?G=me:Z.sibling=me,Z=me,q=Qe}if(b===w.length)return n(x,q),je&&Dn(x,b),G;if(q===null){for(;bb?(Qe=q,q=null):Qe=q.sibling;var wn=z(x,q,me.value,D);if(wn===null){q===null&&(q=Qe);break}e&&q&&wn.alternate===null&&t(x,q),v=o(wn,v,b),Z===null?G=wn:Z.sibling=wn,Z=wn,q=Qe}if(me.done)return n(x,q),je&&Dn(x,b),G;if(q===null){for(;!me.done;b++,me=w.next())me=O(x,me.value,D),me!==null&&(v=o(me,v,b),Z===null?G=me:Z.sibling=me,Z=me);return je&&Dn(x,b),G}for(q=r(x,q);!me.done;b++,me=w.next())me=B(q,x,b,me.value,D),me!==null&&(e&&me.alternate!==null&&q.delete(me.key===null?b:me.key),v=o(me,v,b),Z===null?G=me:Z.sibling=me,Z=me);return e&&q.forEach(function(Xf){return t(x,Xf)}),je&&Dn(x,b),G}function Le(x,v,w,D){if(typeof w=="object"&&w!==null&&w.type===Ce&&w.key===null&&(w=w.props.children),typeof w=="object"&&w!==null){switch(w.$$typeof){case ce:e:{for(var G=w.key,Z=v;Z!==null;){if(Z.key===G){if(G=w.type,G===Ce){if(Z.tag===7){n(x,Z.sibling),v=l(Z,w.props.children),v.return=x,x=v;break e}}else if(Z.elementType===G||typeof G=="object"&&G!==null&&G.$$typeof===Oe&&Ka(G)===Z.type){n(x,Z.sibling),v=l(Z,w.props),v.ref=Hr(x,Z,w),v.return=x,x=v;break e}n(x,Z);break}else t(x,Z);Z=Z.sibling}w.type===Ce?(v=$n(w.props.children,x.mode,D,w.key),v.return=x,x=v):(D=co(w.type,w.key,w.props,null,x.mode,D),D.ref=Hr(x,v,w),D.return=x,x=D)}return a(x);case de:e:{for(Z=w.key;v!==null;){if(v.key===Z)if(v.tag===4&&v.stateNode.containerInfo===w.containerInfo&&v.stateNode.implementation===w.implementation){n(x,v.sibling),v=l(v,w.children||[]),v.return=x,x=v;break e}else{n(x,v);break}else t(x,v);v=v.sibling}v=us(w,x.mode,D),v.return=x,x=v}return a(x);case Oe:return Z=w._init,Le(x,v,Z(w._payload),D)}if(_t(w))return Q(x,v,w,D);if(X(w))return Y(x,v,w,D);Bl(x,w)}return typeof w=="string"&&w!==""||typeof w=="number"?(w=""+w,v!==null&&v.tag===6?(n(x,v.sibling),v=l(v,w),v.return=x,x=v):(n(x,v),v=as(w,x.mode,D),v.return=x,x=v),a(x)):n(x,v)}return Le}var or=Ya(!0),Ga=Ya(!1),Al=cn(null),$l=null,ir=null,yi=null;function xi(){yi=ir=$l=null}function ki(e){var t=Al.current;Se(Al),e._currentValue=t}function wi(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function sr(e,t){$l=e,yi=ir=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(at=!0),e.firstContext=null)}function St(e){var t=e._currentValue;if(yi!==e)if(e={context:e,memoizedValue:t,next:null},ir===null){if($l===null)throw Error(s(308));ir=e,$l.dependencies={lanes:0,firstContext:e}}else ir=ir.next=e;return t}var Mn=null;function Si(e){Mn===null?Mn=[e]:Mn.push(e)}function Ja(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Si(t)):(n.next=l.next,l.next=n),t.interleaved=n,Jt(e,r)}function Jt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var pn=!1;function ji(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function hn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(he&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Jt(e,n)}return l=r.interleaved,l===null?(t.next=t,Si(r)):(t.next=l.next,l.next=t),r.interleaved=t,Jt(e,n)}function Vl(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Wo(e,n)}}function Za(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=a:o=o.next=a,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Hl(e,t,n,r){var l=e.updateQueue;pn=!1;var o=l.firstBaseUpdate,a=l.lastBaseUpdate,f=l.shared.pending;if(f!==null){l.shared.pending=null;var m=f,C=m.next;m.next=null,a===null?o=C:a.next=C,a=m;var L=e.alternate;L!==null&&(L=L.updateQueue,f=L.lastBaseUpdate,f!==a&&(f===null?L.firstBaseUpdate=C:f.next=C,L.lastBaseUpdate=m))}if(o!==null){var O=l.baseState;a=0,L=C=m=null,f=o;do{var z=f.lane,B=f.eventTime;if((r&z)===z){L!==null&&(L=L.next={eventTime:B,lane:0,tag:f.tag,payload:f.payload,callback:f.callback,next:null});e:{var Q=e,Y=f;switch(z=t,B=n,Y.tag){case 1:if(Q=Y.payload,typeof Q=="function"){O=Q.call(B,O,z);break e}O=Q;break e;case 3:Q.flags=Q.flags&-65537|128;case 0:if(Q=Y.payload,z=typeof Q=="function"?Q.call(B,O,z):Q,z==null)break e;O=F({},O,z);break e;case 2:pn=!0}}f.callback!==null&&f.lane!==0&&(e.flags|=64,z=l.effects,z===null?l.effects=[f]:z.push(f))}else B={eventTime:B,lane:z,tag:f.tag,payload:f.payload,callback:f.callback,next:null},L===null?(C=L=B,m=O):L=L.next=B,a|=z;if(f=f.next,f===null){if(f=l.shared.pending,f===null)break;z=f,f=z.next,z.next=null,l.lastBaseUpdate=z,l.shared.pending=null}}while(!0);if(L===null&&(m=O),l.baseState=m,l.firstBaseUpdate=C,l.lastBaseUpdate=L,t=l.shared.interleaved,t!==null){l=t;do a|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);Fn|=a,e.lanes=a,e.memoizedState=O}}function qa(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Pi.transition;Pi.transition={};try{e(!1),t()}finally{ge=n,Pi.transition=r}}function gu(){return jt().memoizedState}function kf(e,t,n){var r=yn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},yu(e))xu(t,n);else if(n=Ja(e,t,n,r),n!==null){var l=ot();Ot(n,e,r,l),ku(n,t,r)}}function wf(e,t,n){var r=yn(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(yu(e))xu(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var a=t.lastRenderedState,f=o(a,n);if(l.hasEagerState=!0,l.eagerState=f,Pt(f,a)){var m=t.interleaved;m===null?(l.next=l,Si(t)):(l.next=m.next,m.next=l),t.interleaved=l;return}}catch{}finally{}n=Ja(e,t,l,r),n!==null&&(l=ot(),Ot(n,e,r,l),ku(n,t,r))}}function yu(e){var t=e.alternate;return e===Ne||t!==null&&t===Ne}function xu(e,t){Gr=Yl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ku(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Wo(e,n)}}var Xl={readContext:St,useCallback:tt,useContext:tt,useEffect:tt,useImperativeHandle:tt,useInsertionEffect:tt,useLayoutEffect:tt,useMemo:tt,useReducer:tt,useRef:tt,useState:tt,useDebugValue:tt,useDeferredValue:tt,useTransition:tt,useMutableSource:tt,useSyncExternalStore:tt,useId:tt,unstable_isNewReconciler:!1},Sf={readContext:St,useCallback:function(e,t){return Ft().memoizedState=[e,t===void 0?null:t],e},useContext:St,useEffect:uu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Gl(4194308,4,fu.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Gl(4194308,4,e,t)},useInsertionEffect:function(e,t){return Gl(4,2,e,t)},useMemo:function(e,t){var n=Ft();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ft();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=kf.bind(null,Ne,e),[r.memoizedState,e]},useRef:function(e){var t=Ft();return e={current:e},t.memoizedState=e},useState:su,useDebugValue:Mi,useDeferredValue:function(e){return Ft().memoizedState=e},useTransition:function(){var e=su(!1),t=e[0];return e=xf.bind(null,e[1]),Ft().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Ne,l=Ft();if(je){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),He===null)throw Error(s(349));(Wn&30)!==0||nu(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,uu(lu.bind(null,r,o,e),[e]),r.flags|=2048,Zr(9,ru.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ft(),t=He.identifierPrefix;if(je){var n=Gt,r=Yt;n=(r&~(1<<32-Nt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Jr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[It]=t,e[$r]=r,Bu(e,t,!1,!1),t.stateNode=e;e:{switch(a=De(n,r),n){case"dialog":ke("cancel",e),ke("close",e),l=r;break;case"iframe":case"object":case"embed":ke("load",e),l=r;break;case"video":case"audio":for(l=0;lfr&&(t.flags|=128,r=!0,qr(o,!1),t.lanes=4194304)}else{if(!r)if(e=Ql(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),qr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!a.alternate&&!je)return tt(t),null}else 2*ze()-o.renderingStartTime>fr&&n!==1073741824&&(t.flags|=128,r=!0,qr(o,!1),t.lanes=4194304);o.isBackwards?(a.sibling=t.child,t.child=a):(n=o.last,n!==null?n.sibling=a:t.child=a,o.last=a)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=ze(),t.sibling=null,n=_e.current,ye(_e,r?n&1|2:n&1),t):(tt(t),null);case 22:case 23:return os(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(gt&1073741824)!==0&&(tt(t),t.subtreeFlags&6&&(t.flags|=8192)):tt(t),null;case 24:return null;case 25:return null}throw Error(s(156,t.tag))}function Rf(e,t){switch(hi(t),t.tag){case 1:return st(t.type)&&Dl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ar(),Se(it),Se(be),Ni(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Ei(t),null;case 13:if(Se(_e),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));lr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Se(_e),null;case 4:return ar(),null;case 10:return wi(t.type._context),null;case 22:case 23:return os(),null;case 24:return null;default:return null}}var eo=!1,nt=!1,zf=typeof WeakSet=="function"?WeakSet:Set,V=null;function cr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Re(e,t,r)}else n.current=null}function Yi(e,t,n){try{n()}catch(r){Re(e,t,r)}}var Vu=!1;function Lf(e,t){if(oi=wl,e=ka(),Zo(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var a=0,f=-1,m=-1,C=0,L=0,O=e,z=null;t:for(;;){for(var B;O!==n||l!==0&&O.nodeType!==3||(f=a+l),O!==o||r!==0&&O.nodeType!==3||(m=a+r),O.nodeType===3&&(a+=O.nodeValue.length),(B=O.firstChild)!==null;)z=O,O=B;for(;;){if(O===e)break t;if(z===n&&++C===l&&(f=a),z===o&&++L===r&&(m=a),(B=O.nextSibling)!==null)break;O=z,z=O.parentNode}O=B}n=f===-1||m===-1?null:{start:f,end:m}}else n=null}n=n||{start:0,end:0}}else n=null;for(ii={focusedElem:e,selectionRange:n},wl=!1,V=t;V!==null;)if(t=V,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,V=e;else for(;V!==null;){t=V;try{var Q=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(Q!==null){var Y=Q.memoizedProps,Le=Q.memoizedState,x=t.stateNode,v=x.getSnapshotBeforeUpdate(t.elementType===t.type?Y:Rt(t.type,Y),Le);x.__reactInternalSnapshotBeforeUpdate=v}break;case 3:var k=t.stateNode.containerInfo;k.nodeType===1?k.textContent="":k.nodeType===9&&k.documentElement&&k.removeChild(k.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(s(163))}}catch(D){Re(t,t.return,D)}if(e=t.sibling,e!==null){e.return=t.return,V=e;break}V=t.return}return Q=Vu,Vu=!1,Q}function br(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Yi(t,n,o)}l=l.next}while(l!==r)}}function to(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Ji(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Hu(e){var t=e.alternate;t!==null&&(e.alternate=null,Hu(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[It],delete t[$r],delete t[ci],delete t[hf],delete t[mf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Qu(e){return e.tag===5||e.tag===3||e.tag===4}function Ku(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Qu(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Gi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Ll));else if(r!==4&&(e=e.child,e!==null))for(Gi(e,t,n),e=e.sibling;e!==null;)Gi(e,t,n),e=e.sibling}function Xi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Xi(e,t,n),e=e.sibling;e!==null;)Xi(e,t,n),e=e.sibling}var Ye=null,zt=!1;function mn(e,t,n){for(n=n.child;n!==null;)Yu(e,t,n),n=n.sibling}function Yu(e,t,n){if(Mt&&typeof Mt.onCommitFiberUnmount=="function")try{Mt.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:nt||cr(n,t);case 6:var r=Ye,l=zt;Ye=null,mn(e,t,n),Ye=r,zt=l,Ye!==null&&(zt?(e=Ye,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Ye.removeChild(n.stateNode));break;case 18:Ye!==null&&(zt?(e=Ye,n=n.stateNode,e.nodeType===8?ui(e.parentNode,n):e.nodeType===1&&ui(e,n),zr(e)):ui(Ye,n.stateNode));break;case 4:r=Ye,l=zt,Ye=n.stateNode.containerInfo,zt=!0,mn(e,t,n),Ye=r,zt=l;break;case 0:case 11:case 14:case 15:if(!nt&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,a=o.destroy;o=o.tag,a!==void 0&&((o&2)!==0||(o&4)!==0)&&Yi(n,t,a),l=l.next}while(l!==r)}mn(e,t,n);break;case 1:if(!nt&&(cr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(f){Re(n,t,f)}mn(e,t,n);break;case 21:mn(e,t,n);break;case 22:n.mode&1?(nt=(r=nt)||n.memoizedState!==null,mn(e,t,n),nt=r):mn(e,t,n);break;default:mn(e,t,n)}}function Ju(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new zf),t.forEach(function(r){var l=Af.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Lt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=a),r&=~o}if(r=l,r=ze()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Df(r/1960))-r,10e?16:e,gn===null)var r=!1;else{if(e=gn,gn=null,io=0,(pe&6)!==0)throw Error(s(331));var l=pe;for(pe|=4,V=e.current;V!==null;){var o=V,a=o.child;if((V.flags&16)!==0){var f=o.deletions;if(f!==null){for(var m=0;mze()-bi?Bn(e,0):qi|=n),ct(e,t)}function sc(e,t){t===0&&((e.mode&1)===0?t=1:(t=vl,vl<<=1,(vl&130023424)===0&&(vl=4194304)));var n=ot();e=Gt(e,t),e!==null&&(_r(e,t,n),ct(e,n))}function Bf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),sc(e,n)}function Af(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(s(314))}r!==null&&r.delete(t),sc(e,n)}var ac;ac=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||it.current)at=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return at=!1,Pf(e,t,n);at=(e.flags&131072)!==0}else at=!1,je&&(t.flags&1048576)!==0&&Aa(t,Fl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;bl(e,t),e=t.pendingProps;var l=tr(t,be.current);sr(t,n),l=Ri(null,t,r,e,l,n);var o=zi();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,st(r)?(o=!0,Ml(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ji(t),l.updater=Zl,t.stateNode=l,l._reactInternals=t,Wi(t,r,e,n),t=Ai(null,t,r,!0,o,n)):(t.tag=0,je&&o&&pi(t),lt(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(bl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Vf(r),e=Rt(r,e),l){case 0:t=Bi(null,t,r,e,n);break e;case 1:t=Du(null,t,r,e,n);break e;case 11:t=Tu(null,t,r,e,n);break e;case 14:t=Ru(null,t,r,Rt(r.type,e),n);break e}throw Error(s(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Bi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Du(e,t,r,l,n);case 3:e:{if(Mu(t),e===null)throw Error(s(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Xa(e,t),Hl(t,r,null,n);var a=t.memoizedState;if(r=a.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=ur(Error(s(423)),t),t=Iu(e,t,r,n,l);break e}else if(r!==l){l=ur(Error(s(424)),t),t=Iu(e,t,r,n,l);break e}else for(vt=un(t.stateNode.containerInfo.firstChild),mt=t,je=!0,Tt=null,n=Ja(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(lr(),r===l){t=Zt(e,t,n);break e}lt(e,t,r,n)}t=t.child}return t;case 5:return ba(t),e===null&&vi(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,a=l.children,si(r,l)?a=null:o!==null&&si(r,o)&&(t.flags|=32),Ou(e,t),lt(e,t,a,n),t.child;case 6:return e===null&&vi(t),null;case 13:return Wu(e,t,n);case 4:return Ci(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=or(t,null,r,n):lt(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Tu(e,t,r,l,n);case 7:return lt(e,t,t.pendingProps,n),t.child;case 8:return lt(e,t,t.pendingProps.children,n),t.child;case 12:return lt(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,a=l.value,ye(Al,r._currentValue),r._currentValue=a,o!==null)if(Pt(o.value,a)){if(o.children===l.children&&!it.current){t=Zt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var f=o.dependencies;if(f!==null){a=o.child;for(var m=f.firstContext;m!==null;){if(m.context===r){if(o.tag===1){m=Xt(-1,n&-n),m.tag=2;var C=o.updateQueue;if(C!==null){C=C.shared;var L=C.pending;L===null?m.next=m:(m.next=L.next,L.next=m),C.pending=m}}o.lanes|=n,m=o.alternate,m!==null&&(m.lanes|=n),ki(o.return,n,t),f.lanes|=n;break}m=m.next}}else if(o.tag===10)a=o.type===t.type?null:o.child;else if(o.tag===18){if(a=o.return,a===null)throw Error(s(341));a.lanes|=n,f=a.alternate,f!==null&&(f.lanes|=n),ki(a,n,t),a=o.sibling}else a=o.child;if(a!==null)a.return=o;else for(a=o;a!==null;){if(a===t){a=null;break}if(o=a.sibling,o!==null){o.return=a.return,a=o;break}a=a.return}o=a}lt(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,sr(t,n),l=St(l),r=r(l),t.flags|=1,lt(e,t,r,n),t.child;case 14:return r=t.type,l=Rt(r,t.pendingProps),l=Rt(r.type,l),Ru(e,t,r,l,n);case 15:return zu(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),bl(e,t),t.tag=1,st(r)?(e=!0,Ml(t)):e=!1,sr(t,n),Su(t,r,l),Wi(t,r,l,n),Ai(null,t,r,!0,e,n);case 19:return Uu(e,t,n);case 22:return Lu(e,t,n)}throw Error(s(156,t.tag))};function uc(e,t){return $s(e,t)}function $f(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Et(e,t,n,r){return new $f(e,t,n,r)}function ss(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vf(e){if(typeof e=="function")return ss(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ee)return 11;if(e===qe)return 14}return 2}function wn(e,t){var n=e.alternate;return n===null?(n=Et(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function co(e,t,n,r,l,o){var a=2;if(r=e,typeof e=="function")ss(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case Ce:return $n(n.children,l,o,t);case xe:a=8,l|=8;break;case Ze:return e=Et(12,n,t,l|2),e.elementType=Ze,e.lanes=o,e;case Te:return e=Et(13,n,t,l),e.elementType=Te,e.lanes=o,e;case Be:return e=Et(19,n,t,l),e.elementType=Be,e.lanes=o,e;case ve:return fo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Ie:a=10;break e;case Ke:a=9;break e;case Ee:a=11;break e;case qe:a=14;break e;case Oe:a=16,r=null;break e}throw Error(s(130,e==null?e:typeof e,""))}return t=Et(a,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function $n(e,t,n,r){return e=Et(7,e,r,t),e.lanes=n,e}function fo(e,t,n,r){return e=Et(22,e,r,t),e.elementType=ve,e.lanes=n,e.stateNode={isHidden:!1},e}function as(e,t,n){return e=Et(6,e,null,t),e.lanes=n,e}function us(e,t,n){return t=Et(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Hf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Io(0),this.expirationTimes=Io(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Io(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function cs(e,t,n,r,l,o,a,f,m){return e=new Hf(e,t,n,f,m),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Et(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ji(o),e}function Qf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(u){console.error(u)}}return i(),vs.exports=ip(),vs.exports}var jc;function sp(){if(jc)return xo;jc=1;var i=Bc();return xo.createRoot=i.createRoot,xo.hydrateRoot=i.hydrateRoot,xo}var ap=sp();Bc();/** +`+o.stack}return{value:e,source:t,stack:l,digest:null}}function Fi(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Ui(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Ef=typeof WeakMap=="function"?WeakMap:Map;function Cu(e,t,n){n=Xt(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){lo||(lo=!0,es=r),Ui(e,t)},n}function Eu(e,t,n){n=Xt(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Ui(e,t)}}var o=e.stateNode;return o!==null&&typeof o.componentDidCatch=="function"&&(n.callback=function(){Ui(e,t),typeof r!="function"&&(vn===null?vn=new Set([this]):vn.add(this));var a=t.stack;this.componentDidCatch(t.value,{componentStack:a!==null?a:""})}),n}function _u(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ef;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Uf.bind(null,e,t,n),t.then(e,e))}function Nu(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Pu(e,t,n,r,l){return(e.mode&1)===0?(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Xt(-1,1),t.tag=2,hn(n,t,1))),n.lanes|=1),e):(e.flags|=65536,e.lanes=l,e)}var _f=U.ReactCurrentOwner,at=!1;function lt(e,t,n,r){t.child=e===null?Ga(t,null,n,r):or(t,e.child,n,r)}function Tu(e,t,n,r,l){n=n.render;var o=t.ref;return sr(t,l),r=Ri(e,t,n,r,o,l),n=zi(),e!==null&&!at?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Zt(e,t,l)):(je&&n&&pi(t),t.flags|=1,lt(e,t,r,l),t.child)}function Ru(e,t,n,r,l){if(e===null){var o=n.type;return typeof o=="function"&&!ss(o)&&o.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=o,zu(e,t,o,r,l)):(e=co(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(o=e.child,(e.lanes&l)===0){var a=o.memoizedProps;if(n=n.compare,n=n!==null?n:Wr,n(a,r)&&e.ref===t.ref)return Zt(e,t,l)}return t.flags|=1,e=kn(o,r),e.ref=t.ref,e.return=t,t.child=e}function zu(e,t,n,r,l){if(e!==null){var o=e.memoizedProps;if(Wr(o,r)&&e.ref===t.ref)if(at=!1,t.pendingProps=r=o,(e.lanes&l)!==0)(e.flags&131072)!==0&&(at=!0);else return t.lanes=e.lanes,Zt(e,t,l)}return Bi(e,t,n,r,l)}function Lu(e,t,n){var r=t.pendingProps,l=r.children,o=e!==null?e.memoizedState:null;if(r.mode==="hidden")if((t.mode&1)===0)t.memoizedState={baseLanes:0,cachePool:null,transitions:null},ye(dr,gt),gt|=n;else{if((n&1073741824)===0)return e=o!==null?o.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,ye(dr,gt),gt|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=o!==null?o.baseLanes:n,ye(dr,gt),gt|=r}else o!==null?(r=o.baseLanes|n,t.memoizedState=null):r=n,ye(dr,gt),gt|=r;return lt(e,t,l,n),t.child}function Ou(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Bi(e,t,n,r,l){var o=st(n)?Ln:et.current;return o=tr(t,o),sr(t,l),n=Ri(e,t,n,r,o,l),r=zi(),e!==null&&!at?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Zt(e,t,l)):(je&&r&&pi(t),t.flags|=1,lt(e,t,n,l),t.child)}function Du(e,t,n,r,l){if(st(n)){var o=!0;Ml(t)}else o=!1;if(sr(t,l),t.stateNode===null)bl(e,t),Su(t,n,r),Wi(t,n,r,l),r=!0;else if(e===null){var a=t.stateNode,f=t.memoizedProps;a.props=f;var m=a.context,C=n.contextType;typeof C=="object"&&C!==null?C=St(C):(C=st(n)?Ln:et.current,C=tr(t,C));var L=n.getDerivedStateFromProps,O=typeof L=="function"||typeof a.getSnapshotBeforeUpdate=="function";O||typeof a.UNSAFE_componentWillReceiveProps!="function"&&typeof a.componentWillReceiveProps!="function"||(f!==r||m!==C)&&ju(t,a,r,C),pn=!1;var z=t.memoizedState;a.state=z,Hl(t,r,a,l),m=t.memoizedState,f!==r||z!==m||it.current||pn?(typeof L=="function"&&(Ii(t,n,L,r),m=t.memoizedState),(f=pn||wu(t,n,f,r,z,m,C))?(O||typeof a.UNSAFE_componentWillMount!="function"&&typeof a.componentWillMount!="function"||(typeof a.componentWillMount=="function"&&a.componentWillMount(),typeof a.UNSAFE_componentWillMount=="function"&&a.UNSAFE_componentWillMount()),typeof a.componentDidMount=="function"&&(t.flags|=4194308)):(typeof a.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=m),a.props=r,a.state=m,a.context=C,r=f):(typeof a.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{a=t.stateNode,Xa(e,t),f=t.memoizedProps,C=t.type===t.elementType?f:Rt(t.type,f),a.props=C,O=t.pendingProps,z=a.context,m=n.contextType,typeof m=="object"&&m!==null?m=St(m):(m=st(n)?Ln:et.current,m=tr(t,m));var B=n.getDerivedStateFromProps;(L=typeof B=="function"||typeof a.getSnapshotBeforeUpdate=="function")||typeof a.UNSAFE_componentWillReceiveProps!="function"&&typeof a.componentWillReceiveProps!="function"||(f!==O||z!==m)&&ju(t,a,r,m),pn=!1,z=t.memoizedState,a.state=z,Hl(t,r,a,l);var Q=t.memoizedState;f!==O||z!==Q||it.current||pn?(typeof B=="function"&&(Ii(t,n,B,r),Q=t.memoizedState),(C=pn||wu(t,n,C,r,z,Q,m)||!1)?(L||typeof a.UNSAFE_componentWillUpdate!="function"&&typeof a.componentWillUpdate!="function"||(typeof a.componentWillUpdate=="function"&&a.componentWillUpdate(r,Q,m),typeof a.UNSAFE_componentWillUpdate=="function"&&a.UNSAFE_componentWillUpdate(r,Q,m)),typeof a.componentDidUpdate=="function"&&(t.flags|=4),typeof a.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof a.componentDidUpdate!="function"||f===e.memoizedProps&&z===e.memoizedState||(t.flags|=4),typeof a.getSnapshotBeforeUpdate!="function"||f===e.memoizedProps&&z===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=Q),a.props=r,a.state=Q,a.context=m,r=C):(typeof a.componentDidUpdate!="function"||f===e.memoizedProps&&z===e.memoizedState||(t.flags|=4),typeof a.getSnapshotBeforeUpdate!="function"||f===e.memoizedProps&&z===e.memoizedState||(t.flags|=1024),r=!1)}return Ai(e,t,n,r,o,l)}function Ai(e,t,n,r,l,o){Ou(e,t);var a=(t.flags&128)!==0;if(!r&&!a)return l&&Ua(t,n,!1),Zt(e,t,o);r=t.stateNode,_f.current=t;var f=a&&typeof n.getDerivedStateFromError!="function"?null:r.render();return t.flags|=1,e!==null&&a?(t.child=or(t,e.child,null,o),t.child=or(t,null,f,o)):lt(e,t,f,o),t.memoizedState=r.state,l&&Ua(t,n,!0),t.child}function Mu(e){var t=e.stateNode;t.pendingContext?Wa(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Wa(e,t.context,!1),Ci(e,t.containerInfo)}function Iu(e,t,n,r,l){return lr(),gi(l),t.flags|=256,lt(e,t,n,r),t.child}var $i={dehydrated:null,treeContext:null,retryLane:0};function Vi(e){return{baseLanes:e,cachePool:null,transitions:null}}function Wu(e,t,n){var r=t.pendingProps,l=_e.current,o=!1,a=(t.flags&128)!==0,f;if((f=a)||(f=e!==null&&e.memoizedState===null?!1:(l&2)!==0),f?(o=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(l|=1),ye(_e,l&1),e===null)return vi(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?((t.mode&1)===0?t.lanes=1:e.data==="$!"?t.lanes=8:t.lanes=1073741824,null):(a=r.children,e=r.fallback,o?(r=t.mode,o=t.child,a={mode:"hidden",children:a},(r&1)===0&&o!==null?(o.childLanes=0,o.pendingProps=a):o=fo(a,r,0,null),e=$n(e,r,n,null),o.return=t,e.return=t,o.sibling=e,t.child=o,t.child.memoizedState=Vi(n),t.memoizedState=$i,e):Hi(t,a));if(l=e.memoizedState,l!==null&&(f=l.dehydrated,f!==null))return Nf(e,t,a,r,f,l,n);if(o){o=r.fallback,a=t.mode,l=e.child,f=l.sibling;var m={mode:"hidden",children:r.children};return(a&1)===0&&t.child!==l?(r=t.child,r.childLanes=0,r.pendingProps=m,t.deletions=null):(r=kn(l,m),r.subtreeFlags=l.subtreeFlags&14680064),f!==null?o=kn(f,o):(o=$n(o,a,n,null),o.flags|=2),o.return=t,r.return=t,r.sibling=o,t.child=r,r=o,o=t.child,a=e.child.memoizedState,a=a===null?Vi(n):{baseLanes:a.baseLanes|n,cachePool:null,transitions:a.transitions},o.memoizedState=a,o.childLanes=e.childLanes&~n,t.memoizedState=$i,r}return o=e.child,e=o.sibling,r=kn(o,{mode:"visible",children:r.children}),(t.mode&1)===0&&(r.lanes=n),r.return=t,r.sibling=null,e!==null&&(n=t.deletions,n===null?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=r,t.memoizedState=null,r}function Hi(e,t){return t=fo({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function ql(e,t,n,r){return r!==null&&gi(r),or(t,e.child,null,n),e=Hi(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Nf(e,t,n,r,l,o,a){if(n)return t.flags&256?(t.flags&=-257,r=Fi(Error(s(422))),ql(e,t,a,r)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(o=r.fallback,l=t.mode,r=fo({mode:"visible",children:r.children},l,0,null),o=$n(o,l,a,null),o.flags|=2,r.return=t,o.return=t,r.sibling=o,t.child=r,(t.mode&1)!==0&&or(t,e.child,null,a),t.child.memoizedState=Vi(a),t.memoizedState=$i,o);if((t.mode&1)===0)return ql(e,t,a,null);if(l.data==="$!"){if(r=l.nextSibling&&l.nextSibling.dataset,r)var f=r.dgst;return r=f,o=Error(s(419)),r=Fi(o,r,void 0),ql(e,t,a,r)}if(f=(a&e.childLanes)!==0,at||f){if(r=He,r!==null){switch(a&-a){case 4:l=2;break;case 16:l=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:l=32;break;case 536870912:l=268435456;break;default:l=0}l=(l&(r.suspendedLanes|a))!==0?0:l,l!==0&&l!==o.retryLane&&(o.retryLane=l,Jt(e,l),Ot(r,e,l,-1))}return is(),r=Fi(Error(s(421))),ql(e,t,a,r)}return l.data==="$?"?(t.flags|=128,t.child=e.child,t=Bf.bind(null,e),l._reactRetry=t,null):(e=o.treeContext,vt=un(l.nextSibling),mt=t,je=!0,Tt=null,e!==null&&(kt[wt++]=Yt,kt[wt++]=Gt,kt[wt++]=On,Yt=e.id,Gt=e.overflow,On=t),t=Hi(t,r.children),t.flags|=4096,t)}function Fu(e,t,n){e.lanes|=t;var r=e.alternate;r!==null&&(r.lanes|=t),wi(e.return,t,n)}function Qi(e,t,n,r,l){var o=e.memoizedState;o===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:l}:(o.isBackwards=t,o.rendering=null,o.renderingStartTime=0,o.last=r,o.tail=n,o.tailMode=l)}function Uu(e,t,n){var r=t.pendingProps,l=r.revealOrder,o=r.tail;if(lt(e,t,r.children,n),r=_e.current,(r&2)!==0)r=r&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Fu(e,n,t);else if(e.tag===19)Fu(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(ye(_e,r),(t.mode&1)===0)t.memoizedState=null;else switch(l){case"forwards":for(n=t.child,l=null;n!==null;)e=n.alternate,e!==null&&Ql(e)===null&&(l=n),n=n.sibling;n=l,n===null?(l=t.child,t.child=null):(l=n.sibling,n.sibling=null),Qi(t,!1,l,n,o);break;case"backwards":for(n=null,l=t.child,t.child=null;l!==null;){if(e=l.alternate,e!==null&&Ql(e)===null){t.child=l;break}e=l.sibling,l.sibling=n,n=l,l=e}Qi(t,!0,n,null,o);break;case"together":Qi(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function bl(e,t){(t.mode&1)===0&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function Zt(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),Fn|=t.lanes,(n&t.childLanes)===0)return null;if(e!==null&&t.child!==e.child)throw Error(s(153));if(t.child!==null){for(e=t.child,n=kn(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=kn(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function Pf(e,t,n){switch(t.tag){case 3:Mu(t),lr();break;case 5:ba(t);break;case 1:st(t.type)&&Ml(t);break;case 4:Ci(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,l=t.memoizedProps.value;ye(Al,r._currentValue),r._currentValue=l;break;case 13:if(r=t.memoizedState,r!==null)return r.dehydrated!==null?(ye(_e,_e.current&1),t.flags|=128,null):(n&t.child.childLanes)!==0?Wu(e,t,n):(ye(_e,_e.current&1),e=Zt(e,t,n),e!==null?e.sibling:null);ye(_e,_e.current&1);break;case 19:if(r=(n&t.childLanes)!==0,(e.flags&128)!==0){if(r)return Uu(e,t,n);t.flags|=128}if(l=t.memoizedState,l!==null&&(l.rendering=null,l.tail=null,l.lastEffect=null),ye(_e,_e.current),r)break;return null;case 22:case 23:return t.lanes=0,Lu(e,t,n)}return Zt(e,t,n)}var Bu,Ki,Au,$u;Bu=function(e,t){for(var n=t.child;n!==null;){if(n.tag===5||n.tag===6)e.appendChild(n.stateNode);else if(n.tag!==4&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}},Ki=function(){},Au=function(e,t,n,r){var l=e.memoizedProps;if(l!==r){e=t.stateNode,In(Wt.current);var o=null;switch(n){case"input":l=Vt(e,l),r=Vt(e,r),o=[];break;case"select":l=F({},l,{value:void 0}),r=F({},r,{value:void 0}),o=[];break;case"textarea":l=Pn(e,l),r=Pn(e,r),o=[];break;default:typeof l.onClick!="function"&&typeof r.onClick=="function"&&(e.onclick=Ll)}ie(n,r);var a;n=null;for(C in l)if(!r.hasOwnProperty(C)&&l.hasOwnProperty(C)&&l[C]!=null)if(C==="style"){var f=l[C];for(a in f)f.hasOwnProperty(a)&&(n||(n={}),n[a]="")}else C!=="dangerouslySetInnerHTML"&&C!=="children"&&C!=="suppressContentEditableWarning"&&C!=="suppressHydrationWarning"&&C!=="autoFocus"&&(p.hasOwnProperty(C)?o||(o=[]):(o=o||[]).push(C,null));for(C in r){var m=r[C];if(f=l!=null?l[C]:void 0,r.hasOwnProperty(C)&&m!==f&&(m!=null||f!=null))if(C==="style")if(f){for(a in f)!f.hasOwnProperty(a)||m&&m.hasOwnProperty(a)||(n||(n={}),n[a]="");for(a in m)m.hasOwnProperty(a)&&f[a]!==m[a]&&(n||(n={}),n[a]=m[a])}else n||(o||(o=[]),o.push(C,n)),n=m;else C==="dangerouslySetInnerHTML"?(m=m?m.__html:void 0,f=f?f.__html:void 0,m!=null&&f!==m&&(o=o||[]).push(C,m)):C==="children"?typeof m!="string"&&typeof m!="number"||(o=o||[]).push(C,""+m):C!=="suppressContentEditableWarning"&&C!=="suppressHydrationWarning"&&(p.hasOwnProperty(C)?(m!=null&&C==="onScroll"&&we("scroll",e),o||f===m||(o=[])):(o=o||[]).push(C,m))}n&&(o=o||[]).push("style",n);var C=o;(t.updateQueue=C)&&(t.flags|=4)}},$u=function(e,t,n,r){n!==r&&(t.flags|=4)};function qr(e,t){if(!je)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;n!==null;)n.alternate!==null&&(r=n),n=n.sibling;r===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:r.sibling=null}}function nt(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,r=0;if(t)for(var l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags&14680064,r|=l.flags&14680064,l.return=e,l=l.sibling;else for(l=e.child;l!==null;)n|=l.lanes|l.childLanes,r|=l.subtreeFlags,r|=l.flags,l.return=e,l=l.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Tf(e,t,n){var r=t.pendingProps;switch(hi(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return nt(t),null;case 1:return st(t.type)&&Dl(),nt(t),null;case 3:return r=t.stateNode,ar(),Se(it),Se(et),Ni(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),(e===null||e.child===null)&&(Ul(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,Tt!==null&&(rs(Tt),Tt=null))),Ki(e,t),nt(t),null;case 5:Ei(t);var l=In(Yr.current);if(n=t.type,e!==null&&t.stateNode!=null)Au(e,t,n,r,l),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(t.stateNode===null)throw Error(s(166));return nt(t),null}if(e=In(Wt.current),Ul(t)){r=t.stateNode,n=t.type;var o=t.memoizedProps;switch(r[It]=t,r[$r]=o,e=(t.mode&1)!==0,n){case"dialog":we("cancel",r),we("close",r);break;case"iframe":case"object":case"embed":we("load",r);break;case"video":case"audio":for(l=0;l<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[It]=t,e[$r]=r,Bu(e,t,!1,!1),t.stateNode=e;e:{switch(a=Me(n,r),n){case"dialog":we("cancel",e),we("close",e),l=r;break;case"iframe":case"object":case"embed":we("load",e),l=r;break;case"video":case"audio":for(l=0;lfr&&(t.flags|=128,r=!0,qr(o,!1),t.lanes=4194304)}else{if(!r)if(e=Ql(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),qr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!a.alternate&&!je)return nt(t),null}else 2*ze()-o.renderingStartTime>fr&&n!==1073741824&&(t.flags|=128,r=!0,qr(o,!1),t.lanes=4194304);o.isBackwards?(a.sibling=t.child,t.child=a):(n=o.last,n!==null?n.sibling=a:t.child=a,o.last=a)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=ze(),t.sibling=null,n=_e.current,ye(_e,r?n&1|2:n&1),t):(nt(t),null);case 22:case 23:return os(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(gt&1073741824)!==0&&(nt(t),t.subtreeFlags&6&&(t.flags|=8192)):nt(t),null;case 24:return null;case 25:return null}throw Error(s(156,t.tag))}function Rf(e,t){switch(hi(t),t.tag){case 1:return st(t.type)&&Dl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ar(),Se(it),Se(et),Ni(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Ei(t),null;case 13:if(Se(_e),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));lr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Se(_e),null;case 4:return ar(),null;case 10:return ki(t.type._context),null;case 22:case 23:return os(),null;case 24:return null;default:return null}}var eo=!1,rt=!1,zf=typeof WeakSet=="function"?WeakSet:Set,V=null;function cr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Re(e,t,r)}else n.current=null}function Yi(e,t,n){try{n()}catch(r){Re(e,t,r)}}var Vu=!1;function Lf(e,t){if(oi=kl,e=wa(),Zo(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var a=0,f=-1,m=-1,C=0,L=0,O=e,z=null;t:for(;;){for(var B;O!==n||l!==0&&O.nodeType!==3||(f=a+l),O!==o||r!==0&&O.nodeType!==3||(m=a+r),O.nodeType===3&&(a+=O.nodeValue.length),(B=O.firstChild)!==null;)z=O,O=B;for(;;){if(O===e)break t;if(z===n&&++C===l&&(f=a),z===o&&++L===r&&(m=a),(B=O.nextSibling)!==null)break;O=z,z=O.parentNode}O=B}n=f===-1||m===-1?null:{start:f,end:m}}else n=null}n=n||{start:0,end:0}}else n=null;for(ii={focusedElem:e,selectionRange:n},kl=!1,V=t;V!==null;)if(t=V,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,V=e;else for(;V!==null;){t=V;try{var Q=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(Q!==null){var Y=Q.memoizedProps,Le=Q.memoizedState,x=t.stateNode,v=x.getSnapshotBeforeUpdate(t.elementType===t.type?Y:Rt(t.type,Y),Le);x.__reactInternalSnapshotBeforeUpdate=v}break;case 3:var w=t.stateNode.containerInfo;w.nodeType===1?w.textContent="":w.nodeType===9&&w.documentElement&&w.removeChild(w.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(s(163))}}catch(D){Re(t,t.return,D)}if(e=t.sibling,e!==null){e.return=t.return,V=e;break}V=t.return}return Q=Vu,Vu=!1,Q}function br(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&Yi(t,n,o)}l=l.next}while(l!==r)}}function to(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Gi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Hu(e){var t=e.alternate;t!==null&&(e.alternate=null,Hu(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[It],delete t[$r],delete t[ci],delete t[hf],delete t[mf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Qu(e){return e.tag===5||e.tag===3||e.tag===4}function Ku(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Qu(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ji(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Ll));else if(r!==4&&(e=e.child,e!==null))for(Ji(e,t,n),e=e.sibling;e!==null;)Ji(e,t,n),e=e.sibling}function Xi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Xi(e,t,n),e=e.sibling;e!==null;)Xi(e,t,n),e=e.sibling}var Ge=null,zt=!1;function mn(e,t,n){for(n=n.child;n!==null;)Yu(e,t,n),n=n.sibling}function Yu(e,t,n){if(Mt&&typeof Mt.onCommitFiberUnmount=="function")try{Mt.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:rt||cr(n,t);case 6:var r=Ge,l=zt;Ge=null,mn(e,t,n),Ge=r,zt=l,Ge!==null&&(zt?(e=Ge,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Ge.removeChild(n.stateNode));break;case 18:Ge!==null&&(zt?(e=Ge,n=n.stateNode,e.nodeType===8?ui(e.parentNode,n):e.nodeType===1&&ui(e,n),zr(e)):ui(Ge,n.stateNode));break;case 4:r=Ge,l=zt,Ge=n.stateNode.containerInfo,zt=!0,mn(e,t,n),Ge=r,zt=l;break;case 0:case 11:case 14:case 15:if(!rt&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,a=o.destroy;o=o.tag,a!==void 0&&((o&2)!==0||(o&4)!==0)&&Yi(n,t,a),l=l.next}while(l!==r)}mn(e,t,n);break;case 1:if(!rt&&(cr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(f){Re(n,t,f)}mn(e,t,n);break;case 21:mn(e,t,n);break;case 22:n.mode&1?(rt=(r=rt)||n.memoizedState!==null,mn(e,t,n),rt=r):mn(e,t,n);break;default:mn(e,t,n)}}function Gu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new zf),t.forEach(function(r){var l=Af.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function Lt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=a),r&=~o}if(r=l,r=ze()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Df(r/1960))-r,10e?16:e,gn===null)var r=!1;else{if(e=gn,gn=null,io=0,(he&6)!==0)throw Error(s(331));var l=he;for(he|=4,V=e.current;V!==null;){var o=V,a=o.child;if((V.flags&16)!==0){var f=o.deletions;if(f!==null){for(var m=0;mze()-bi?Bn(e,0):qi|=n),ct(e,t)}function sc(e,t){t===0&&((e.mode&1)===0?t=1:(t=vl,vl<<=1,(vl&130023424)===0&&(vl=4194304)));var n=ot();e=Jt(e,t),e!==null&&(_r(e,t,n),ct(e,n))}function Bf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),sc(e,n)}function Af(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(s(314))}r!==null&&r.delete(t),sc(e,n)}var ac;ac=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||it.current)at=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return at=!1,Pf(e,t,n);at=(e.flags&131072)!==0}else at=!1,je&&(t.flags&1048576)!==0&&Aa(t,Fl,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;bl(e,t),e=t.pendingProps;var l=tr(t,et.current);sr(t,n),l=Ri(null,t,r,e,l,n);var o=zi();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,st(r)?(o=!0,Ml(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,ji(t),l.updater=Zl,t.stateNode=l,l._reactInternals=t,Wi(t,r,e,n),t=Ai(null,t,r,!0,o,n)):(t.tag=0,je&&o&&pi(t),lt(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(bl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=Vf(r),e=Rt(r,e),l){case 0:t=Bi(null,t,r,e,n);break e;case 1:t=Du(null,t,r,e,n);break e;case 11:t=Tu(null,t,r,e,n);break e;case 14:t=Ru(null,t,r,Rt(r.type,e),n);break e}throw Error(s(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Bi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Du(e,t,r,l,n);case 3:e:{if(Mu(t),e===null)throw Error(s(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Xa(e,t),Hl(t,r,null,n);var a=t.memoizedState;if(r=a.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=ur(Error(s(423)),t),t=Iu(e,t,r,n,l);break e}else if(r!==l){l=ur(Error(s(424)),t),t=Iu(e,t,r,n,l);break e}else for(vt=un(t.stateNode.containerInfo.firstChild),mt=t,je=!0,Tt=null,n=Ga(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(lr(),r===l){t=Zt(e,t,n);break e}lt(e,t,r,n)}t=t.child}return t;case 5:return ba(t),e===null&&vi(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,a=l.children,si(r,l)?a=null:o!==null&&si(r,o)&&(t.flags|=32),Ou(e,t),lt(e,t,a,n),t.child;case 6:return e===null&&vi(t),null;case 13:return Wu(e,t,n);case 4:return Ci(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=or(t,null,r,n):lt(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),Tu(e,t,r,l,n);case 7:return lt(e,t,t.pendingProps,n),t.child;case 8:return lt(e,t,t.pendingProps.children,n),t.child;case 12:return lt(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,a=l.value,ye(Al,r._currentValue),r._currentValue=a,o!==null)if(Pt(o.value,a)){if(o.children===l.children&&!it.current){t=Zt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var f=o.dependencies;if(f!==null){a=o.child;for(var m=f.firstContext;m!==null;){if(m.context===r){if(o.tag===1){m=Xt(-1,n&-n),m.tag=2;var C=o.updateQueue;if(C!==null){C=C.shared;var L=C.pending;L===null?m.next=m:(m.next=L.next,L.next=m),C.pending=m}}o.lanes|=n,m=o.alternate,m!==null&&(m.lanes|=n),wi(o.return,n,t),f.lanes|=n;break}m=m.next}}else if(o.tag===10)a=o.type===t.type?null:o.child;else if(o.tag===18){if(a=o.return,a===null)throw Error(s(341));a.lanes|=n,f=a.alternate,f!==null&&(f.lanes|=n),wi(a,n,t),a=o.sibling}else a=o.child;if(a!==null)a.return=o;else for(a=o;a!==null;){if(a===t){a=null;break}if(o=a.sibling,o!==null){o.return=a.return,a=o;break}a=a.return}o=a}lt(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,sr(t,n),l=St(l),r=r(l),t.flags|=1,lt(e,t,r,n),t.child;case 14:return r=t.type,l=Rt(r,t.pendingProps),l=Rt(r.type,l),Ru(e,t,r,l,n);case 15:return zu(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:Rt(r,l),bl(e,t),t.tag=1,st(r)?(e=!0,Ml(t)):e=!1,sr(t,n),Su(t,r,l),Wi(t,r,l,n),Ai(null,t,r,!0,e,n);case 19:return Uu(e,t,n);case 22:return Lu(e,t,n)}throw Error(s(156,t.tag))};function uc(e,t){return $s(e,t)}function $f(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Et(e,t,n,r){return new $f(e,t,n,r)}function ss(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vf(e){if(typeof e=="function")return ss(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ee)return 11;if(e===be)return 14}return 2}function kn(e,t){var n=e.alternate;return n===null?(n=Et(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function co(e,t,n,r,l,o){var a=2;if(r=e,typeof e=="function")ss(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case Ce:return $n(n.children,l,o,t);case xe:a=8,l|=8;break;case qe:return e=Et(12,n,t,l|2),e.elementType=qe,e.lanes=o,e;case Te:return e=Et(13,n,t,l),e.elementType=Te,e.lanes=o,e;case Ae:return e=Et(19,n,t,l),e.elementType=Ae,e.lanes=o,e;case ve:return fo(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case We:a=10;break e;case Ke:a=9;break e;case Ee:a=11;break e;case be:a=14;break e;case Oe:a=16,r=null;break e}throw Error(s(130,e==null?e:typeof e,""))}return t=Et(a,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function $n(e,t,n,r){return e=Et(7,e,r,t),e.lanes=n,e}function fo(e,t,n,r){return e=Et(22,e,r,t),e.elementType=ve,e.lanes=n,e.stateNode={isHidden:!1},e}function as(e,t,n){return e=Et(6,e,null,t),e.lanes=n,e}function us(e,t,n){return t=Et(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Hf(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Io(0),this.expirationTimes=Io(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Io(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function cs(e,t,n,r,l,o,a,f,m){return e=new Hf(e,t,n,f,m),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Et(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ji(o),e}function Qf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(i)}catch(u){console.error(u)}}return i(),vs.exports=ip(),vs.exports}var jc;function sp(){if(jc)return xo;jc=1;var i=Bc();return xo.createRoot=i.createRoot,xo.hydrateRoot=i.hydrateRoot,xo}var ap=sp();Bc();/** * @remix-run/router v1.23.3 * * Copyright (c) Remix Software Inc. @@ -46,7 +46,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function al(){return al=Object.assign?Object.assign.bind():function(i){for(var u=1;u"u")throw new Error(u)}function Ts(i,u){if(!i){typeof console<"u"&&console.warn(u);try{throw new Error(u)}catch{}}}function cp(){return Math.random().toString(36).substr(2,8)}function Ec(i,u){return{usr:i.state,key:i.key,idx:u}}function js(i,u,s,d){return s===void 0&&(s=null),al({pathname:typeof i=="string"?i:i.pathname,search:"",hash:""},typeof u=="string"?yr(u):u,{state:s,key:u&&u.key||d||cp()})}function Co(i){let{pathname:u="/",search:s="",hash:d=""}=i;return s&&s!=="?"&&(u+=s.charAt(0)==="?"?s:"?"+s),d&&d!=="#"&&(u+=d.charAt(0)==="#"?d:"#"+d),u}function yr(i){let u={};if(i){let s=i.indexOf("#");s>=0&&(u.hash=i.substr(s),i=i.substr(0,s));let d=i.indexOf("?");d>=0&&(u.search=i.substr(d),i=i.substr(0,d)),i&&(u.pathname=i)}return u}function dp(i,u,s,d){d===void 0&&(d={});let{window:p=document.defaultView,v5Compat:h=!1}=d,g=p.history,j=Sn.Pop,w=null,_=P();_==null&&(_=0,g.replaceState(al({},g.state,{idx:_}),""));function P(){return(g.state||{idx:null}).idx}function N(){j=Sn.Pop;let E=P(),A=E==null?null:E-_;_=E,w&&w({action:j,location:T.location,delta:A})}function M(E,A){j=Sn.Push;let $=js(T.location,E,A);_=P()+1;let G=Ec($,_),U=T.createHref($);try{g.pushState(G,"",U)}catch(ce){if(ce instanceof DOMException&&ce.name==="DataCloneError")throw ce;p.location.assign(U)}h&&w&&w({action:j,location:T.location,delta:1})}function H(E,A){j=Sn.Replace;let $=js(T.location,E,A);_=P();let G=Ec($,_),U=T.createHref($);g.replaceState(G,"",U),h&&w&&w({action:j,location:T.location,delta:0})}function K(E){let A=p.location.origin!=="null"?p.location.origin:p.location.href,$=typeof E=="string"?E:Co(E);return $=$.replace(/ $/,"%20"),Pe(A,"No window.location.(origin|href) available to create URL for href: "+$),new URL($,A)}let T={get action(){return j},get location(){return i(p,g)},listen(E){if(w)throw new Error("A history only accepts one active listener");return p.addEventListener(Cc,N),w=E,()=>{p.removeEventListener(Cc,N),w=null}},createHref(E){return u(p,E)},createURL:K,encodeLocation(E){let A=K(E);return{pathname:A.pathname,search:A.search,hash:A.hash}},push:M,replace:H,go(E){return g.go(E)}};return T}var _c;(function(i){i.data="data",i.deferred="deferred",i.redirect="redirect",i.error="error"})(_c||(_c={}));function fp(i,u,s){return s===void 0&&(s="/"),pp(i,u,s)}function pp(i,u,s,d){let p=typeof u=="string"?yr(u):u,h=vr(p.pathname||"/",s);if(h==null)return null;let g=Ac(i);hp(g);let j=null,w=Ep(h);for(let _=0;j==null&&_{let w={relativePath:j===void 0?h.path||"":j,caseSensitive:h.caseSensitive===!0,childrenIndex:g,route:h};w.relativePath.startsWith("/")&&(Pe(w.relativePath.startsWith(d),'Absolute route path "'+w.relativePath+'" nested under path '+('"'+d+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),w.relativePath=w.relativePath.slice(d.length));let _=jn([d,w.relativePath]),P=s.concat(w);h.children&&h.children.length>0&&(Pe(h.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+_+'".')),Ac(h.children,u,P,_)),!(h.path==null&&!h.index)&&u.push({path:_,score:kp(_,h.index),routesMeta:P})};return i.forEach((h,g)=>{var j;if(h.path===""||!((j=h.path)!=null&&j.includes("?")))p(h,g);else for(let w of $c(h.path))p(h,g,w)}),u}function $c(i){let u=i.split("/");if(u.length===0)return[];let[s,...d]=u,p=s.endsWith("?"),h=s.replace(/\?$/,"");if(d.length===0)return p?[h,""]:[h];let g=$c(d.join("/")),j=[];return j.push(...g.map(w=>w===""?h:[h,w].join("/"))),p&&j.push(...g),j.map(w=>i.startsWith("/")&&w===""?"/":w)}function hp(i){i.sort((u,s)=>u.score!==s.score?s.score-u.score:Sp(u.routesMeta.map(d=>d.childrenIndex),s.routesMeta.map(d=>d.childrenIndex)))}const mp=/^:[\w-]+$/,vp=3,gp=2,yp=1,xp=10,wp=-2,Nc=i=>i==="*";function kp(i,u){let s=i.split("/"),d=s.length;return s.some(Nc)&&(d+=wp),u&&(d+=gp),s.filter(p=>!Nc(p)).reduce((p,h)=>p+(mp.test(h)?vp:h===""?yp:xp),d)}function Sp(i,u){return i.length===u.length&&i.slice(0,-1).every((d,p)=>d===u[p])?i[i.length-1]-u[u.length-1]:0}function jp(i,u,s){let{routesMeta:d}=i,p={},h="/",g=[];for(let j=0;j{let{paramName:M,isOptional:H}=P;if(M==="*"){let T=j[N]||"";g=h.slice(0,h.length-T.length).replace(/(.)\/+$/,"$1")}const K=j[N];return H&&!K?_[M]=void 0:_[M]=(K||"").replace(/%2F/g,"/"),_},{}),pathname:h,pathnameBase:g,pattern:i}}function Cp(i,u,s){u===void 0&&(u=!1),s===void 0&&(s=!0),Ts(i==="*"||!i.endsWith("*")||i.endsWith("/*"),'Route path "'+i+'" will be treated as if it were '+('"'+i.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+i.replace(/\*$/,"/*")+'".'));let d=[],p="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(g,j,w)=>(d.push({paramName:j,isOptional:w!=null}),w?"/?([^\\/]+)?":"/([^\\/]+)"));return i.endsWith("*")?(d.push({paramName:"*"}),p+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?p+="\\/*$":i!==""&&i!=="/"&&(p+="(?:(?=\\/|$))"),[new RegExp(p,u?void 0:"i"),d]}function Ep(i){try{return i.split("/").map(u=>decodeURIComponent(u).replace(/\//g,"%2F")).join("/")}catch(u){return Ts(!1,'The URL path "'+i+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+u+").")),i}}function vr(i,u){if(u==="/")return i;if(!i.toLowerCase().startsWith(u.toLowerCase()))return null;let s=u.endsWith("/")?u.length-1:u.length,d=i.charAt(s);return d&&d!=="/"?null:i.slice(s)||"/"}const _p=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Np=i=>_p.test(i);function Pp(i,u){u===void 0&&(u="/");let{pathname:s,search:d="",hash:p=""}=typeof i=="string"?yr(i):i,h;if(s)if(Np(s))h=s;else{if(s.includes("//")){let g=s;s=Vc(s),Ts(!1,"Pathnames cannot have embedded double slashes - normalizing "+(g+" -> "+s))}s.startsWith("/")?h=Pc(s.substring(1),"/"):h=Pc(s,u)}else h=u;return{pathname:h,search:zp(d),hash:Lp(p)}}function Pc(i,u){let s=u.replace(/\/+$/,"").split("/");return i.split("/").forEach(p=>{p===".."?s.length>1&&s.pop():p!=="."&&s.push(p)}),s.length>1?s.join("/"):"/"}function xs(i,u,s,d){return"Cannot include a '"+i+"' character in a manually specified "+("`to."+u+"` field ["+JSON.stringify(d)+"]. Please separate it out to the ")+("`to."+s+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function Tp(i){return i.filter((u,s)=>s===0||u.route.path&&u.route.path.length>0)}function Rs(i,u){let s=Tp(i);return u?s.map((d,p)=>p===s.length-1?d.pathname:d.pathnameBase):s.map(d=>d.pathnameBase)}function zs(i,u,s,d){d===void 0&&(d=!1);let p;typeof i=="string"?p=yr(i):(p=al({},i),Pe(!p.pathname||!p.pathname.includes("?"),xs("?","pathname","search",p)),Pe(!p.pathname||!p.pathname.includes("#"),xs("#","pathname","hash",p)),Pe(!p.search||!p.search.includes("#"),xs("#","search","hash",p)));let h=i===""||p.pathname==="",g=h?"/":p.pathname,j;if(g==null)j=s;else{let N=u.length-1;if(!d&&g.startsWith("..")){let M=g.split("/");for(;M[0]==="..";)M.shift(),N-=1;p.pathname=M.join("/")}j=N>=0?u[N]:"/"}let w=Pp(p,j),_=g&&g!=="/"&&g.endsWith("/"),P=(h||g===".")&&s.endsWith("/");return!w.pathname.endsWith("/")&&(_||P)&&(w.pathname+="/"),w}const Vc=i=>i.replace(/\/\/+/g,"/"),jn=i=>Vc(i.join("/")),Rp=i=>i.replace(/\/+$/,"").replace(/^\/*/,"/"),zp=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,Lp=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i;function Op(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}const Hc=["post","put","patch","delete"];new Set(Hc);const Dp=["get",...Hc];new Set(Dp);/** + */function al(){return al=Object.assign?Object.assign.bind():function(i){for(var u=1;u"u")throw new Error(u)}function Ts(i,u){if(!i){typeof console<"u"&&console.warn(u);try{throw new Error(u)}catch{}}}function cp(){return Math.random().toString(36).substr(2,8)}function Ec(i,u){return{usr:i.state,key:i.key,idx:u}}function js(i,u,s,d){return s===void 0&&(s=null),al({pathname:typeof i=="string"?i:i.pathname,search:"",hash:""},typeof u=="string"?yr(u):u,{state:s,key:u&&u.key||d||cp()})}function Co(i){let{pathname:u="/",search:s="",hash:d=""}=i;return s&&s!=="?"&&(u+=s.charAt(0)==="?"?s:"?"+s),d&&d!=="#"&&(u+=d.charAt(0)==="#"?d:"#"+d),u}function yr(i){let u={};if(i){let s=i.indexOf("#");s>=0&&(u.hash=i.substr(s),i=i.substr(0,s));let d=i.indexOf("?");d>=0&&(u.search=i.substr(d),i=i.substr(0,d)),i&&(u.pathname=i)}return u}function dp(i,u,s,d){d===void 0&&(d={});let{window:p=document.defaultView,v5Compat:h=!1}=d,g=p.history,j=Sn.Pop,k=null,_=P();_==null&&(_=0,g.replaceState(al({},g.state,{idx:_}),""));function P(){return(g.state||{idx:null}).idx}function N(){j=Sn.Pop;let E=P(),A=E==null?null:E-_;_=E,k&&k({action:j,location:T.location,delta:A})}function M(E,A){j=Sn.Push;let $=js(T.location,E,A);_=P()+1;let J=Ec($,_),U=T.createHref($);try{g.pushState(J,"",U)}catch(ce){if(ce instanceof DOMException&&ce.name==="DataCloneError")throw ce;p.location.assign(U)}h&&k&&k({action:j,location:T.location,delta:1})}function H(E,A){j=Sn.Replace;let $=js(T.location,E,A);_=P();let J=Ec($,_),U=T.createHref($);g.replaceState(J,"",U),h&&k&&k({action:j,location:T.location,delta:0})}function K(E){let A=p.location.origin!=="null"?p.location.origin:p.location.href,$=typeof E=="string"?E:Co(E);return $=$.replace(/ $/,"%20"),Pe(A,"No window.location.(origin|href) available to create URL for href: "+$),new URL($,A)}let T={get action(){return j},get location(){return i(p,g)},listen(E){if(k)throw new Error("A history only accepts one active listener");return p.addEventListener(Cc,N),k=E,()=>{p.removeEventListener(Cc,N),k=null}},createHref(E){return u(p,E)},createURL:K,encodeLocation(E){let A=K(E);return{pathname:A.pathname,search:A.search,hash:A.hash}},push:M,replace:H,go(E){return g.go(E)}};return T}var _c;(function(i){i.data="data",i.deferred="deferred",i.redirect="redirect",i.error="error"})(_c||(_c={}));function fp(i,u,s){return s===void 0&&(s="/"),pp(i,u,s)}function pp(i,u,s,d){let p=typeof u=="string"?yr(u):u,h=vr(p.pathname||"/",s);if(h==null)return null;let g=Ac(i);hp(g);let j=null,k=Ep(h);for(let _=0;j==null&&_{let k={relativePath:j===void 0?h.path||"":j,caseSensitive:h.caseSensitive===!0,childrenIndex:g,route:h};k.relativePath.startsWith("/")&&(Pe(k.relativePath.startsWith(d),'Absolute route path "'+k.relativePath+'" nested under path '+('"'+d+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),k.relativePath=k.relativePath.slice(d.length));let _=jn([d,k.relativePath]),P=s.concat(k);h.children&&h.children.length>0&&(Pe(h.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+_+'".')),Ac(h.children,u,P,_)),!(h.path==null&&!h.index)&&u.push({path:_,score:wp(_,h.index),routesMeta:P})};return i.forEach((h,g)=>{var j;if(h.path===""||!((j=h.path)!=null&&j.includes("?")))p(h,g);else for(let k of $c(h.path))p(h,g,k)}),u}function $c(i){let u=i.split("/");if(u.length===0)return[];let[s,...d]=u,p=s.endsWith("?"),h=s.replace(/\?$/,"");if(d.length===0)return p?[h,""]:[h];let g=$c(d.join("/")),j=[];return j.push(...g.map(k=>k===""?h:[h,k].join("/"))),p&&j.push(...g),j.map(k=>i.startsWith("/")&&k===""?"/":k)}function hp(i){i.sort((u,s)=>u.score!==s.score?s.score-u.score:Sp(u.routesMeta.map(d=>d.childrenIndex),s.routesMeta.map(d=>d.childrenIndex)))}const mp=/^:[\w-]+$/,vp=3,gp=2,yp=1,xp=10,kp=-2,Nc=i=>i==="*";function wp(i,u){let s=i.split("/"),d=s.length;return s.some(Nc)&&(d+=kp),u&&(d+=gp),s.filter(p=>!Nc(p)).reduce((p,h)=>p+(mp.test(h)?vp:h===""?yp:xp),d)}function Sp(i,u){return i.length===u.length&&i.slice(0,-1).every((d,p)=>d===u[p])?i[i.length-1]-u[u.length-1]:0}function jp(i,u,s){let{routesMeta:d}=i,p={},h="/",g=[];for(let j=0;j{let{paramName:M,isOptional:H}=P;if(M==="*"){let T=j[N]||"";g=h.slice(0,h.length-T.length).replace(/(.)\/+$/,"$1")}const K=j[N];return H&&!K?_[M]=void 0:_[M]=(K||"").replace(/%2F/g,"/"),_},{}),pathname:h,pathnameBase:g,pattern:i}}function Cp(i,u,s){u===void 0&&(u=!1),s===void 0&&(s=!0),Ts(i==="*"||!i.endsWith("*")||i.endsWith("/*"),'Route path "'+i+'" will be treated as if it were '+('"'+i.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+i.replace(/\*$/,"/*")+'".'));let d=[],p="^"+i.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(g,j,k)=>(d.push({paramName:j,isOptional:k!=null}),k?"/?([^\\/]+)?":"/([^\\/]+)"));return i.endsWith("*")?(d.push({paramName:"*"}),p+=i==="*"||i==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?p+="\\/*$":i!==""&&i!=="/"&&(p+="(?:(?=\\/|$))"),[new RegExp(p,u?void 0:"i"),d]}function Ep(i){try{return i.split("/").map(u=>decodeURIComponent(u).replace(/\//g,"%2F")).join("/")}catch(u){return Ts(!1,'The URL path "'+i+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+u+").")),i}}function vr(i,u){if(u==="/")return i;if(!i.toLowerCase().startsWith(u.toLowerCase()))return null;let s=u.endsWith("/")?u.length-1:u.length,d=i.charAt(s);return d&&d!=="/"?null:i.slice(s)||"/"}const _p=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Np=i=>_p.test(i);function Pp(i,u){u===void 0&&(u="/");let{pathname:s,search:d="",hash:p=""}=typeof i=="string"?yr(i):i,h;if(s)if(Np(s))h=s;else{if(s.includes("//")){let g=s;s=Vc(s),Ts(!1,"Pathnames cannot have embedded double slashes - normalizing "+(g+" -> "+s))}s.startsWith("/")?h=Pc(s.substring(1),"/"):h=Pc(s,u)}else h=u;return{pathname:h,search:zp(d),hash:Lp(p)}}function Pc(i,u){let s=u.replace(/\/+$/,"").split("/");return i.split("/").forEach(p=>{p===".."?s.length>1&&s.pop():p!=="."&&s.push(p)}),s.length>1?s.join("/"):"/"}function xs(i,u,s,d){return"Cannot include a '"+i+"' character in a manually specified "+("`to."+u+"` field ["+JSON.stringify(d)+"]. Please separate it out to the ")+("`to."+s+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function Tp(i){return i.filter((u,s)=>s===0||u.route.path&&u.route.path.length>0)}function Rs(i,u){let s=Tp(i);return u?s.map((d,p)=>p===s.length-1?d.pathname:d.pathnameBase):s.map(d=>d.pathnameBase)}function zs(i,u,s,d){d===void 0&&(d=!1);let p;typeof i=="string"?p=yr(i):(p=al({},i),Pe(!p.pathname||!p.pathname.includes("?"),xs("?","pathname","search",p)),Pe(!p.pathname||!p.pathname.includes("#"),xs("#","pathname","hash",p)),Pe(!p.search||!p.search.includes("#"),xs("#","search","hash",p)));let h=i===""||p.pathname==="",g=h?"/":p.pathname,j;if(g==null)j=s;else{let N=u.length-1;if(!d&&g.startsWith("..")){let M=g.split("/");for(;M[0]==="..";)M.shift(),N-=1;p.pathname=M.join("/")}j=N>=0?u[N]:"/"}let k=Pp(p,j),_=g&&g!=="/"&&g.endsWith("/"),P=(h||g===".")&&s.endsWith("/");return!k.pathname.endsWith("/")&&(_||P)&&(k.pathname+="/"),k}const Vc=i=>i.replace(/\/\/+/g,"/"),jn=i=>Vc(i.join("/")),Rp=i=>i.replace(/\/+$/,"").replace(/^\/*/,"/"),zp=i=>!i||i==="?"?"":i.startsWith("?")?i:"?"+i,Lp=i=>!i||i==="#"?"":i.startsWith("#")?i:"#"+i;function Op(i){return i!=null&&typeof i.status=="number"&&typeof i.statusText=="string"&&typeof i.internal=="boolean"&&"data"in i}const Hc=["post","put","patch","delete"];new Set(Hc);const Dp=["get",...Hc];new Set(Dp);/** * React Router v6.30.4 * * Copyright (c) Remix Software Inc. @@ -55,7 +55,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function ul(){return ul=Object.assign?Object.assign.bind():function(i){for(var u=1;u{j.current=!0}),S.useCallback(function(_,P){if(P===void 0&&(P={}),!j.current)return;if(typeof _=="number"){d.go(_);return}let N=zs(_,JSON.parse(g),h,P.relative==="path");i==null&&u!=="/"&&(N.pathname=N.pathname==="/"?u:jn([u,N.pathname])),(P.replace?d.replace:d.push)(N,P.state,P)},[u,d,g,h,i])}function Po(i,u){let{relative:s}=u===void 0?{}:u,{future:d}=S.useContext(bt),{matches:p}=S.useContext(Cn),{pathname:h}=wr(),g=JSON.stringify(Rs(p,d.v7_relativeSplatPath));return S.useMemo(()=>zs(i,JSON.parse(g),h,s==="path"),[i,g,h,s])}function Wp(i,u){return Fp(i,u)}function Fp(i,u,s,d){xr()||Pe(!1);let{navigator:p}=S.useContext(bt),{matches:h}=S.useContext(Cn),g=h[h.length-1],j=g?g.params:{};g&&g.pathname;let w=g?g.pathnameBase:"/";g&&g.route;let _=wr(),P;if(u){var N;let E=typeof u=="string"?yr(u):u;w==="/"||(N=E.pathname)!=null&&N.startsWith(w)||Pe(!1),P=E}else P=_;let M=P.pathname||"/",H=M;if(w!=="/"){let E=w.replace(/^\//,"").split("/");H="/"+M.replace(/^\//,"").split("/").slice(E.length).join("/")}let K=fp(i,{pathname:H}),T=Vp(K&&K.map(E=>Object.assign({},E,{params:Object.assign({},j,E.params),pathname:jn([w,p.encodeLocation?p.encodeLocation(E.pathname).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?w:jn([w,p.encodeLocation?p.encodeLocation(E.pathnameBase).pathname:E.pathnameBase])})),h,s,d);return u&&T?S.createElement(No.Provider,{value:{location:ul({pathname:"/",search:"",hash:"",state:null,key:"default"},P),navigationType:Sn.Pop}},T):T}function Up(){let i=Yp(),u=Op(i)?i.status+" "+i.statusText:i instanceof Error?i.message:JSON.stringify(i),s=i instanceof Error?i.stack:null,p={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return S.createElement(S.Fragment,null,S.createElement("h2",null,"Unexpected Application Error!"),S.createElement("h3",{style:{fontStyle:"italic"}},u),s?S.createElement("pre",{style:p},s):null,null)}const Bp=S.createElement(Up,null);class Ap extends S.Component{constructor(u){super(u),this.state={location:u.location,revalidation:u.revalidation,error:u.error}}static getDerivedStateFromError(u){return{error:u}}static getDerivedStateFromProps(u,s){return s.location!==u.location||s.revalidation!=="idle"&&u.revalidation==="idle"?{error:u.error,location:u.location,revalidation:u.revalidation}:{error:u.error!==void 0?u.error:s.error,location:s.location,revalidation:u.revalidation||s.revalidation}}componentDidCatch(u,s){console.error("React Router caught the following error during render",u,s)}render(){return this.state.error!==void 0?S.createElement(Cn.Provider,{value:this.props.routeContext},S.createElement(Kc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function $p(i){let{routeContext:u,match:s,children:d}=i,p=S.useContext(_o);return p&&p.static&&p.staticContext&&(s.route.errorElement||s.route.ErrorBoundary)&&(p.staticContext._deepestRenderedBoundaryId=s.route.id),S.createElement(Cn.Provider,{value:u},d)}function Vp(i,u,s,d){var p;if(u===void 0&&(u=[]),s===void 0&&(s=null),d===void 0&&(d=null),i==null){var h;if(!s)return null;if(s.errors)i=s.matches;else if((h=d)!=null&&h.v7_partialHydration&&u.length===0&&!s.initialized&&s.matches.length>0)i=s.matches;else return null}let g=i,j=(p=s)==null?void 0:p.errors;if(j!=null){let P=g.findIndex(N=>N.route.id&&(j==null?void 0:j[N.route.id])!==void 0);P>=0||Pe(!1),g=g.slice(0,Math.min(g.length,P+1))}let w=!1,_=-1;if(s&&d&&d.v7_partialHydration)for(let P=0;P=0?g=g.slice(0,_+1):g=[g[0]];break}}}return g.reduceRight((P,N,M)=>{let H,K=!1,T=null,E=null;s&&(H=j&&N.route.id?j[N.route.id]:void 0,T=N.route.errorElement||Bp,w&&(_<0&&M===0?(Gp("route-fallback"),K=!0,E=null):_===M&&(K=!0,E=N.route.hydrateFallbackElement||null)));let A=u.concat(g.slice(0,M+1)),$=()=>{let G;return H?G=T:K?G=E:N.route.Component?G=S.createElement(N.route.Component,null):N.route.element?G=N.route.element:G=P,S.createElement($p,{match:N,routeContext:{outlet:P,matches:A,isDataRoute:s!=null},children:G})};return s&&(N.route.ErrorBoundary||N.route.errorElement||M===0)?S.createElement(Ap,{location:s.location,revalidation:s.revalidation,component:T,error:H,children:$(),routeContext:{outlet:null,matches:A,isDataRoute:!0}}):$()},null)}var Gc=(function(i){return i.UseBlocker="useBlocker",i.UseRevalidator="useRevalidator",i.UseNavigateStable="useNavigate",i})(Gc||{}),Xc=(function(i){return i.UseBlocker="useBlocker",i.UseLoaderData="useLoaderData",i.UseActionData="useActionData",i.UseRouteError="useRouteError",i.UseNavigation="useNavigation",i.UseRouteLoaderData="useRouteLoaderData",i.UseMatches="useMatches",i.UseRevalidator="useRevalidator",i.UseNavigateStable="useNavigate",i.UseRouteId="useRouteId",i})(Xc||{});function Hp(i){let u=S.useContext(_o);return u||Pe(!1),u}function Qp(i){let u=S.useContext(Qc);return u||Pe(!1),u}function Kp(i){let u=S.useContext(Cn);return u||Pe(!1),u}function Zc(i){let u=Kp(),s=u.matches[u.matches.length-1];return s.route.id||Pe(!1),s.route.id}function Yp(){var i;let u=S.useContext(Kc),s=Qp(),d=Zc();return u!==void 0?u:(i=s.errors)==null?void 0:i[d]}function Jp(){let{router:i}=Hp(Gc.UseNavigateStable),u=Zc(Xc.UseNavigateStable),s=S.useRef(!1);return Yc(()=>{s.current=!0}),S.useCallback(function(p,h){h===void 0&&(h={}),s.current&&(typeof p=="number"?i.navigate(p):i.navigate(p,ul({fromRouteId:u},h)))},[i,u])}const Tc={};function Gp(i,u,s){Tc[i]||(Tc[i]=!0)}function Xp(i,u){i==null||i.v7_startTransition,i==null||i.v7_relativeSplatPath}function ws(i){let{to:u,replace:s,state:d,relative:p}=i;xr()||Pe(!1);let{future:h,static:g}=S.useContext(bt),{matches:j}=S.useContext(Cn),{pathname:w}=wr(),_=Jc(),P=zs(u,Rs(j,h.v7_relativeSplatPath),w,p==="path"),N=JSON.stringify(P);return S.useEffect(()=>_(JSON.parse(N),{replace:s,state:d,relative:p}),[_,N,p,s,d]),null}function il(i){Pe(!1)}function Zp(i){let{basename:u="/",children:s=null,location:d,navigationType:p=Sn.Pop,navigator:h,static:g=!1,future:j}=i;xr()&&Pe(!1);let w=u.replace(/^\/*/,"/"),_=S.useMemo(()=>({basename:w,navigator:h,static:g,future:ul({v7_relativeSplatPath:!1},j)}),[w,j,h,g]);typeof d=="string"&&(d=yr(d));let{pathname:P="/",search:N="",hash:M="",state:H=null,key:K="default"}=d,T=S.useMemo(()=>{let E=vr(P,w);return E==null?null:{location:{pathname:E,search:N,hash:M,state:H,key:K},navigationType:p}},[w,P,N,M,H,K,p]);return T==null?null:S.createElement(bt.Provider,{value:_},S.createElement(No.Provider,{children:s,value:T}))}function qp(i){let{children:u,location:s}=i;return Wp(Es(u),s)}new Promise(()=>{});function Es(i,u){u===void 0&&(u=[]);let s=[];return S.Children.forEach(i,(d,p)=>{if(!S.isValidElement(d))return;let h=[...u,p];if(d.type===S.Fragment){s.push.apply(s,Es(d.props.children,h));return}d.type!==il&&Pe(!1),!d.props.index||!d.props.children||Pe(!1);let g={id:d.props.id||h.join("-"),caseSensitive:d.props.caseSensitive,element:d.props.element,Component:d.props.Component,index:d.props.index,path:d.props.path,loader:d.props.loader,action:d.props.action,errorElement:d.props.errorElement,ErrorBoundary:d.props.ErrorBoundary,hasErrorBoundary:d.props.ErrorBoundary!=null||d.props.errorElement!=null,shouldRevalidate:d.props.shouldRevalidate,handle:d.props.handle,lazy:d.props.lazy};d.props.children&&(g.children=Es(d.props.children,h)),s.push(g)}),s}/** + */function ul(){return ul=Object.assign?Object.assign.bind():function(i){for(var u=1;u{j.current=!0}),S.useCallback(function(_,P){if(P===void 0&&(P={}),!j.current)return;if(typeof _=="number"){d.go(_);return}let N=zs(_,JSON.parse(g),h,P.relative==="path");i==null&&u!=="/"&&(N.pathname=N.pathname==="/"?u:jn([u,N.pathname])),(P.replace?d.replace:d.push)(N,P.state,P)},[u,d,g,h,i])}function Po(i,u){let{relative:s}=u===void 0?{}:u,{future:d}=S.useContext(bt),{matches:p}=S.useContext(Cn),{pathname:h}=kr(),g=JSON.stringify(Rs(p,d.v7_relativeSplatPath));return S.useMemo(()=>zs(i,JSON.parse(g),h,s==="path"),[i,g,h,s])}function Wp(i,u){return Fp(i,u)}function Fp(i,u,s,d){xr()||Pe(!1);let{navigator:p}=S.useContext(bt),{matches:h}=S.useContext(Cn),g=h[h.length-1],j=g?g.params:{};g&&g.pathname;let k=g?g.pathnameBase:"/";g&&g.route;let _=kr(),P;if(u){var N;let E=typeof u=="string"?yr(u):u;k==="/"||(N=E.pathname)!=null&&N.startsWith(k)||Pe(!1),P=E}else P=_;let M=P.pathname||"/",H=M;if(k!=="/"){let E=k.replace(/^\//,"").split("/");H="/"+M.replace(/^\//,"").split("/").slice(E.length).join("/")}let K=fp(i,{pathname:H}),T=Vp(K&&K.map(E=>Object.assign({},E,{params:Object.assign({},j,E.params),pathname:jn([k,p.encodeLocation?p.encodeLocation(E.pathname).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?k:jn([k,p.encodeLocation?p.encodeLocation(E.pathnameBase).pathname:E.pathnameBase])})),h,s,d);return u&&T?S.createElement(No.Provider,{value:{location:ul({pathname:"/",search:"",hash:"",state:null,key:"default"},P),navigationType:Sn.Pop}},T):T}function Up(){let i=Yp(),u=Op(i)?i.status+" "+i.statusText:i instanceof Error?i.message:JSON.stringify(i),s=i instanceof Error?i.stack:null,p={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return S.createElement(S.Fragment,null,S.createElement("h2",null,"Unexpected Application Error!"),S.createElement("h3",{style:{fontStyle:"italic"}},u),s?S.createElement("pre",{style:p},s):null,null)}const Bp=S.createElement(Up,null);class Ap extends S.Component{constructor(u){super(u),this.state={location:u.location,revalidation:u.revalidation,error:u.error}}static getDerivedStateFromError(u){return{error:u}}static getDerivedStateFromProps(u,s){return s.location!==u.location||s.revalidation!=="idle"&&u.revalidation==="idle"?{error:u.error,location:u.location,revalidation:u.revalidation}:{error:u.error!==void 0?u.error:s.error,location:s.location,revalidation:u.revalidation||s.revalidation}}componentDidCatch(u,s){console.error("React Router caught the following error during render",u,s)}render(){return this.state.error!==void 0?S.createElement(Cn.Provider,{value:this.props.routeContext},S.createElement(Kc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function $p(i){let{routeContext:u,match:s,children:d}=i,p=S.useContext(_o);return p&&p.static&&p.staticContext&&(s.route.errorElement||s.route.ErrorBoundary)&&(p.staticContext._deepestRenderedBoundaryId=s.route.id),S.createElement(Cn.Provider,{value:u},d)}function Vp(i,u,s,d){var p;if(u===void 0&&(u=[]),s===void 0&&(s=null),d===void 0&&(d=null),i==null){var h;if(!s)return null;if(s.errors)i=s.matches;else if((h=d)!=null&&h.v7_partialHydration&&u.length===0&&!s.initialized&&s.matches.length>0)i=s.matches;else return null}let g=i,j=(p=s)==null?void 0:p.errors;if(j!=null){let P=g.findIndex(N=>N.route.id&&(j==null?void 0:j[N.route.id])!==void 0);P>=0||Pe(!1),g=g.slice(0,Math.min(g.length,P+1))}let k=!1,_=-1;if(s&&d&&d.v7_partialHydration)for(let P=0;P=0?g=g.slice(0,_+1):g=[g[0]];break}}}return g.reduceRight((P,N,M)=>{let H,K=!1,T=null,E=null;s&&(H=j&&N.route.id?j[N.route.id]:void 0,T=N.route.errorElement||Bp,k&&(_<0&&M===0?(Jp("route-fallback"),K=!0,E=null):_===M&&(K=!0,E=N.route.hydrateFallbackElement||null)));let A=u.concat(g.slice(0,M+1)),$=()=>{let J;return H?J=T:K?J=E:N.route.Component?J=S.createElement(N.route.Component,null):N.route.element?J=N.route.element:J=P,S.createElement($p,{match:N,routeContext:{outlet:P,matches:A,isDataRoute:s!=null},children:J})};return s&&(N.route.ErrorBoundary||N.route.errorElement||M===0)?S.createElement(Ap,{location:s.location,revalidation:s.revalidation,component:T,error:H,children:$(),routeContext:{outlet:null,matches:A,isDataRoute:!0}}):$()},null)}var Jc=(function(i){return i.UseBlocker="useBlocker",i.UseRevalidator="useRevalidator",i.UseNavigateStable="useNavigate",i})(Jc||{}),Xc=(function(i){return i.UseBlocker="useBlocker",i.UseLoaderData="useLoaderData",i.UseActionData="useActionData",i.UseRouteError="useRouteError",i.UseNavigation="useNavigation",i.UseRouteLoaderData="useRouteLoaderData",i.UseMatches="useMatches",i.UseRevalidator="useRevalidator",i.UseNavigateStable="useNavigate",i.UseRouteId="useRouteId",i})(Xc||{});function Hp(i){let u=S.useContext(_o);return u||Pe(!1),u}function Qp(i){let u=S.useContext(Qc);return u||Pe(!1),u}function Kp(i){let u=S.useContext(Cn);return u||Pe(!1),u}function Zc(i){let u=Kp(),s=u.matches[u.matches.length-1];return s.route.id||Pe(!1),s.route.id}function Yp(){var i;let u=S.useContext(Kc),s=Qp(),d=Zc();return u!==void 0?u:(i=s.errors)==null?void 0:i[d]}function Gp(){let{router:i}=Hp(Jc.UseNavigateStable),u=Zc(Xc.UseNavigateStable),s=S.useRef(!1);return Yc(()=>{s.current=!0}),S.useCallback(function(p,h){h===void 0&&(h={}),s.current&&(typeof p=="number"?i.navigate(p):i.navigate(p,ul({fromRouteId:u},h)))},[i,u])}const Tc={};function Jp(i,u,s){Tc[i]||(Tc[i]=!0)}function Xp(i,u){i==null||i.v7_startTransition,i==null||i.v7_relativeSplatPath}function ks(i){let{to:u,replace:s,state:d,relative:p}=i;xr()||Pe(!1);let{future:h,static:g}=S.useContext(bt),{matches:j}=S.useContext(Cn),{pathname:k}=kr(),_=Gc(),P=zs(u,Rs(j,h.v7_relativeSplatPath),k,p==="path"),N=JSON.stringify(P);return S.useEffect(()=>_(JSON.parse(N),{replace:s,state:d,relative:p}),[_,N,p,s,d]),null}function il(i){Pe(!1)}function Zp(i){let{basename:u="/",children:s=null,location:d,navigationType:p=Sn.Pop,navigator:h,static:g=!1,future:j}=i;xr()&&Pe(!1);let k=u.replace(/^\/*/,"/"),_=S.useMemo(()=>({basename:k,navigator:h,static:g,future:ul({v7_relativeSplatPath:!1},j)}),[k,j,h,g]);typeof d=="string"&&(d=yr(d));let{pathname:P="/",search:N="",hash:M="",state:H=null,key:K="default"}=d,T=S.useMemo(()=>{let E=vr(P,k);return E==null?null:{location:{pathname:E,search:N,hash:M,state:H,key:K},navigationType:p}},[k,P,N,M,H,K,p]);return T==null?null:S.createElement(bt.Provider,{value:_},S.createElement(No.Provider,{children:s,value:T}))}function qp(i){let{children:u,location:s}=i;return Wp(Es(u),s)}new Promise(()=>{});function Es(i,u){u===void 0&&(u=[]);let s=[];return S.Children.forEach(i,(d,p)=>{if(!S.isValidElement(d))return;let h=[...u,p];if(d.type===S.Fragment){s.push.apply(s,Es(d.props.children,h));return}d.type!==il&&Pe(!1),!d.props.index||!d.props.children||Pe(!1);let g={id:d.props.id||h.join("-"),caseSensitive:d.props.caseSensitive,element:d.props.element,Component:d.props.Component,index:d.props.index,path:d.props.path,loader:d.props.loader,action:d.props.action,errorElement:d.props.errorElement,ErrorBoundary:d.props.ErrorBoundary,hasErrorBoundary:d.props.ErrorBoundary!=null||d.props.errorElement!=null,shouldRevalidate:d.props.shouldRevalidate,handle:d.props.handle,lazy:d.props.lazy};d.props.children&&(g.children=Es(d.props.children,h)),s.push(g)}),s}/** * React Router DOM v6.30.4 * * Copyright (c) Remix Software Inc. @@ -64,7 +64,7 @@ Error generating stack: `+o.message+` * LICENSE.md file in the root directory of this source tree. * * @license MIT - */function Eo(){return Eo=Object.assign?Object.assign.bind():function(i){for(var u=1;u{_&&Rc?Rc(()=>w(N)):w(N)},[w,_]);return S.useLayoutEffect(()=>g.listen(P),[g,P]),S.useEffect(()=>Xp(d),[d]),S.createElement(Zp,{basename:u,children:s,location:j.location,navigationType:j.action,navigator:g,future:d})}const sh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ah=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,uh=S.forwardRef(function(u,s){let{onClick:d,relative:p,reloadDocument:h,replace:g,state:j,target:w,to:_,preventScrollReset:P,viewTransition:N}=u,M=qc(u,th),{basename:H}=S.useContext(bt),K,T=!1;if(typeof _=="string"&&ah.test(_)&&(K=_,sh))try{let G=new URL(window.location.href),U=_.startsWith("//")?new URL(G.protocol+_):new URL(_),ce=vr(U.pathname,H);U.origin===G.origin&&ce!=null?_=ce+U.search+U.hash:T=!0}catch{}let E=Mp(_,{relative:p}),A=fh(_,{replace:g,state:j,target:w,preventScrollReset:P,relative:p,viewTransition:N});function $(G){d&&d(G),G.defaultPrevented||A(G)}return S.createElement("a",Eo({},M,{href:K||E,onClick:T||h?d:$,ref:s,target:w}))}),ch=S.forwardRef(function(u,s){let{"aria-current":d="page",caseSensitive:p=!1,className:h="",end:g=!1,style:j,to:w,viewTransition:_,children:P}=u,N=qc(u,nh),M=Po(w,{relative:N.relative}),H=wr(),K=S.useContext(Qc),{navigator:T,basename:E}=S.useContext(bt),A=K!=null&&ph(M)&&_===!0,$=T.encodeLocation?T.encodeLocation(M).pathname:M.pathname,G=H.pathname,U=K&&K.navigation&&K.navigation.location?K.navigation.location.pathname:null;p||(G=G.toLowerCase(),U=U?U.toLowerCase():null,$=$.toLowerCase()),U&&E&&(U=vr(U,E)||U);const ce=$!=="/"&&$.endsWith("/")?$.length-1:$.length;let de=G===$||!g&&G.startsWith($)&&G.charAt(ce)==="/",Ce=U!=null&&(U===$||!g&&U.startsWith($)&&U.charAt($.length)==="/"),xe={isActive:de,isPending:Ce,isTransitioning:A},Ze=de?d:void 0,Ie;typeof h=="function"?Ie=h(xe):Ie=[h,de?"active":null,Ce?"pending":null,A?"transitioning":null].filter(Boolean).join(" ");let Ke=typeof j=="function"?j(xe):j;return S.createElement(uh,Eo({},N,{"aria-current":Ze,className:Ie,ref:s,style:Ke,to:w,viewTransition:_}),typeof P=="function"?P(xe):P)});var _s;(function(i){i.UseScrollRestoration="useScrollRestoration",i.UseSubmit="useSubmit",i.UseSubmitFetcher="useSubmitFetcher",i.UseFetcher="useFetcher",i.useViewTransitionState="useViewTransitionState"})(_s||(_s={}));var zc;(function(i){i.UseFetcher="useFetcher",i.UseFetchers="useFetchers",i.UseScrollRestoration="useScrollRestoration"})(zc||(zc={}));function dh(i){let u=S.useContext(_o);return u||Pe(!1),u}function fh(i,u){let{target:s,replace:d,state:p,preventScrollReset:h,relative:g,viewTransition:j}=u===void 0?{}:u,w=Jc(),_=wr(),P=Po(i,{relative:g});return S.useCallback(N=>{if(eh(N,s)){N.preventDefault();let M=d!==void 0?d:Co(_)===Co(P);w(i,{replace:M,state:p,preventScrollReset:h,relative:g,viewTransition:j})}},[_,w,P,d,p,s,i,h,g,j])}function ph(i,u){u===void 0&&(u={});let s=S.useContext(lh);s==null&&Pe(!1);let{basename:d}=dh(_s.useViewTransitionState),p=Po(i,{relative:u.relative});if(!s.isTransitioning)return!1;let h=vr(s.currentLocation.pathname,d)||s.currentLocation.pathname,g=vr(s.nextLocation.pathname,d)||s.nextLocation.pathname;return Cs(p.pathname,g)!=null||Cs(p.pathname,h)!=null}function hh(){if(window.matchMedia("(display-mode: standalone)").matches)return;const i=document.cookie.split(";").map(s=>s.trim()).find(s=>s.startsWith("hnf_inactivity_mins="));if(!i)return;const u=parseInt(i.split("=")[1]);return isNaN(u)||u<=0?void 0:u*60*1e3}const Lc={background:"var(--navy-dark)",border:"1px solid var(--surface-2)",borderRadius:"6px",color:"var(--text)",padding:"0.625rem 0.75rem",fontSize:"1rem",width:"100%",outline:"none"},mh={background:"var(--hk-green)",color:"#fff",border:"none",borderRadius:"6px",padding:"0.625rem",fontSize:"1rem",fontWeight:600,marginTop:"0.25rem",width:"100%"};function vh({children:i}){const[u,s]=S.useState("checking"),[d,p]=S.useState(null),[h,g]=S.useState(""),[j,w]=S.useState(""),[_,P]=S.useState(""),[N,M]=S.useState(!1),H=S.useRef(null);S.useEffect(()=>{fetch("/api/auth/verify?app=hk-planner",{credentials:"include"}).then(async T=>{T.ok?(p(await T.json()),s("authed")):s("login")}).catch(()=>s("login"))},[]),S.useEffect(()=>{const T=hh();if(u!=="authed"||!T)return;async function E(){await fetch("/api/auth/logout",{method:"POST",credentials:"include"}).catch(()=>{}),p(null),s("login")}function A(){H.current&&clearTimeout(H.current),H.current=setTimeout(E,T)}const $=["mousemove","keydown","click","touchstart"];return $.forEach(G=>window.addEventListener(G,A,{passive:!0})),A(),()=>{H.current&&clearTimeout(H.current),$.forEach(G=>window.removeEventListener(G,A))}},[u]);async function K(T){T.preventDefault(),M(!0),P("");try{if(!(await fetch("/api/auth/login",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:h,password:j})})).ok){P("Invalid email or password");return}const A=await fetch("/api/auth/verify?app=hk-planner",{credentials:"include"});A.ok?(p(await A.json()),s("authed")):P("You don't have access to this app.")}catch{P("Connection error — please try again")}finally{M(!1)}}return u==="checking"?c.jsx("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100dvh"},children:c.jsx("div",{style:{color:"var(--text-muted)"},children:"Loading…"})}):u==="login"?c.jsx("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100dvh",padding:"1.5rem",background:"var(--navy-dark)"},children:c.jsxs("div",{style:{background:"var(--navy)",borderRadius:"var(--radius)",padding:"2rem",width:"100%",maxWidth:"360px",border:"1px solid var(--surface-2)"},children:[c.jsx("h1",{style:{fontSize:"1.4rem",marginBottom:"0.25rem",color:"#74c69d"},children:"HK Planner"}),c.jsx("p",{style:{color:"var(--text-muted)",fontSize:"0.875rem",marginBottom:"1.5rem"},children:void 0}),c.jsxs("form",{onSubmit:K,style:{display:"flex",flexDirection:"column",gap:"0.75rem"},children:[c.jsx("input",{type:"email",value:h,onChange:T=>g(T.target.value),placeholder:"Email",required:!0,autoComplete:"email",style:Lc}),c.jsx("input",{type:"password",value:j,onChange:T=>w(T.target.value),placeholder:"Password",required:!0,autoComplete:"current-password",style:Lc}),_&&c.jsx("p",{style:{color:"#f87171",fontSize:"0.875rem"},children:_}),c.jsx("button",{type:"submit",disabled:N,style:mh,children:N?"Signing in…":"Sign in"})]})]})}):c.jsx(c.Fragment,{children:i(d)})}/** + */function Eo(){return Eo=Object.assign?Object.assign.bind():function(i){for(var u=1;u{_&&Rc?Rc(()=>k(N)):k(N)},[k,_]);return S.useLayoutEffect(()=>g.listen(P),[g,P]),S.useEffect(()=>Xp(d),[d]),S.createElement(Zp,{basename:u,children:s,location:j.location,navigationType:j.action,navigator:g,future:d})}const sh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",ah=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,uh=S.forwardRef(function(u,s){let{onClick:d,relative:p,reloadDocument:h,replace:g,state:j,target:k,to:_,preventScrollReset:P,viewTransition:N}=u,M=qc(u,th),{basename:H}=S.useContext(bt),K,T=!1;if(typeof _=="string"&&ah.test(_)&&(K=_,sh))try{let J=new URL(window.location.href),U=_.startsWith("//")?new URL(J.protocol+_):new URL(_),ce=vr(U.pathname,H);U.origin===J.origin&&ce!=null?_=ce+U.search+U.hash:T=!0}catch{}let E=Mp(_,{relative:p}),A=fh(_,{replace:g,state:j,target:k,preventScrollReset:P,relative:p,viewTransition:N});function $(J){d&&d(J),J.defaultPrevented||A(J)}return S.createElement("a",Eo({},M,{href:K||E,onClick:T||h?d:$,ref:s,target:k}))}),ch=S.forwardRef(function(u,s){let{"aria-current":d="page",caseSensitive:p=!1,className:h="",end:g=!1,style:j,to:k,viewTransition:_,children:P}=u,N=qc(u,nh),M=Po(k,{relative:N.relative}),H=kr(),K=S.useContext(Qc),{navigator:T,basename:E}=S.useContext(bt),A=K!=null&&ph(M)&&_===!0,$=T.encodeLocation?T.encodeLocation(M).pathname:M.pathname,J=H.pathname,U=K&&K.navigation&&K.navigation.location?K.navigation.location.pathname:null;p||(J=J.toLowerCase(),U=U?U.toLowerCase():null,$=$.toLowerCase()),U&&E&&(U=vr(U,E)||U);const ce=$!=="/"&&$.endsWith("/")?$.length-1:$.length;let de=J===$||!g&&J.startsWith($)&&J.charAt(ce)==="/",Ce=U!=null&&(U===$||!g&&U.startsWith($)&&U.charAt($.length)==="/"),xe={isActive:de,isPending:Ce,isTransitioning:A},qe=de?d:void 0,We;typeof h=="function"?We=h(xe):We=[h,de?"active":null,Ce?"pending":null,A?"transitioning":null].filter(Boolean).join(" ");let Ke=typeof j=="function"?j(xe):j;return S.createElement(uh,Eo({},N,{"aria-current":qe,className:We,ref:s,style:Ke,to:k,viewTransition:_}),typeof P=="function"?P(xe):P)});var _s;(function(i){i.UseScrollRestoration="useScrollRestoration",i.UseSubmit="useSubmit",i.UseSubmitFetcher="useSubmitFetcher",i.UseFetcher="useFetcher",i.useViewTransitionState="useViewTransitionState"})(_s||(_s={}));var zc;(function(i){i.UseFetcher="useFetcher",i.UseFetchers="useFetchers",i.UseScrollRestoration="useScrollRestoration"})(zc||(zc={}));function dh(i){let u=S.useContext(_o);return u||Pe(!1),u}function fh(i,u){let{target:s,replace:d,state:p,preventScrollReset:h,relative:g,viewTransition:j}=u===void 0?{}:u,k=Gc(),_=kr(),P=Po(i,{relative:g});return S.useCallback(N=>{if(eh(N,s)){N.preventDefault();let M=d!==void 0?d:Co(_)===Co(P);k(i,{replace:M,state:p,preventScrollReset:h,relative:g,viewTransition:j})}},[_,k,P,d,p,s,i,h,g,j])}function ph(i,u){u===void 0&&(u={});let s=S.useContext(lh);s==null&&Pe(!1);let{basename:d}=dh(_s.useViewTransitionState),p=Po(i,{relative:u.relative});if(!s.isTransitioning)return!1;let h=vr(s.currentLocation.pathname,d)||s.currentLocation.pathname,g=vr(s.nextLocation.pathname,d)||s.nextLocation.pathname;return Cs(p.pathname,g)!=null||Cs(p.pathname,h)!=null}function hh(){if(window.matchMedia("(display-mode: standalone)").matches)return;const i=document.cookie.split(";").map(s=>s.trim()).find(s=>s.startsWith("hnf_inactivity_mins="));if(!i)return;const u=parseInt(i.split("=")[1]);return isNaN(u)||u<=0?void 0:u*60*1e3}const Lc={background:"var(--navy-dark)",border:"1px solid var(--surface-2)",borderRadius:"6px",color:"var(--text)",padding:"0.625rem 0.75rem",fontSize:"1rem",width:"100%",outline:"none"},mh={background:"var(--hk-green)",color:"#fff",border:"none",borderRadius:"6px",padding:"0.625rem",fontSize:"1rem",fontWeight:600,marginTop:"0.25rem",width:"100%"};function vh({children:i}){const[u,s]=S.useState("checking"),[d,p]=S.useState(null),[h,g]=S.useState(""),[j,k]=S.useState(""),[_,P]=S.useState(""),[N,M]=S.useState(!1),H=S.useRef(null);S.useEffect(()=>{fetch("/api/auth/verify?app=hk-planner",{credentials:"include"}).then(async T=>{T.ok?(p(await T.json()),s("authed")):s("login")}).catch(()=>s("login"))},[]),S.useEffect(()=>{const T=hh();if(u!=="authed"||!T)return;async function E(){await fetch("/api/auth/logout",{method:"POST",credentials:"include"}).catch(()=>{}),p(null),s("login")}function A(){H.current&&clearTimeout(H.current),H.current=setTimeout(E,T)}const $=["mousemove","keydown","click","touchstart"];return $.forEach(J=>window.addEventListener(J,A,{passive:!0})),A(),()=>{H.current&&clearTimeout(H.current),$.forEach(J=>window.removeEventListener(J,A))}},[u]);async function K(T){T.preventDefault(),M(!0),P("");try{if(!(await fetch("/api/auth/login",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:h,password:j})})).ok){P("Invalid email or password");return}const A=await fetch("/api/auth/verify?app=hk-planner",{credentials:"include"});A.ok?(p(await A.json()),s("authed")):P("You don't have access to this app.")}catch{P("Connection error — please try again")}finally{M(!1)}}return u==="checking"?c.jsx("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",height:"100dvh"},children:c.jsx("div",{style:{color:"var(--text-muted)"},children:"Loading…"})}):u==="login"?c.jsx("div",{style:{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100dvh",padding:"1.5rem",background:"var(--navy-dark)"},children:c.jsxs("div",{style:{background:"var(--navy)",borderRadius:"var(--radius)",padding:"2rem",width:"100%",maxWidth:"360px",border:"1px solid var(--surface-2)"},children:[c.jsx("h1",{style:{fontSize:"1.4rem",marginBottom:"0.25rem",color:"#74c69d"},children:"HK Planner"}),c.jsx("p",{style:{color:"var(--text-muted)",fontSize:"0.875rem",marginBottom:"1.5rem"},children:void 0}),c.jsxs("form",{onSubmit:K,style:{display:"flex",flexDirection:"column",gap:"0.75rem"},children:[c.jsx("input",{type:"email",value:h,onChange:T=>g(T.target.value),placeholder:"Email",required:!0,autoComplete:"email",style:Lc}),c.jsx("input",{type:"password",value:j,onChange:T=>k(T.target.value),placeholder:"Password",required:!0,autoComplete:"current-password",style:Lc}),_&&c.jsx("p",{style:{color:"#f87171",fontSize:"0.875rem"},children:_}),c.jsx("button",{type:"submit",disabled:N,style:mh,children:N?"Signing in…":"Sign in"})]})]})}):c.jsx(c.Fragment,{children:i(d)})}/** * @license lucide-react v1.24.0 - ISC * * This source code is licensed under the ISC license. @@ -89,12 +89,12 @@ Error generating stack: `+o.message+` * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */var ks={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + */var ws={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** * @license lucide-react v1.24.0 - ISC * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const xh=i=>{for(const u in i)if(u.startsWith("aria-")||u==="role"||u==="title")return!0;return!1},wh=S.createContext({}),kh=()=>S.useContext(wh),Sh=S.forwardRef(({color:i,size:u,strokeWidth:s,absoluteStrokeWidth:d,className:p="",children:h,iconNode:g,...j},w)=>{const{size:_=24,strokeWidth:P=2,absoluteStrokeWidth:N=!1,color:M="currentColor",className:H=""}=kh()??{},K=d??N?Number(s??P)*24/Number(u??_):s??P;return S.createElement("svg",{ref:w,...ks,width:u??_??ks.width,height:u??_??ks.height,stroke:i??M,strokeWidth:K,className:bc("lucide",H,p),...!h&&!xh(j)&&{"aria-hidden":"true"},...j},[...g.map(([T,E])=>S.createElement(T,E)),...Array.isArray(h)?h:[h]])});/** + */const xh=i=>{for(const u in i)if(u.startsWith("aria-")||u==="role"||u==="title")return!0;return!1},kh=S.createContext({}),wh=()=>S.useContext(kh),Sh=S.forwardRef(({color:i,size:u,strokeWidth:s,absoluteStrokeWidth:d,className:p="",children:h,iconNode:g,...j},k)=>{const{size:_=24,strokeWidth:P=2,absoluteStrokeWidth:N=!1,color:M="currentColor",className:H=""}=wh()??{},K=d??N?Number(s??P)*24/Number(u??_):s??P;return S.createElement("svg",{ref:k,...ws,width:u??_??ws.width,height:u??_??ws.height,stroke:i??M,strokeWidth:K,className:bc("lucide",H,p),...!h&&!xh(j)&&{"aria-hidden":"true"},...j},[...g.map(([T,E])=>S.createElement(T,E)),...Array.isArray(h)?h:[h]])});/** * @license lucide-react v1.24.0 - ISC * * This source code is licensed under the ISC license. @@ -124,4 +124,4 @@ Error generating stack: `+o.message+` * * This source code is licensed under the ISC license. * See the LICENSE file in the root directory of this source tree. - */const Th=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],Rh=cl("settings",Th);function zh({visible:i}){return i?c.jsxs("div",{style:{position:"fixed",bottom:0,left:0,right:0,zIndex:9999,background:"var(--sidebar)",color:"var(--text-light)",display:"flex",alignItems:"center",justifyContent:"center",gap:"12px",padding:"10px 16px",fontSize:"14px",boxShadow:"0 -2px 8px rgba(0,0,0,0.3)"},children:[c.jsx("span",{children:"A new version is available."}),c.jsxs("button",{onClick:()=>window.location.reload(),style:{display:"flex",alignItems:"center",gap:"6px",background:"var(--accent)",color:"var(--sidebar)",border:"none",borderRadius:"4px",padding:"6px 14px",fontWeight:600,cursor:"pointer",fontSize:"13px"},children:[c.jsx(Ns,{size:14,strokeWidth:1.75}),"Reload"]})]}):null}const Lh=120*1e3;function Oh(i){const[u,s]=S.useState(!1);return S.useEffect(()=>{let d=null;async function p(){try{const j=await fetch(i,{cache:"no-store"});if(!j.ok)return;const _=(await j.json()).version;if(!_)return;d===null?d=_:_!==d&&s(!0)}catch{}}p();const h=setInterval(p,Lh);function g(){document.visibilityState==="visible"&&p()}return document.addEventListener("visibilitychange",g),()=>{clearInterval(h),document.removeEventListener("visibilitychange",g)}},[i]),u}function ed(i,u){return i.is_admin||i.caps.includes(u)}const Dh="width=1280",Mh="width=device-width, initial-scale=1.0";function Mc(i){const u=document.querySelector('meta[name="viewport"]');u&&(u.content=i)}function Ih(i){S.useEffect(()=>(Mc(Dh),window.parent.postMessage({type:"hnf:viewport",mode:i},"*"),()=>{Mc(Mh),window.parent.postMessage({type:"hnf:viewport",mode:"responsive"},"*")}),[i])}function Wh({user:i,children:u}){Ih("desktop");async function s(){await fetch("/hk-planner/api/auth/logout",{method:"POST",credentials:"include"}),window.location.reload()}return c.jsxs("div",{style:{display:"flex",height:"100dvh",overflow:"hidden"},children:[c.jsxs("nav",{style:{width:"200px",flexShrink:0,background:"var(--navy)",display:"flex",flexDirection:"column",padding:"1rem 0",borderRight:"1px solid var(--surface-2)"},children:[c.jsx("div",{style:{padding:"0 1rem 1rem",borderBottom:"1px solid var(--surface-2)"},children:c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem"},children:[c.jsx(Dc,{size:20,strokeWidth:1.75,color:"#74c69d"}),c.jsx("span",{style:{color:"#74c69d",fontWeight:700,fontSize:"0.95rem"},children:"HK Planner"})]})}),c.jsxs("div",{className:"nav-scroll",style:{flex:1,padding:"0.5rem 0",overflowY:"auto"},children:[c.jsx(Ic,{to:"/planner",icon:Dc,label:"Planner"}),ed(i,"settings")&&c.jsx(Ic,{to:"/settings",icon:Rh,label:"Settings"})]}),c.jsxs("div",{style:{padding:"0.75rem 1rem",borderTop:"1px solid var(--surface-2)"},children:[c.jsxs("div",{style:{marginBottom:"0.5rem"},children:[c.jsx("div",{style:{color:"var(--text)",fontSize:"0.8rem",fontWeight:600},children:i.name}),c.jsx("div",{style:{color:"var(--text-muted)",fontSize:"0.72rem"},children:i.email})]}),c.jsxs("button",{onClick:s,style:{display:"flex",alignItems:"center",gap:"0.5rem",background:"none",border:"none",color:"var(--text-muted)",fontSize:"0.8rem",padding:"0.375rem 0",width:"100%",cursor:"pointer"},children:[c.jsx(Nh,{size:14,strokeWidth:1.75}),"Sign out"]})]})]}),c.jsx("main",{style:{flex:1,overflow:"auto",background:"var(--body-bg)"},children:u})]})}function Ic({to:i,icon:u,label:s}){return c.jsxs(ch,{to:i,style:({isActive:d})=>({display:"flex",alignItems:"center",gap:"0.625rem",padding:"0.625rem 1rem",textDecoration:"none",color:d?"#74c69d":"var(--text)",background:d?"var(--surface)":"transparent",borderLeft:d?"2px solid #74c69d":"2px solid transparent",fontSize:"0.875rem",transition:"background 0.15s"}),children:[c.jsx(u,{size:15,strokeWidth:1.75}),s]})}const Fh="/hk-planner/api";async function Xe(i,u){const s=await fetch(Fh+i,{credentials:"include",...u});if(s.status===401)throw(window.top??window).location.href="/login",new Error("Unauthenticated");if(!s.ok){const d=await s.json().catch(()=>({}));throw new Error(d.error||`HTTP ${s.status}`)}return s.json()}function Uh(i,u,s=!1){const d=new URLSearchParams;return i&&d.set("week_start",i),u&&d.set("last_viewed",u),s&&d.set("force_refresh","1"),Xe(`/bookings?${d}`)}function td(){return Xe("/config")}function Bh(i,u,s){return Xe("/config/time-requirements",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({cat:i,action:u,value:s})})}function Ah(i){return Xe("/config/staff",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({staff_data:i})})}function $h(i){return Xe("/config/pickup",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({pickup_data:i})})}function Vh(i){return Xe("/config/general-tasks",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({general_tasks:i})})}function Hh(i){return Xe("/config/last-reviewed",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({date:i})})}function Qh(){return Xe("/categories")}function Kh(i,u){return Xe("/categories",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({order:i,excluded:u})})}function Yh(){return Xe("/newbook/test",{method:"POST"})}function Jh(){return Xe("/workforce/departments")}function Gh(i){return Xe("/config/workforce-departments",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({dept_ids:i})})}function Xh(){return Xe("/workforce/sync",{method:"POST"})}function Wc(i,u){return Xe(`/workforce/shifts?start=${i}&end=${u}`)}function Zh(){return Xe("/workforce/staff")}function qh(i){return Xe("/config/adjustments",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({adjustments:i})})}function bh(i){return Xe("/config/warning-thresholds",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}function wo(){const i=new Date;return`${i.getFullYear()}-${String(i.getMonth()+1).padStart(2,"0")}-${String(i.getDate()).padStart(2,"0")}`}function ko(i,u){const[s,d,p]=i.split("-").map(Number),h=new Date(s,d-1,p);return h.setDate(h.getDate()+u),`${h.getFullYear()}-${String(h.getMonth()+1).padStart(2,"0")}-${String(h.getDate()).padStart(2,"0")}`}function To(i){const u=new Date(i+"T00:00:00");return["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][u.getDay()]+" "+u.getDate()+" "+["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][u.getMonth()]}function em(i){const u=new Date(i+"T00:00:00");return["Mon","Tue","Wed","Thu","Fri","Sat","Sun"][(u.getDay()+6)%7]}function tm(i){const u=new Date(i+"T00:00:00");return["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][u.getDay()]}function sl(i){const u=new Date(i+"T00:00:00").getDay();return u===0||u===6}function nm(i,u){const s=u.pickup_hint??0,d=u.pickup_lead??0,p=s>0?`${s} room${s===1?"":"s"} picked up within ${d} day${d===1?"":"s"} of arrival`:"no late pickups";return`Last ${tm(i)}: ${u.prior_occ} occ (${u.prior_vac} vac) — ${p}`}function Fc(i){return new Date(i+"T00:00:00").toLocaleDateString("en-GB",{weekday:"short",day:"numeric",month:"short"})}function Ge(i){return i.toFixed(2)+"h"}function gr(i,u,s,d,p){var _;const h=Math.max(0,p-d),g=(_=i[u])==null?void 0:_[s];if(!g||!g.count)return 0;const j=g.total-g.count,w=d>j?Math.max(0,g.total-d):g.count;return Math.min(w,h)}function rm(i,u,s,d,p){const h={};for(const g of i.dates){const j=em(g),w=d.reduce((P,N)=>P+(N.hours[j]||0)/60,0),_=p.reduce((P,N)=>P+(N.hours[g]||0),0);h[g]={booked:0,pickup:0,general:w,adjustments:_,total:w+_,by_cat:{},pickup_by_cat:{}}}for(const g of i.categories){const j=u[g.id]||{depart:0,stay:0,arrive:0};i.dates.forEach((w,_)=>{const P=g.days[w],N=P.stays+P.arrivals,M=gr(s,g.id,w,N,g.total_rooms),H=(P.departs*(j.depart||0)+P.stays*(j.stay||0)+P.arrivals*(j.arrive||0))/60,K=M*(j.arrive||0)/60;h[w].by_cat[g.id]=H,h[w].pickup_by_cat[g.id]=(h[w].pickup_by_cat[g.id]||0)+K,h[w].booked+=H,h[w].pickup+=K,h[w].total+=H+K;const T=i.dates[_+1];if(T&&M>0){const E=M*(j.depart||0)/60;h[T].pickup_by_cat[g.id]=(h[T].pickup_by_cat[g.id]||0)+E,h[T].pickup+=E,h[T].total+=E}})}return h}function lm(){const[i,u]=S.useState(null),[s,d]=S.useState({}),[p,h]=S.useState([]),[g,j]=S.useState({}),[w,_]=S.useState([]),[P,N]=S.useState([]),[M,H]=S.useState(4),[K,T]=S.useState(1),[E,A]=S.useState(1),[$,G]=S.useState(2),[U,ce]=S.useState({}),[de,Ce]=S.useState([]),[xe,Ze]=S.useState(!1),[Ie,Ke]=S.useState(null),[Ee,Te]=S.useState(""),[Be,qe]=S.useState(""),[Oe,ve]=S.useState(!0),[W,X]=S.useState(""),[F,y]=S.useState(null),R=S.useRef({}),ee=S.useRef(null),re=S.useRef(!1),le=S.useRef(null);le.current=Ie;function oe(ne,me,Ae=400){clearTimeout(R.current[ne]),R.current[ne]=setTimeout(me,Ae)}function ae(ne,me=!1){ee.current&&clearTimeout(ee.current),y({text:ne,err:me}),ee.current=setTimeout(()=>y(null),me?5e3:2500)}function se(){const ne=wo();ne!==Be&&(qe(ne),Hh(ne).catch(()=>{}))}const fe=S.useCallback(async(ne=!1,me)=>{ve(!0),X("");const Ae=wo(),rt=le.current||Ae,ft=ko(rt,6),pt=me!==void 0?me||ko(Ae,-1):Ee||Be||ko(Ae,-1);try{const[$e,we,yt]=await Promise.all([Uh(rt,pt,ne),td(),Wc(rt,ft).catch(()=>({}))]);u($e),d(we.time_requirements||{}),h(we.staff_data||[]),j(we.pickup_data||{}),_(we.general_tasks||[]),N(we.adjustments||[]),H(we.warn_over_red_hrs??4),T(we.warn_over_amber_hrs??1),A(we.warn_under_amber_hrs??1),G(we.warn_under_red_hrs??2),ce(yt),we.last_reviewed&&(qe(we.last_reviewed),Ee||Te(we.last_reviewed))}catch($e){X($e instanceof Error?$e.message:"Failed to load data")}finally{ve(!1)}},[Ee,Be]);S.useEffect(()=>{fe(!1)},[]),S.useEffect(()=>{Object.keys(U).length>0&&!re.current&&(re.current=!0,Zh().then(Ce).catch(()=>{}))},[U]),S.useEffect(()=>{function ne(){p.length&&navigator.sendBeacon("/hk-planner/api/config/staff",JSON.stringify({staff_data:p})),Object.keys(g).length&&navigator.sendBeacon("/hk-planner/api/config/pickup",JSON.stringify({pickup_data:g})),P.length&&navigator.sendBeacon("/hk-planner/api/config/adjustments",JSON.stringify({adjustments:P}))}return window.addEventListener("beforeunload",ne),()=>window.removeEventListener("beforeunload",ne)},[p,g,P]);function We(ne){const me=wo(),Ae=ko(le.current||me,ne);le.current=Ae,Ke(Ae),se(),fe(!0)}function At(){le.current=null,Ke(null),se(),fe(!0)}function En(){se(),fe(!0)}function $t(ne,me,Ae,rt){const ft=rt.days[me],pt=ft.stays+ft.arrivals,$e=Math.max(0,rt.total_rooms-pt),we=gr(g,ne,me,pt,rt.total_rooms),yt=Math.max(0,Math.min(we+Ae,$e)),Tn={...g,[ne]:{...g[ne]||{},[me]:{count:yt,total:pt+yt}}};j(Tn),se(),oe("pickup",()=>$h(Tn).then(()=>ae("Pickup saved")).catch(Vn=>ae(Vn.message,!0)))}function Vt(ne){N(ne),oe("adjustments",()=>qh(ne).then(()=>ae("Adjustments saved")).catch(me=>ae(me.message,!0)))}function _n(){Vt([...P,{label:"",hours:{}}])}function en(ne){h(ne),se(),oe("staff",()=>Ah(ne).then(()=>ae("Staff hours saved")).catch(me=>ae(me.message,!0)))}function Ht(){en([...p,{name:"",hours:{}}])}async function Nn(){Ze(!0);try{if(await Xh(),i){const ne=await Wc(i.dates[0],i.dates[i.dates.length-1]);ce(ne)}ae("Rota synced from Workforce")}catch(ne){ae(ne instanceof Error?ne.message:"Sync failed",!0)}finally{Ze(!1)}}function tn(){if(!i)return"";const ne=i.dates.filter($e=>U[$e]);if(!ne.length)return"Not synced";const me=ne.reduce(($e,we)=>new Date(U[we].synced_at)!U[$e]).length?`Partial sync — from ${rt} ${ft}`:`From ${rt} ${ft}`}const _t=i?rm(i,s,g,w,P):null,Dt=wo(),Pn=Ie||Dt;return c.jsxs("div",{style:{padding:"1.5rem",maxWidth:"1600px"},children:[c.jsxs("div",{style:{display:"flex",flexWrap:"wrap",alignItems:"center",gap:"0.75rem",marginBottom:"1.25rem"},children:[c.jsxs("div",{children:[c.jsx("h1",{style:{fontSize:"1.15rem",fontWeight:700,color:"var(--text-dark)"},children:"Housekeeping Planner"}),i&&c.jsxs("p",{style:{fontSize:"0.8rem",color:"var(--text-mid)",marginTop:"0.1rem"},children:[Fc(i.dates[0])," – ",Fc(i.dates[i.dates.length-1])]})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.4rem",marginLeft:"auto",flexWrap:"wrap"},children:[c.jsx(hr,{onClick:()=>We(-7),children:"‹ Prev"}),c.jsx(hr,{onClick:At,primary:!0,children:"Today"}),c.jsx(hr,{onClick:()=>We(7),children:"Next ›"}),c.jsx("input",{type:"date",value:Pn,onChange:ne=>{if(ne.target.value){const me=ne.target.value;le.current=me,Ke(me),se(),oe("weekStart",()=>fe(!0),600)}},style:{border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.35rem 0.5rem",fontSize:"0.82rem",color:"var(--text-dark)"}}),c.jsx("div",{style:{width:"1px",height:"24px",background:"var(--card-border)"}}),c.jsxs("label",{style:{fontSize:"0.78rem",color:"var(--text-mid)",display:"flex",alignItems:"center",gap:"0.3rem"},children:[c.jsx("span",{children:"Since"}),c.jsx("input",{type:"date",value:Ee,onChange:ne=>{if(ne.target.value){const me=ne.target.value;Te(me),oe("lastViewed",()=>fe(!0,me),600)}},style:{border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.3rem 0.5rem",fontSize:"0.78rem",color:"var(--text-dark)"}})]}),c.jsx(hr,{onClick:()=>{se(),fe(!0)},children:"Update to now"}),c.jsx("div",{style:{width:"1px",height:"24px",background:"var(--card-border)"}}),c.jsx("button",{onClick:En,title:"Refresh",style:{background:"var(--card-bg)",border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.35rem 0.5rem",display:"flex",alignItems:"center",gap:"0.3rem",fontSize:"0.82rem",color:"var(--text-dark)"},children:c.jsx(Ns,{size:13,strokeWidth:1.75,style:{animation:Oe?"spin 1s linear infinite":"none"}})})]})]}),c.jsx("style",{children:"@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"}),F&&c.jsx("div",{style:{display:"inline-block",marginBottom:"0.75rem",padding:"0.35rem 0.75rem",borderRadius:"6px",fontSize:"0.8rem",fontWeight:500,background:F.err?"#fee2e2":"#dcfce7",color:F.err?"var(--danger)":"var(--success)"},children:F.text}),W&&c.jsxs("div",{style:{marginBottom:"1rem",padding:"0.75rem 1rem",borderRadius:"8px",background:"#fee2e2",color:"var(--danger)",fontSize:"0.875rem"},children:["Error: ",W]}),Oe&&!i&&c.jsx("div",{style:{color:"var(--text-mid)",padding:"2rem 0",textAlign:"center"},children:"Loading bookings…"}),i&&_t&&c.jsxs(c.Fragment,{children:[c.jsx(So,{title:"7-Day Occupancy",children:c.jsx("div",{className:"table-scroll",children:c.jsx(om,{bookings:i,pickup:g,onPickupChange:$t})})}),c.jsx(So,{title:"Required Hours",children:c.jsx("div",{className:"table-scroll",children:c.jsx(am,{bookings:i,required:_t})})}),c.jsx(So,{title:"Adjustments",action:c.jsx(hr,{onClick:_n,children:"+ Add adjustment"}),children:c.jsx("div",{className:"table-scroll",children:c.jsx(fm,{bookings:i,adjustments:P,onChange:Vt})})}),c.jsx(So,{title:"Staff Rota",action:c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",flexWrap:"wrap"},children:[c.jsxs("button",{onClick:Nn,disabled:xe,style:{display:"flex",alignItems:"center",gap:"0.3rem",background:"var(--card-bg)",color:"var(--text-dark)",border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.3rem 0.65rem",fontSize:"0.78rem",fontWeight:600},children:[c.jsx(Ns,{size:11,strokeWidth:1.75,style:{animation:xe?"spin 1s linear infinite":"none"}}),xe?"Syncing…":"Sync from Workforce"]}),c.jsx("span",{style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:tn()}),c.jsx(hr,{onClick:Ht,children:"+ Add staff"})]}),children:c.jsx("div",{className:"table-scroll",children:c.jsx(cm,{bookings:i,staff:p,required:_t,warnOverRed:M,warnOverAmber:K,warnUnderAmber:E,warnUnderRed:$,onChange:en,wfShifts:U,wfStaff:de})})})]})]})}function So({title:i,children:u,action:s}){return c.jsxs("div",{style:{marginBottom:"1.5rem"},children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem",marginBottom:"0.5rem"},children:[c.jsx("h2",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--text-mid)",textTransform:"uppercase",letterSpacing:"0.08em"},children:i}),s]}),u]})}function hr({children:i,onClick:u,primary:s}){return c.jsx("button",{onClick:u,style:{background:s?"var(--hk-green)":"var(--card-bg)",color:s?"#fff":"var(--text-dark)",border:`1px solid ${s?"var(--hk-green)":"var(--card-border)"}`,borderRadius:"6px",padding:"0.35rem 0.75rem",fontSize:"0.82rem",fontWeight:600},children:i})}function om({bookings:i,pickup:u,onPickupChange:s}){const{dates:d,categories:p}=i;return c.jsxs("table",{className:"hk-table",children:[c.jsxs("thead",{children:[c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",rowSpan:2,children:"Category"}),d.map(h=>c.jsx("th",{colSpan:2,style:{borderLeft:"2px solid rgba(255,255,255,0.3)",...sl(h)?{background:"#253555"}:{}},children:To(h)},h))]}),c.jsx("tr",{children:d.map(h=>c.jsxs(c.Fragment,{children:[c.jsx("th",{style:{fontSize:"0.72rem",fontWeight:400,minWidth:"72px",background:sl(h)?"#2a3d5e":"#1e2d42",borderLeft:"2px solid rgba(255,255,255,0.3)"},children:"Rooms"},h+"-r"),c.jsx("th",{style:{fontSize:"0.72rem",fontWeight:400,minWidth:"56px",textAlign:"center",background:sl(h)?"#2a3d5e":"#1e2d42",borderLeft:"1px solid rgba(255,255,255,0.1)"},children:"D / S / A"},h+"-d")]}))})]}),c.jsx("tbody",{children:p.map((h,g)=>{const j=g===p.length-1;return c.jsx(im,{cat:h,dates:d,pickup:u,onPickupChange:(w,_)=>s(h.id,w,_,h),isLast:j},h.id)})}),c.jsx("tfoot",{children:c.jsx(sm,{dates:d,cats:p,pickup:u})})]})}function im({cat:i,dates:u,pickup:s,onPickupChange:d,isLast:p}){const h=p?"2px solid var(--card-border)":void 0;return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{borderBottom:h},children:i.name}),u.map((g,j)=>{var ce;const w=i.days[g],_=w.stays+w.arrivals,P=Math.max(0,i.total_rooms-_),N=gr(s,i.id,g,_,i.total_rooms),M=u[j-1],H=M?i.days[M].stays+i.days[M].arrivals:0,K=M?gr(s,i.id,M,H,i.total_rooms):0,T=w.delta_new||0,E=w.delta_cancelled||0,A=sl(g),$=(ce=s[i.id])==null?void 0:ce[g],G=nm(g,w);let U="";if($&&$.count){const de=$.total-$.count,Ce=_>de?"changed":"unchanged",xe=_>de?"decreased":"remains";U=`+${$.count} pickup set when booked was ${de} (target ${$.total}). Booked ${Ce}, so pickup ${xe}.`}return U=(U?U+" | ":"")+G,c.jsxs(c.Fragment,{children:[c.jsxs("td",{style:{textAlign:"center",verticalAlign:"middle",borderBottom:h,padding:"0.4rem 0.3rem",borderLeft:"2px solid var(--card-border)",...A?{background:"rgba(100,120,160,0.07)"}:{}},children:[(T>0||E>0)&&c.jsxs("div",{style:{marginBottom:"2px",fontSize:"0.72rem"},children:[T>0&&c.jsxs("span",{className:"delta-new",children:["▲",T]}),E>0&&c.jsxs("span",{className:"delta-canc",children:[" ▼",E]})]}),c.jsxs("div",{style:{fontWeight:700,fontSize:"1.1rem"},title:U,children:[_,N>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",N]})]}),c.jsxs("div",{style:{fontSize:"0.72rem",color:"var(--text-mid)",marginBottom:"4px"},children:[P," vac",N>0&&c.jsxs("span",{className:"pickup-tag",children:[" (",P-N,")"]})]}),c.jsxs("div",{className:"pickup-ctrl",children:[c.jsx("button",{className:"pickup-btn",disabled:N<=0,onClick:()=>d(g,-1),children:"−"}),c.jsx("span",{className:"pickup-num",children:N}),c.jsx("button",{className:"pickup-btn",disabled:N>=P,title:G,onClick:()=>d(g,1),children:"+"})]})]},g+"-r"),c.jsxs("td",{style:{verticalAlign:"middle",borderLeft:"1px solid var(--card-border)",borderBottom:h,padding:"0.4rem 0.3rem",textAlign:"center",...A?{background:"rgba(100,120,160,0.07)"}:{}},children:[c.jsxs("div",{style:{fontSize:"0.8rem",color:"#c2502e",fontWeight:600},children:[w.departs,"d",K>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",K]})]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1d6fb8",fontWeight:600},children:[w.stays,"s"]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1a7a4a",fontWeight:600},children:[w.arrivals,"a",N>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",N]})]})]},g+"-dsa")]})})]})}function sm({dates:i,cats:u,pickup:s}){return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontStyle:"italic"},children:"Total"}),i.map((d,p)=>{let h=0,g=0,j=0,w=0,_=0,P=0,N=0,M=0,H=0;for(const T of u){const E=T.days[d],A=E.stays+E.arrivals,$=Math.max(0,T.total_rooms-A),G=gr(s,T.id,d,A,T.total_rooms),U=i[p-1],ce=U?T.days[U].stays+T.days[U].arrivals:0,de=U?gr(s,T.id,U,ce,T.total_rooms):0;h+=A,g+=$,j+=G,w+=de,_+=E.departs,P+=E.stays,N+=E.arrivals,M+=E.delta_new||0,H+=E.delta_cancelled||0}const K=sl(d);return c.jsxs(c.Fragment,{children:[c.jsxs("td",{style:{textAlign:"center",verticalAlign:"middle",padding:"0.4rem 0.3rem",borderLeft:"2px solid var(--card-border)",...K?{background:"rgba(100,120,160,0.07)"}:{}},children:[(M>0||H>0)&&c.jsxs("div",{style:{marginBottom:"2px",fontSize:"0.72rem"},children:[M>0&&c.jsxs("span",{className:"delta-new",children:["▲",M]}),H>0&&c.jsxs("span",{className:"delta-canc",children:[" ▼",H]})]}),c.jsxs("div",{style:{fontWeight:700,fontSize:"1.1rem"},children:[h,j>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",j]})]}),c.jsxs("div",{style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:[g," vac",j>0&&c.jsxs("span",{className:"pickup-tag",children:[" (",g-j,")"]})]})]},d+"-r"),c.jsxs("td",{style:{verticalAlign:"middle",borderLeft:"1px solid var(--card-border)",padding:"0.4rem 0.3rem",textAlign:"center",...K?{background:"rgba(100,120,160,0.07)"}:{}},children:[c.jsxs("div",{style:{fontSize:"0.8rem",color:"#c2502e",fontWeight:600},children:[_,"d",w>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",w]})]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1d6fb8",fontWeight:600},children:[P,"s"]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1a7a4a",fontWeight:600},children:[N,"a",j>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",j]})]})]},d+"-dsa")]})})]})}function am({bookings:i,required:u}){const{dates:s,categories:d}=i;return c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Category"}),s.map(p=>c.jsx("th",{children:To(p)},p))]})}),c.jsx("tbody",{children:d.map(p=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",children:p.name}),s.map(h=>{const g=u[h].by_cat[p.id]||0,j=u[h].pickup_by_cat[p.id]||0;return c.jsxs("td",{children:[g===0&&j===0?"—":Ge(g),j>.001&&c.jsxs("span",{className:"pickup-tag",children:[" (+",Ge(j),")"]})]},h)})]},p.id))}),c.jsxs("tfoot",{children:[c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Booked hrs"}),s.map(p=>c.jsx("td",{children:Ge(u[p].booked)},p))]}),c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Pickup hrs"}),s.map(p=>c.jsx("td",{children:u[p].pickup>.001?Ge(u[p].pickup):"—"},p))]}),c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Recurring tasks"}),s.map(p=>c.jsx("td",{children:u[p].general>.001?Ge(u[p].general):"—"},p))]}),s.some(p=>u[p].adjustments!==0)&&c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Adjustments"}),s.map(p=>{const h=u[p].adjustments;return c.jsx("td",{style:{color:h<0?"var(--danger)":h>0?"#1a7a4a":"var(--text-mid)"},children:h===0?"—":(h>0?"+":"")+Ge(h)},p)})]}),c.jsxs("tr",{style:{background:"#e2e8f0"},children:[c.jsx("td",{className:"col-label",children:"Total Required"}),s.map(p=>c.jsxs("td",{style:{fontWeight:700},children:[Ge(u[p].total),u[p].pickup>.001&&c.jsxs("span",{className:"pickup-tag",style:{display:"block",fontSize:"0.7rem"},children:["inc ",Ge(u[p].pickup)," pickup"]})]},p))]})]})]})}function um(i,u){const s={};for(const d of u){const p=i[d];if(p)for(const h of p.staff)s[h.id]||(s[h.id]={id:h.id,name:h.name,days:{}}),s[h.id].days[d]={hours:h.hours,times:h.times}}return Object.values(s).sort((d,p)=>d.name.localeCompare(p.name))}function cm({bookings:i,staff:u,required:s,warnOverRed:d,warnOverAmber:p,warnUnderAmber:h,warnUnderRed:g,onChange:j,wfShifts:w,wfStaff:_}){const{dates:P}=i;function N(T,E){const A=u.map(($,G)=>G===T?{...$,name:E}:$);j(A)}function M(T,E,A){const $=parseFloat(A),G=u.map((U,ce)=>{if(ce!==T)return U;const de={...U.hours};return!isNaN($)&&$>=0?de[E]=$:delete de[E],{...U,hours:de}});j(G)}function H(T){j(u.filter((E,A)=>A!==T))}const K=um(w,P);return c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Staff Member"}),P.map(T=>c.jsx("th",{children:To(T)},T)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsxs("tbody",{children:[K.map(T=>c.jsxs("tr",{style:{background:"rgba(42,100,72,0.05)"},children:[c.jsxs("td",{className:"col-label",children:[c.jsx("span",{style:{display:"inline-block",fontSize:"0.67rem",fontWeight:700,background:"rgba(42,100,72,0.18)",color:"#1a7a4a",borderRadius:"3px",padding:"0 4px",marginRight:"0.4rem",lineHeight:"1.5"},children:"WF"}),T.name]}),P.map(E=>{const A=T.days[E];return c.jsx("td",{style:{textAlign:"center",padding:"0.3rem 0.4rem",verticalAlign:"middle"},children:A?c.jsxs(c.Fragment,{children:[c.jsx("div",{style:{fontSize:"0.68rem",color:"var(--text-mid)",lineHeight:1.25},children:A.times}),c.jsx("div",{style:{fontWeight:600},children:Ge(A.hours)})]}):c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"})},E)}),c.jsx("td",{})]},"wf-"+T.id)),_.length>0&&c.jsx("datalist",{id:"wf-staff-datalist",children:_.map(T=>c.jsx("option",{value:T.name},T.id))}),u.map((T,E)=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:T.name,placeholder:"Staff name",list:_.length>0?"wf-staff-datalist":void 0,onChange:A=>N(E,A.target.value)})}),P.map(A=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:24,step:.5,value:T.hours[A]??"",placeholder:"0",onChange:$=>M(E,A,$.target.value)})},A)),c.jsx("td",{children:c.jsx("button",{onClick:()=>H(E),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},E))]}),c.jsx("tfoot",{children:(()=>{const T={};for(const E of P){const A=K.reduce((G,U)=>{var ce;return G+(((ce=U.days[E])==null?void 0:ce.hours)||0)},0),$=u.reduce((G,U)=>G+(U.hours[E]||0),0);T[E]=A+$}return c.jsxs(c.Fragment,{children:[c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.78rem",color:"var(--text-mid)"},children:"Total Available"}),P.map(E=>c.jsx("td",{children:Ge(T[E])},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"vs Booked"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"with Recurring Tasks"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked+s[E].general,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"with Pickup"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked+s[E].general+s[E].pickup,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{style:{background:"#e2e8f0"},children:[c.jsx("td",{className:"col-label",style:{fontWeight:700},children:"with Adjustments"}),P.map(E=>c.jsx("td",{children:c.jsx(dm,{available:T[E],required:s[E].total,warnOverRed:d,warnOverAmber:p,warnUnderAmber:h,warnUnderRed:g})},E)),c.jsx("td",{})]})]})})()})]})}function Ss({available:i,required:u,warnOverAmber:s,warnUnderAmber:d}){if(i===0&&u===0)return c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"});const p=i-u,h=p>s?"⚠":p<-d?"✗":"✓";return c.jsxs("span",{style:{color:"var(--text-mid)",fontSize:"0.82rem"},children:[h," ",p>=0?"+":"",Ge(p)]})}function dm({available:i,required:u,warnOverRed:s,warnOverAmber:d,warnUnderAmber:p,warnUnderRed:h}){if(i===0&&u===0)return c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"});const g=i-u;return g>s?c.jsxs("span",{className:"diff-over-red",children:["⚠ ",Ge(g)," spare"]}):g>d?c.jsxs("span",{className:"diff-over",children:["⚠ ",Ge(g)," spare"]}):g<-h?c.jsxs("span",{className:"diff-under",children:["✗ ",Ge(Math.abs(g))," short"]}):g<-p?c.jsxs("span",{className:"diff-under-amber",children:["✗ ",Ge(Math.abs(g))," short"]}):c.jsxs("span",{className:"diff-ok",children:["✓ ",g>=0?"+":"",Ge(g)]})}function fm({bookings:i,adjustments:u,onChange:s}){const{dates:d}=i;function p(j,w){s(u.map((_,P)=>P===j?{..._,label:w}:_))}function h(j,w,_){const P=parseFloat(_);s(u.map((N,M)=>{if(M!==j)return N;const H={...N.hours};return isNaN(P)?delete H[w]:H[w]=P,{...N,hours:H}}))}function g(j){s(u.filter((w,_)=>_!==j))}return u.length===0?c.jsx("p",{style:{color:"var(--text-mid)",fontSize:"0.82rem",padding:"0.5rem 0"},children:'No adjustments — use "+ Add adjustment" above to add a one-off hour offset for a specific date.'}):c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Label"}),d.map(j=>c.jsx("th",{children:To(j)},j)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsx("tbody",{children:u.map((j,w)=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:j.label,placeholder:"e.g. Rooms from Sunday",onChange:_=>p(w,_.target.value)})}),d.map(_=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",step:.25,value:j.hours[_]??"",placeholder:"0",onChange:P=>h(w,_,P.target.value)})},_)),c.jsx("td",{children:c.jsx("button",{onClick:()=>g(w),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},w))}),c.jsx("tfoot",{children:c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.78rem",color:"var(--text-mid)"},children:"Total"}),d.map(j=>{const w=u.reduce((_,P)=>_+(P.hours[j]||0),0);return c.jsx("td",{style:{color:w<0?"var(--danger)":w>0?"#1a7a4a":"var(--text-mid)",fontWeight:w!==0?600:void 0},children:w===0?"—":(w>0?"+":"")+Ge(w)},j)}),c.jsx("td",{})]})})]})}const Uc=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];function pm(){const[i,u]=S.useState([]),[s,d]=S.useState(!0),[p,h]=S.useState(!1),[g,j]=S.useState(!1),[w,_]=S.useState(""),[P,N]=S.useState(""),[M,H]=S.useState(null),[K,T]=S.useState([]),[E,A]=S.useState([]),[$,G]=S.useState(!0),[U,ce]=S.useState(""),[de,Ce]=S.useState(!1),[xe,Ze]=S.useState(!1),[Ie,Ke]=S.useState(""),[Ee,Te]=S.useState([]),[Be,qe]=S.useState(!1),[Oe,ve]=S.useState(""),[W,X]=S.useState(""),[F,y]=S.useState({}),[R,ee]=S.useState(""),[re,le]=S.useState(""),[oe,ae]=S.useState(4),[se,fe]=S.useState(1),[We,At]=S.useState(1),[En,$t]=S.useState(2),[Vt,_n]=S.useState(!1),[en,Ht]=S.useState(""),[Nn,tn]=S.useState("");S.useEffect(()=>{Promise.all([Qh(),td(),Jh().catch(I=>(I.message.includes("503")||I.message.toLowerCase().includes("not configured")?Ce(!0):ce(I.message),null))]).then(([I,te,ie])=>{u(I.categories),Te(te.general_tasks||[]),y(te.time_requirements||{}),A(te.workforce_departments||[]),ae(te.warn_over_red_hrs??4),fe(te.warn_over_amber_hrs??1),At(te.warn_under_amber_hrs??1),$t(te.warn_under_red_hrs??2),ie&&T(ie),d(!1),G(!1)}).catch(I=>{_(I.message),d(!1),G(!1)})},[]);function _t(I){u(i.map((te,ie)=>ie===I?{...te,excluded:!te.excluded}:te))}function Dt(I,te){H(te),I.dataTransfer.effectAllowed="move"}function Pn(I,te){if(I.preventDefault(),M===null||M===te)return;const ie=[...i],[De]=ie.splice(M,1);ie.splice(te,0,De),u(ie),H(te)}function ne(){H(null)}async function me(){h(!0),_(""),N("");try{await Kh(i.map(I=>I.id),i.filter(I=>I.excluded).map(I=>I.id)),N("Saved"),setTimeout(()=>N(""),2500)}catch(I){_(I instanceof Error?I.message:"Save failed")}finally{h(!1)}}async function Ae(){j(!0),_(""),N("");try{const I=await Yh();N(I.ok?`Connection OK: ${I.message||""}`:`Failed: ${I.error||"unknown"}`)}catch(I){_(I instanceof Error?I.message:"Test failed")}finally{j(!1)}}function rt(I){A(te=>te.includes(I)?te.filter(ie=>ie!==I):[...te,I])}async function ft(){Ze(!0),ce(""),Ke("");try{await Gh(E),Ke("Departments saved"),setTimeout(()=>Ke(""),2500)}catch(I){ce(I instanceof Error?I.message:"Save failed")}finally{Ze(!1)}}function pt(I,te){Te(Ee.map((ie,De)=>De===I?{...ie,name:te}:ie))}function $e(I,te,ie){const De=parseInt(ie,10);Te(Ee.map((xt,kr)=>kr!==I?xt:{...xt,hours:{...xt.hours,[te]:isNaN(De)?0:Math.max(0,De)}}))}function we(I){Te(Ee.filter((te,ie)=>ie!==I))}async function yt(){qe(!0),X(""),ve("");try{await Vh(Ee),ve("Saved"),setTimeout(()=>ve(""),2500)}catch(I){X(I instanceof Error?I.message:"Save failed")}finally{qe(!1)}}async function Tn(I,te,ie){const De={...F,[I]:{...F[I]||{depart:0,stay:0,arrive:0},[te]:ie}};y(De);try{await Bh(I,te,ie),ee("Saved"),setTimeout(()=>ee(""),1500)}catch(xt){le(xt instanceof Error?xt.message:"Save failed")}}async function Vn(){_n(!0),tn(""),Ht("");try{await bh({warn_over_red_hrs:oe,warn_over_amber_hrs:se,warn_under_amber_hrs:We,warn_under_red_hrs:En}),Ht("Saved"),setTimeout(()=>Ht(""),2500)}catch(I){tn(I instanceof Error?I.message:"Save failed")}finally{_n(!1)}}return s?c.jsx("div",{style:{padding:"2rem",color:"var(--text-mid)"},children:"Loading…"}):c.jsxs("div",{style:{padding:"1.5rem",maxWidth:"680px"},children:[c.jsx("h1",{style:{fontSize:"1.1rem",fontWeight:700,color:"var(--text-dark)",marginBottom:"1.5rem"},children:"Settings"}),c.jsx(ol,{title:"Room Categories"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Drag to reorder. Toggle to exclude categories from the planner."}),w&&c.jsx(Bt,{type:"error",children:w}),P&&c.jsx(Bt,{type:"ok",children:P}),c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem",marginBottom:"1rem"},children:i.map((I,te)=>c.jsxs("div",{draggable:!0,onDragStart:ie=>Dt(ie,te),onDragOver:ie=>Pn(ie,te),onDragEnd:ne,style:{display:"flex",alignItems:"center",gap:"0.75rem",padding:"0.625rem 0.875rem",border:"1px solid var(--card-border)",borderRadius:"8px",background:I.excluded?"#f8fafc":"var(--card-bg)",opacity:M===te?.5:1},children:[c.jsx(Eh,{size:16,color:"var(--text-mid)",style:{cursor:"grab",flexShrink:0}}),c.jsx("span",{style:{flex:1,fontSize:"0.9rem",color:I.excluded?"var(--text-mid)":"var(--text-dark)",textDecoration:I.excluded?"line-through":"none"},children:I.name}),c.jsxs("span",{style:{fontSize:"0.75rem",color:"var(--text-mid)",marginRight:"0.5rem"},children:[I.room_count," rooms"]}),c.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"0.375rem",fontSize:"0.8rem",color:"var(--text-mid)"},children:[c.jsx("input",{type:"checkbox",checked:!I.excluded,onChange:()=>_t(te)}),"Active"]})]},I.id))}),c.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",marginBottom:"2.5rem"},children:[c.jsx(mr,{onClick:me,disabled:p,primary:!0,children:p?"Saving…":"Save Order & Visibility"}),c.jsx(mr,{onClick:Ae,disabled:g,children:g?"Testing…":"Test Newbook Connection"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Workforce Departments"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Select the department(s) whose shifts should appear in the HK staff rota."}),de&&c.jsxs("div",{style:{padding:"0.75rem",borderRadius:"8px",background:"#f8fafc",border:"1px solid var(--card-border)",color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"2rem"},children:["Workforce integration not configured — add the bearer token in ",c.jsx("strong",{children:"Settings → Integrations → Workforce"}),"."]}),!de&&$&&c.jsx("div",{style:{color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"2rem"},children:"Loading departments…"}),!de&&!$&&c.jsxs("div",{style:{marginBottom:"2.5rem"},children:[U&&c.jsx(Bt,{type:"error",children:U}),Ie&&c.jsx(Bt,{type:"ok",children:Ie}),K.length===0?c.jsx("div",{style:{color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"1rem"},children:"No departments found for this location."}):c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.4rem",marginBottom:"1rem"},children:K.map(I=>c.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"0.625rem",padding:"0.5rem 0.75rem",border:"1px solid var(--card-border)",borderRadius:"7px",background:E.includes(I.id)?"rgba(42,100,72,0.05)":"var(--card-bg)",cursor:"pointer",fontSize:"0.9rem",color:"var(--text-dark)"},children:[c.jsx("input",{type:"checkbox",checked:E.includes(I.id),onChange:()=>rt(I.id)}),I.name]},I.id))}),E.length===0&&K.length>0&&c.jsx("div",{style:{marginBottom:"0.75rem",fontSize:"0.8rem",color:"#b45309"},children:"Select at least one department to enable Workforce sync."}),c.jsx(mr,{onClick:ft,disabled:xe,primary:!0,children:xe?"Saving…":"Save Departments"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Recurring General Tasks"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Tasks that recur every week. Enter minutes per day. These add to the total required hours every week."}),W&&c.jsx(Bt,{type:"error",children:W}),Oe&&c.jsx(Bt,{type:"ok",children:Oe}),c.jsx("div",{style:{overflowX:"auto",marginBottom:"1rem"},children:c.jsxs("table",{className:"hk-table",style:{minWidth:"560px"},children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Task"}),Uc.map(I=>c.jsx("th",{children:I},I)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsxs("tbody",{children:[Ee.length===0&&c.jsx("tr",{children:c.jsx("td",{colSpan:9,style:{color:"var(--text-mid)",textAlign:"center",padding:"1rem"},children:"No tasks yet"})}),Ee.map((I,te)=>c.jsxs("tr",{children:[c.jsx("td",{style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:I.name,placeholder:"Task name",onChange:ie=>pt(te,ie.target.value)})}),Uc.map(ie=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:999,value:I.hours[ie]||"",placeholder:"0",onChange:De=>$e(te,ie,De.target.value)})},ie)),c.jsx("td",{children:c.jsx("button",{onClick:()=>we(te),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},te))]})]})}),c.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",marginBottom:"2.5rem"},children:[c.jsx(mr,{onClick:()=>Te([...Ee,{name:"",hours:{}}]),children:"+ Add task"}),c.jsx(mr,{onClick:yt,disabled:Be,primary:!0,children:Be?"Saving…":"Save Tasks"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Time Requirements (minutes per room)"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"How many minutes each room type takes depending on guest status."}),re&&c.jsx(Bt,{type:"error",children:re}),R&&c.jsx(Bt,{type:"ok",children:R}),c.jsx("div",{style:{overflowX:"auto",marginBottom:"2rem"},children:c.jsxs("table",{className:"hk-table",style:{maxWidth:"500px"},children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Category"}),c.jsx("th",{children:"Depart (mins)"}),c.jsx("th",{children:"Stay (mins)"}),c.jsx("th",{children:"Arrive (mins)"})]})}),c.jsx("tbody",{children:i.filter(I=>!I.excluded).map(I=>{const te=F[I.id]||{depart:0,stay:0,arrive:0};return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",children:I.name}),["depart","stay","arrive"].map(ie=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:999,value:te[ie]||"",placeholder:"0",onChange:De=>Tn(I.id,ie,parseInt(De.target.value,10)||0),onBlur:De=>Tn(I.id,ie,parseInt(De.target.value,10)||0)})},ie))]},I.id)})})]})}),c.jsx(jo,{}),c.jsx(ol,{title:"Warning Thresholds"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:'Controls the colour and icon shown on the "with Adjustments" row in the staff rota. All values are in hours.'}),Nn&&c.jsx(Bt,{type:"error",children:Nn}),en&&c.jsx(Bt,{type:"ok",children:en}),c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.625rem",marginBottom:"1.25rem",maxWidth:"420px"},children:[{label:"⚠ Red warning — spare over",value:oe,set:ae,help:"default 4h"},{label:"⚠ Amber warning — spare over",value:se,set:fe,help:"default 1h"},{label:"✗ Amber cross — short over",value:We,set:At,help:"default 1h"},{label:"✗ Red cross — short over",value:En,set:$t,help:"default 2h"}].map(({label:I,value:te,set:ie,help:De})=>c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[c.jsx("span",{style:{flex:1,fontSize:"0.875rem",color:"var(--text-dark)"},children:I}),c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:24,step:.5,value:te,onChange:xt=>ie(parseFloat(xt.target.value)||0),style:{width:"68px"}}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--text-mid)",minWidth:"52px"},children:De})]},I))}),c.jsx("p",{style:{fontSize:"0.78rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Green ✓ is automatic — shown when the difference is within the amber spare and short thresholds above."}),c.jsx("div",{style:{marginBottom:"2.5rem"},children:c.jsx(mr,{onClick:Vn,disabled:Vt,primary:!0,children:Vt?"Saving…":"Save Thresholds"})})]})}function ol({title:i}){return c.jsx("h2",{style:{fontSize:"0.95rem",fontWeight:700,color:"var(--text-dark)",marginBottom:"0.25rem"},children:i})}function jo(){return c.jsx("div",{style:{height:"1px",background:"var(--card-border)",margin:"0.5rem 0 2rem"}})}function Bt({type:i,children:u}){return c.jsx("div",{style:{marginBottom:"0.75rem",padding:"0.75rem",borderRadius:"8px",fontSize:"0.875rem",background:i==="ok"?"#dcfce7":"#fee2e2",color:i==="ok"?"var(--success)":"var(--danger)"},children:u})}function mr({children:i,onClick:u,disabled:s,primary:d}){return c.jsx("button",{onClick:u,disabled:s,style:{background:d?"var(--hk-green)":"var(--card-bg)",color:d?"#fff":"var(--text-dark)",border:`1px solid ${d?"var(--hk-green)":"var(--card-border)"}`,borderRadius:"6px",padding:"0.5rem 1.25rem",fontSize:"0.875rem",fontWeight:600},children:i})}function hm({user:i}){return c.jsx(Wh,{user:i,children:c.jsxs(qp,{children:[c.jsx(il,{path:"/",element:c.jsx(ws,{to:"/planner",replace:!0})}),c.jsx(il,{path:"/planner",element:c.jsx(lm,{})}),c.jsx(il,{path:"/settings",element:ed(i,"settings")?c.jsx(pm,{}):c.jsx(ws,{to:"/planner",replace:!0})}),c.jsx(il,{path:"*",element:c.jsx(ws,{to:"/planner",replace:!0})})]})})}function mm(){const i=Oh("/hk-planner/health");return c.jsxs(c.Fragment,{children:[c.jsx(ih,{basename:"/hk-planner",children:c.jsx(vh,{children:u=>c.jsx(hm,{user:u})})}),c.jsx(zh,{visible:i})]})}new URLSearchParams(window.location.search).has("install")&&window.addEventListener("beforeinstallprompt",i=>{i.preventDefault(),i.prompt()},{once:!0});ap.createRoot(document.getElementById("root")).render(c.jsx(S.StrictMode,{children:c.jsx(mm,{})})); + */const Th=[["path",{d:"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915",key:"1i5ecw"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],Rh=cl("settings",Th);function zh({visible:i}){return i?c.jsxs("div",{style:{position:"fixed",bottom:0,left:0,right:0,zIndex:9999,background:"var(--sidebar)",color:"var(--text-light)",display:"flex",alignItems:"center",justifyContent:"center",gap:"12px",padding:"10px 16px",fontSize:"14px",boxShadow:"0 -2px 8px rgba(0,0,0,0.3)"},children:[c.jsx("span",{children:"A new version is available."}),c.jsxs("button",{onClick:()=>window.location.reload(),style:{display:"flex",alignItems:"center",gap:"6px",background:"var(--accent)",color:"var(--sidebar)",border:"none",borderRadius:"4px",padding:"6px 14px",fontWeight:600,cursor:"pointer",fontSize:"13px"},children:[c.jsx(Ns,{size:14,strokeWidth:1.75}),"Reload"]})]}):null}const Lh=120*1e3;function Oh(i){const[u,s]=S.useState(!1);return S.useEffect(()=>{let d=null;async function p(){try{const j=await fetch(i,{cache:"no-store"});if(!j.ok)return;const _=(await j.json()).version;if(!_)return;d===null?d=_:_!==d&&s(!0)}catch{}}p();const h=setInterval(p,Lh);function g(){document.visibilityState==="visible"&&p()}return document.addEventListener("visibilitychange",g),()=>{clearInterval(h),document.removeEventListener("visibilitychange",g)}},[i]),u}function ed(i,u){return i.is_admin||i.caps.includes(u)}const Dh="width=1280",Mh="width=device-width, initial-scale=1.0";function Mc(i){const u=document.querySelector('meta[name="viewport"]');u&&(u.content=i)}function Ih(i){S.useEffect(()=>(Mc(Dh),window.parent.postMessage({type:"hnf:viewport",mode:i},"*"),()=>{Mc(Mh),window.parent.postMessage({type:"hnf:viewport",mode:"responsive"},"*")}),[i])}function Wh({user:i,children:u}){Ih("desktop");async function s(){await fetch("/hk-planner/api/auth/logout",{method:"POST",credentials:"include"}),window.location.reload()}return c.jsxs("div",{style:{display:"flex",height:"100dvh",overflow:"hidden"},children:[c.jsxs("nav",{style:{width:"200px",flexShrink:0,background:"var(--navy)",display:"flex",flexDirection:"column",padding:"1rem 0",borderRight:"1px solid var(--surface-2)"},children:[c.jsx("div",{style:{padding:"0 1rem 1rem",borderBottom:"1px solid var(--surface-2)"},children:c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem"},children:[c.jsx(Dc,{size:20,strokeWidth:1.75,color:"#74c69d"}),c.jsx("span",{style:{color:"#74c69d",fontWeight:700,fontSize:"0.95rem"},children:"HK Planner"})]})}),c.jsxs("div",{className:"nav-scroll",style:{flex:1,padding:"0.5rem 0",overflowY:"auto"},children:[c.jsx(Ic,{to:"/planner",icon:Dc,label:"Planner"}),ed(i,"settings")&&c.jsx(Ic,{to:"/settings",icon:Rh,label:"Settings"})]}),c.jsxs("div",{style:{padding:"0.75rem 1rem",borderTop:"1px solid var(--surface-2)"},children:[c.jsxs("div",{style:{marginBottom:"0.5rem"},children:[c.jsx("div",{style:{color:"var(--text)",fontSize:"0.8rem",fontWeight:600},children:i.name}),c.jsx("div",{style:{color:"var(--text-muted)",fontSize:"0.72rem"},children:i.email})]}),c.jsxs("button",{onClick:s,style:{display:"flex",alignItems:"center",gap:"0.5rem",background:"none",border:"none",color:"var(--text-muted)",fontSize:"0.8rem",padding:"0.375rem 0",width:"100%",cursor:"pointer"},children:[c.jsx(Nh,{size:14,strokeWidth:1.75}),"Sign out"]})]})]}),c.jsx("main",{style:{flex:1,overflow:"auto",background:"var(--body-bg)"},children:u})]})}function Ic({to:i,icon:u,label:s}){return c.jsxs(ch,{to:i,style:({isActive:d})=>({display:"flex",alignItems:"center",gap:"0.625rem",padding:"0.625rem 1rem",textDecoration:"none",color:d?"#74c69d":"var(--text)",background:d?"var(--surface)":"transparent",borderLeft:d?"2px solid #74c69d":"2px solid transparent",fontSize:"0.875rem",transition:"background 0.15s"}),children:[c.jsx(u,{size:15,strokeWidth:1.75}),s]})}const Fh="/hk-planner/api";async function Ze(i,u){const s=await fetch(Fh+i,{credentials:"include",...u});if(s.status===401)throw(window.top??window).location.href="/login",new Error("Unauthenticated");if(!s.ok){const d=await s.json().catch(()=>({}));throw new Error(d.error||`HTTP ${s.status}`)}return s.json()}function Uh(i,u,s=!1){const d=new URLSearchParams;return i&&d.set("week_start",i),u&&d.set("last_viewed",u),s&&d.set("force_refresh","1"),Ze(`/bookings?${d}`)}function td(){return Ze("/config")}function Bh(i,u,s){return Ze("/config/time-requirements",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({cat:i,action:u,value:s})})}function Ah(i){return Ze("/config/staff",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({staff_data:i})})}function $h(i){return Ze("/config/pickup",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({pickup_data:i})})}function Vh(i){return Ze("/config/general-tasks",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({general_tasks:i})})}function Hh(i){return Ze("/config/last-reviewed",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({date:i})})}function Qh(){return Ze("/categories")}function Kh(i,u){return Ze("/categories",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({order:i,excluded:u})})}function Yh(){return Ze("/newbook/test",{method:"POST"})}function Gh(){return Ze("/workforce/departments")}function Jh(i){return Ze("/config/workforce-departments",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({dept_ids:i})})}function Xh(){return Ze("/workforce/sync",{method:"POST"})}function Wc(i,u){return Ze(`/workforce/shifts?start=${i}&end=${u}`)}function Zh(){return Ze("/workforce/staff")}function qh(i){return Ze("/config/adjustments",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({adjustments:i})})}function bh(i){return Ze("/config/warning-thresholds",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})}function ko(){const i=new Date;return`${i.getFullYear()}-${String(i.getMonth()+1).padStart(2,"0")}-${String(i.getDate()).padStart(2,"0")}`}function wo(i,u){const[s,d,p]=i.split("-").map(Number),h=new Date(s,d-1,p);return h.setDate(h.getDate()+u),`${h.getFullYear()}-${String(h.getMonth()+1).padStart(2,"0")}-${String(h.getDate()).padStart(2,"0")}`}function To(i){const u=new Date(i+"T00:00:00");return["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][u.getDay()]+" "+u.getDate()+" "+["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][u.getMonth()]}function em(i){const u=new Date(i+"T00:00:00");return["Mon","Tue","Wed","Thu","Fri","Sat","Sun"][(u.getDay()+6)%7]}function tm(i){const u=new Date(i+"T00:00:00");return["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][u.getDay()]}function sl(i){const u=new Date(i+"T00:00:00").getDay();return u===0||u===6}function nm(i,u){const s=u.pickup_hint??0,d=u.pickup_lead??0,p=s>0?`${s} room${s===1?"":"s"} picked up within ${d} day${d===1?"":"s"} of arrival`:"no late pickups";return`Last ${tm(i)}: ${u.prior_occ} occ (${u.prior_vac} vac) — ${p}`}function Fc(i){return new Date(i+"T00:00:00").toLocaleDateString("en-GB",{weekday:"short",day:"numeric",month:"short"})}function Xe(i){return i.toFixed(2)+"h"}function gr(i,u,s,d,p){var _;const h=Math.max(0,p-d),g=(_=i[u])==null?void 0:_[s];if(!g||!g.count)return 0;const j=g.total-g.count,k=d>j?Math.max(0,g.total-d):g.count;return Math.min(k,h)}function rm(i,u,s,d,p){const h={};for(const g of i.dates){const j=em(g),k=d.reduce((P,N)=>P+(N.hours[j]||0)/60,0),_=p.reduce((P,N)=>P+(N.hours[g]||0),0);h[g]={booked:0,pickup:0,general:k,adjustments:_,total:k+_,by_cat:{},pickup_by_cat:{}}}for(const g of i.categories){const j=u[g.id]||{depart:0,stay:0,arrive:0};i.dates.forEach((k,_)=>{const P=g.days[k],N=P.stays+P.arrivals,M=gr(s,g.id,k,N,g.total_rooms),H=(P.departs*(j.depart||0)+P.stays*(j.stay||0)+P.arrivals*(j.arrive||0))/60,K=M*(j.arrive||0)/60;h[k].by_cat[g.id]=H,h[k].pickup_by_cat[g.id]=(h[k].pickup_by_cat[g.id]||0)+K,h[k].booked+=H,h[k].pickup+=K,h[k].total+=H+K;const T=i.dates[_+1];if(T&&M>0){const E=M*(j.depart||0)/60;h[T].pickup_by_cat[g.id]=(h[T].pickup_by_cat[g.id]||0)+E,h[T].pickup+=E,h[T].total+=E}})}return h}function lm(){const[i,u]=S.useState(null),[s,d]=S.useState({}),[p,h]=S.useState([]),[g,j]=S.useState({}),[k,_]=S.useState([]),[P,N]=S.useState([]),[M,H]=S.useState(4),[K,T]=S.useState(1),[E,A]=S.useState(1),[$,J]=S.useState(2),[U,ce]=S.useState({}),[de,Ce]=S.useState([]),[xe,qe]=S.useState(!1),[We,Ke]=S.useState(null),[Ee,Te]=S.useState(""),[Ae,be]=S.useState(""),[Oe,ve]=S.useState(!0),[W,X]=S.useState(""),[F,y]=S.useState(null),R=S.useRef({}),ee=S.useRef(null),re=S.useRef(!1),le=S.useRef(null);le.current=We;function oe(te,pe,De=400){clearTimeout(R.current[te]),R.current[te]=setTimeout(pe,De)}function ae(te,pe=!1){ee.current&&clearTimeout(ee.current),y({text:te,err:pe}),ee.current=setTimeout(()=>y(null),pe?15e3:2500)}function se(){const te=ko();te!==Ae&&(be(te),Hh(te).catch(()=>{}))}const fe=S.useCallback(async(te=!1,pe)=>{ve(!0),X("");const De=ko(),Ye=le.current||De,ft=wo(Ye,6),pt=pe!==void 0?pe||wo(De,-1):Ee||Ae||wo(De,-1);try{const[$e,ke,yt]=await Promise.all([Uh(Ye,pt,te),td(),Wc(Ye,ft).catch(()=>({}))]);u($e),d(ke.time_requirements||{}),h(ke.staff_data||[]),j(ke.pickup_data||{}),_(ke.general_tasks||[]),N(ke.adjustments||[]),H(ke.warn_over_red_hrs??4),T(ke.warn_over_amber_hrs??1),A(ke.warn_under_amber_hrs??1),J(ke.warn_under_red_hrs??2),ce(yt),ke.last_reviewed&&(be(ke.last_reviewed),Ee||Te(ke.last_reviewed))}catch($e){X($e instanceof Error?$e.message:"Failed to load data")}finally{ve(!1)}},[Ee,Ae]);S.useEffect(()=>{fe(!1)},[]),S.useEffect(()=>{Object.keys(U).length>0&&!re.current&&(re.current=!0,Zh().then(Ce).catch(()=>{}))},[U]),S.useEffect(()=>{function te(De,Ye){fetch(De,{method:"PUT",keepalive:!0,credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(Ye)})}function pe(){p.length&&te("/hk-planner/api/config/staff",{staff_data:p}),Object.keys(g).length&&te("/hk-planner/api/config/pickup",{pickup_data:g}),P.length&&te("/hk-planner/api/config/adjustments",{adjustments:P})}return window.addEventListener("beforeunload",pe),()=>window.removeEventListener("beforeunload",pe)},[p,g,P]);function Fe(te){const pe=ko(),De=wo(le.current||pe,te);le.current=De,Ke(De),se(),fe(!0)}function At(){le.current=null,Ke(null),se(),fe(!0)}function En(){se(),fe(!0)}function $t(te,pe,De,Ye){const ft=Ye.days[pe],pt=ft.stays+ft.arrivals,$e=Math.max(0,Ye.total_rooms-pt),ke=gr(g,te,pe,pt,Ye.total_rooms),yt=Math.max(0,Math.min(ke+De,$e)),Tn={...g,[te]:{...g[te]||{},[pe]:{count:yt,total:pt+yt}}};j(Tn),se(),oe("pickup",()=>$h(Tn).then(()=>ae("Pickup saved")).catch(Vn=>ae(Vn.message,!0)))}function Vt(te){N(te),oe("adjustments",()=>qh(te).then(()=>ae("Adjustments saved")).catch(pe=>ae(pe.message,!0)))}function _n(){Vt([...P,{label:"",hours:{}}])}function en(te){h(te),se(),oe("staff",()=>Ah(te).then(()=>ae("Staff hours saved")).catch(pe=>ae(pe.message,!0)))}function Ht(){en([...p,{name:"",hours:{}}])}async function Nn(){qe(!0);try{if(await Xh(),i){const te=await Wc(i.dates[0],i.dates[i.dates.length-1]);ce(te)}ae("Rota synced from Workforce")}catch(te){ae(te instanceof Error?te.message:"Sync failed",!0)}finally{qe(!1)}}function tn(){if(!i)return"";const te=i.dates.filter($e=>U[$e]);if(!te.length)return"Not synced";const pe=te.reduce(($e,ke)=>new Date(U[ke].synced_at)!U[$e]).length?`Partial sync — from ${Ye} ${ft}`:`From ${Ye} ${ft}`}const _t=i?rm(i,s,g,k,P):null,Dt=ko(),Pn=We||Dt;return c.jsxs("div",{style:{padding:"1.5rem",maxWidth:"1600px"},children:[c.jsxs("div",{style:{display:"flex",flexWrap:"wrap",alignItems:"center",gap:"0.75rem",marginBottom:"1.25rem"},children:[c.jsxs("div",{children:[c.jsx("h1",{style:{fontSize:"1.15rem",fontWeight:700,color:"var(--text-dark)"},children:"Housekeeping Planner"}),i&&c.jsxs("p",{style:{fontSize:"0.8rem",color:"var(--text-mid)",marginTop:"0.1rem"},children:[Fc(i.dates[0])," – ",Fc(i.dates[i.dates.length-1])]})]}),c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.4rem",marginLeft:"auto",flexWrap:"wrap"},children:[c.jsx(hr,{onClick:()=>Fe(-7),children:"‹ Prev"}),c.jsx(hr,{onClick:At,primary:!0,children:"Today"}),c.jsx(hr,{onClick:()=>Fe(7),children:"Next ›"}),c.jsx("input",{type:"date",value:Pn,onChange:te=>{if(te.target.value){const pe=te.target.value;le.current=pe,Ke(pe),se(),oe("weekStart",()=>fe(!0),600)}},style:{border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.35rem 0.5rem",fontSize:"0.82rem",color:"var(--text-dark)"}}),c.jsx("div",{style:{width:"1px",height:"24px",background:"var(--card-border)"}}),c.jsxs("label",{style:{fontSize:"0.78rem",color:"var(--text-mid)",display:"flex",alignItems:"center",gap:"0.3rem"},children:[c.jsx("span",{children:"Since"}),c.jsx("input",{type:"date",value:Ee,onChange:te=>{if(te.target.value){const pe=te.target.value;Te(pe),oe("lastViewed",()=>fe(!0,pe),600)}},style:{border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.3rem 0.5rem",fontSize:"0.78rem",color:"var(--text-dark)"}})]}),c.jsx(hr,{onClick:()=>{se(),fe(!0)},children:"Update to now"}),c.jsx("div",{style:{width:"1px",height:"24px",background:"var(--card-border)"}}),c.jsx("button",{onClick:En,title:"Refresh",style:{background:"var(--card-bg)",border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.35rem 0.5rem",display:"flex",alignItems:"center",gap:"0.3rem",fontSize:"0.82rem",color:"var(--text-dark)"},children:c.jsx(Ns,{size:13,strokeWidth:1.75,style:{animation:Oe?"spin 1s linear infinite":"none"}})})]})]}),c.jsx("style",{children:"@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }"}),F&&c.jsx("div",{style:{display:"inline-block",marginBottom:"0.75rem",padding:"0.35rem 0.75rem",borderRadius:"6px",fontSize:"0.8rem",fontWeight:500,background:F.err?"#fee2e2":"#dcfce7",color:F.err?"var(--danger)":"var(--success)"},children:F.text}),W&&c.jsxs("div",{style:{marginBottom:"1rem",padding:"0.75rem 1rem",borderRadius:"8px",background:"#fee2e2",color:"var(--danger)",fontSize:"0.875rem"},children:["Error: ",W]}),Oe&&!i&&c.jsx("div",{style:{color:"var(--text-mid)",padding:"2rem 0",textAlign:"center"},children:"Loading bookings…"}),i&&_t&&c.jsxs(c.Fragment,{children:[c.jsx(So,{title:"7-Day Occupancy",children:c.jsx("div",{className:"table-scroll",children:c.jsx(om,{bookings:i,pickup:g,onPickupChange:$t})})}),c.jsx(So,{title:"Required Hours",children:c.jsx("div",{className:"table-scroll",children:c.jsx(am,{bookings:i,required:_t})})}),c.jsx(So,{title:"Adjustments",action:c.jsx(hr,{onClick:_n,children:"+ Add adjustment"}),children:c.jsx("div",{className:"table-scroll",children:c.jsx(fm,{bookings:i,adjustments:P,onChange:Vt})})}),c.jsx(So,{title:"Staff Rota",action:c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.5rem",flexWrap:"wrap"},children:[c.jsxs("button",{onClick:Nn,disabled:xe,style:{display:"flex",alignItems:"center",gap:"0.3rem",background:"var(--card-bg)",color:"var(--text-dark)",border:"1px solid var(--card-border)",borderRadius:"6px",padding:"0.3rem 0.65rem",fontSize:"0.78rem",fontWeight:600},children:[c.jsx(Ns,{size:11,strokeWidth:1.75,style:{animation:xe?"spin 1s linear infinite":"none"}}),xe?"Syncing…":"Sync from Workforce"]}),c.jsx("span",{style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:tn()}),c.jsx(hr,{onClick:Ht,children:"+ Add staff"})]}),children:c.jsx("div",{className:"table-scroll",children:c.jsx(cm,{bookings:i,staff:p,required:_t,warnOverRed:M,warnOverAmber:K,warnUnderAmber:E,warnUnderRed:$,onChange:en,wfShifts:U,wfStaff:de})})})]})]})}function So({title:i,children:u,action:s}){return c.jsxs("div",{style:{marginBottom:"1.5rem"},children:[c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem",marginBottom:"0.5rem"},children:[c.jsx("h2",{style:{fontSize:"0.72rem",fontWeight:700,color:"var(--text-mid)",textTransform:"uppercase",letterSpacing:"0.08em"},children:i}),s]}),u]})}function hr({children:i,onClick:u,primary:s}){return c.jsx("button",{onClick:u,style:{background:s?"var(--hk-green)":"var(--card-bg)",color:s?"#fff":"var(--text-dark)",border:`1px solid ${s?"var(--hk-green)":"var(--card-border)"}`,borderRadius:"6px",padding:"0.35rem 0.75rem",fontSize:"0.82rem",fontWeight:600},children:i})}function om({bookings:i,pickup:u,onPickupChange:s}){const{dates:d,categories:p}=i;return c.jsxs("table",{className:"hk-table",children:[c.jsxs("thead",{children:[c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",rowSpan:2,children:"Category"}),d.map(h=>c.jsx("th",{colSpan:2,style:{borderLeft:"2px solid rgba(255,255,255,0.3)",...sl(h)?{background:"#253555"}:{}},children:To(h)},h))]}),c.jsx("tr",{children:d.map(h=>c.jsxs(c.Fragment,{children:[c.jsx("th",{style:{fontSize:"0.72rem",fontWeight:400,minWidth:"72px",background:sl(h)?"#2a3d5e":"#1e2d42",borderLeft:"2px solid rgba(255,255,255,0.3)"},children:"Rooms"},h+"-r"),c.jsx("th",{style:{fontSize:"0.72rem",fontWeight:400,minWidth:"56px",textAlign:"center",background:sl(h)?"#2a3d5e":"#1e2d42",borderLeft:"1px solid rgba(255,255,255,0.1)"},children:"D / S / A"},h+"-d")]}))})]}),c.jsx("tbody",{children:p.map((h,g)=>{const j=g===p.length-1;return c.jsx(im,{cat:h,dates:d,pickup:u,onPickupChange:(k,_)=>s(h.id,k,_,h),isLast:j},h.id)})}),c.jsx("tfoot",{children:c.jsx(sm,{dates:d,cats:p,pickup:u})})]})}function im({cat:i,dates:u,pickup:s,onPickupChange:d,isLast:p}){const h=p?"2px solid var(--card-border)":void 0;return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{borderBottom:h},children:i.name}),u.map((g,j)=>{var ce;const k=i.days[g],_=k.stays+k.arrivals,P=Math.max(0,i.total_rooms-_),N=gr(s,i.id,g,_,i.total_rooms),M=u[j-1],H=M?i.days[M].stays+i.days[M].arrivals:0,K=M?gr(s,i.id,M,H,i.total_rooms):0,T=k.delta_new||0,E=k.delta_cancelled||0,A=sl(g),$=(ce=s[i.id])==null?void 0:ce[g],J=nm(g,k);let U="";if($&&$.count){const de=$.total-$.count,Ce=_>de?"changed":"unchanged",xe=_>de?"decreased":"remains";U=`+${$.count} pickup set when booked was ${de} (target ${$.total}). Booked ${Ce}, so pickup ${xe}.`}return U=(U?U+" | ":"")+J,c.jsxs(c.Fragment,{children:[c.jsxs("td",{style:{textAlign:"center",verticalAlign:"middle",borderBottom:h,padding:"0.4rem 0.3rem",borderLeft:"2px solid var(--card-border)",...A?{background:"rgba(100,120,160,0.07)"}:{}},children:[(T>0||E>0)&&c.jsxs("div",{style:{marginBottom:"2px",fontSize:"0.72rem"},children:[T>0&&c.jsxs("span",{className:"delta-new",children:["▲",T]}),E>0&&c.jsxs("span",{className:"delta-canc",children:[" ▼",E]})]}),c.jsxs("div",{style:{fontWeight:700,fontSize:"1.1rem"},title:U,children:[_,N>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",N]})]}),c.jsxs("div",{style:{fontSize:"0.72rem",color:"var(--text-mid)",marginBottom:"4px"},children:[P," vac",N>0&&c.jsxs("span",{className:"pickup-tag",children:[" (",P-N,")"]})]}),c.jsxs("div",{className:"pickup-ctrl",children:[c.jsx("button",{className:"pickup-btn",disabled:N<=0,onClick:()=>d(g,-1),children:"−"}),c.jsx("span",{className:"pickup-num",children:N}),c.jsx("button",{className:"pickup-btn",disabled:N>=P,title:J,onClick:()=>d(g,1),children:"+"})]})]},g+"-r"),c.jsxs("td",{style:{verticalAlign:"middle",borderLeft:"1px solid var(--card-border)",borderBottom:h,padding:"0.4rem 0.3rem",textAlign:"center",...A?{background:"rgba(100,120,160,0.07)"}:{}},children:[c.jsxs("div",{style:{fontSize:"0.8rem",color:"#c2502e",fontWeight:600},children:[k.departs,"d",K>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",K]})]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1d6fb8",fontWeight:600},children:[k.stays,"s"]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1a7a4a",fontWeight:600},children:[k.arrivals,"a",N>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",N]})]})]},g+"-dsa")]})})]})}function sm({dates:i,cats:u,pickup:s}){return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontStyle:"italic"},children:"Total"}),i.map((d,p)=>{let h=0,g=0,j=0,k=0,_=0,P=0,N=0,M=0,H=0;for(const T of u){const E=T.days[d],A=E.stays+E.arrivals,$=Math.max(0,T.total_rooms-A),J=gr(s,T.id,d,A,T.total_rooms),U=i[p-1],ce=U?T.days[U].stays+T.days[U].arrivals:0,de=U?gr(s,T.id,U,ce,T.total_rooms):0;h+=A,g+=$,j+=J,k+=de,_+=E.departs,P+=E.stays,N+=E.arrivals,M+=E.delta_new||0,H+=E.delta_cancelled||0}const K=sl(d);return c.jsxs(c.Fragment,{children:[c.jsxs("td",{style:{textAlign:"center",verticalAlign:"middle",padding:"0.4rem 0.3rem",borderLeft:"2px solid var(--card-border)",...K?{background:"rgba(100,120,160,0.07)"}:{}},children:[(M>0||H>0)&&c.jsxs("div",{style:{marginBottom:"2px",fontSize:"0.72rem"},children:[M>0&&c.jsxs("span",{className:"delta-new",children:["▲",M]}),H>0&&c.jsxs("span",{className:"delta-canc",children:[" ▼",H]})]}),c.jsxs("div",{style:{fontWeight:700,fontSize:"1.1rem"},children:[h,j>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",j]})]}),c.jsxs("div",{style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:[g," vac",j>0&&c.jsxs("span",{className:"pickup-tag",children:[" (",g-j,")"]})]})]},d+"-r"),c.jsxs("td",{style:{verticalAlign:"middle",borderLeft:"1px solid var(--card-border)",padding:"0.4rem 0.3rem",textAlign:"center",...K?{background:"rgba(100,120,160,0.07)"}:{}},children:[c.jsxs("div",{style:{fontSize:"0.8rem",color:"#c2502e",fontWeight:600},children:[_,"d",k>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",k]})]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1d6fb8",fontWeight:600},children:[P,"s"]}),c.jsxs("div",{style:{fontSize:"0.8rem",color:"#1a7a4a",fontWeight:600},children:[N,"a",j>0&&c.jsxs("span",{className:"pickup-tag",children:[" +",j]})]})]},d+"-dsa")]})})]})}function am({bookings:i,required:u}){const{dates:s,categories:d}=i;return c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Category"}),s.map(p=>c.jsx("th",{children:To(p)},p))]})}),c.jsx("tbody",{children:d.map(p=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",children:p.name}),s.map(h=>{const g=u[h].by_cat[p.id]||0,j=u[h].pickup_by_cat[p.id]||0;return c.jsxs("td",{children:[g===0&&j===0?"—":Xe(g),j>.001&&c.jsxs("span",{className:"pickup-tag",children:[" (+",Xe(j),")"]})]},h)})]},p.id))}),c.jsxs("tfoot",{children:[c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Booked hrs"}),s.map(p=>c.jsx("td",{children:Xe(u[p].booked)},p))]}),c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Pickup hrs"}),s.map(p=>c.jsx("td",{children:u[p].pickup>.001?Xe(u[p].pickup):"—"},p))]}),c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Recurring tasks"}),s.map(p=>c.jsx("td",{children:u[p].general>.001?Xe(u[p].general):"—"},p))]}),s.some(p=>u[p].adjustments!==0)&&c.jsxs("tr",{style:{background:"#f1f5f9"},children:[c.jsx("td",{className:"col-label",style:{color:"var(--text-mid)",fontSize:"0.78rem"},children:"Adjustments"}),s.map(p=>{const h=u[p].adjustments;return c.jsx("td",{style:{color:h<0?"var(--danger)":h>0?"#1a7a4a":"var(--text-mid)"},children:h===0?"—":(h>0?"+":"")+Xe(h)},p)})]}),c.jsxs("tr",{style:{background:"#e2e8f0"},children:[c.jsx("td",{className:"col-label",children:"Total Required"}),s.map(p=>c.jsxs("td",{style:{fontWeight:700},children:[Xe(u[p].total),u[p].pickup>.001&&c.jsxs("span",{className:"pickup-tag",style:{display:"block",fontSize:"0.7rem"},children:["inc ",Xe(u[p].pickup)," pickup"]})]},p))]})]})]})}function um(i,u){const s={};for(const d of u){const p=i[d];if(p)for(const h of p.staff)s[h.id]||(s[h.id]={id:h.id,name:h.name,days:{}}),s[h.id].days[d]={hours:h.hours,times:h.times}}return Object.values(s).sort((d,p)=>d.name.localeCompare(p.name))}function cm({bookings:i,staff:u,required:s,warnOverRed:d,warnOverAmber:p,warnUnderAmber:h,warnUnderRed:g,onChange:j,wfShifts:k,wfStaff:_}){const{dates:P}=i;function N(T,E){const A=u.map(($,J)=>J===T?{...$,name:E}:$);j(A)}function M(T,E,A){const $=parseFloat(A),J=u.map((U,ce)=>{if(ce!==T)return U;const de={...U.hours};return!isNaN($)&&$>=0?de[E]=$:delete de[E],{...U,hours:de}});j(J)}function H(T){j(u.filter((E,A)=>A!==T))}const K=um(k,P);return c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Staff Member"}),P.map(T=>c.jsx("th",{children:To(T)},T)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsxs("tbody",{children:[K.map(T=>c.jsxs("tr",{style:{background:"rgba(42,100,72,0.05)"},children:[c.jsxs("td",{className:"col-label",children:[c.jsx("span",{style:{display:"inline-block",fontSize:"0.67rem",fontWeight:700,background:"rgba(42,100,72,0.18)",color:"#1a7a4a",borderRadius:"3px",padding:"0 4px",marginRight:"0.4rem",lineHeight:"1.5"},children:"WF"}),T.name]}),P.map(E=>{const A=T.days[E];return c.jsx("td",{style:{textAlign:"center",padding:"0.3rem 0.4rem",verticalAlign:"middle"},children:A?c.jsxs(c.Fragment,{children:[c.jsx("div",{style:{fontSize:"0.68rem",color:"var(--text-mid)",lineHeight:1.25},children:A.times}),c.jsx("div",{style:{fontWeight:600},children:Xe(A.hours)})]}):c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"})},E)}),c.jsx("td",{})]},"wf-"+T.id)),_.length>0&&c.jsx("datalist",{id:"wf-staff-datalist",children:_.map(T=>c.jsx("option",{value:T.name},T.id))}),u.map((T,E)=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:T.name,placeholder:"Staff name",list:_.length>0?"wf-staff-datalist":void 0,onChange:A=>N(E,A.target.value)})}),P.map(A=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:24,step:.5,value:T.hours[A]??"",placeholder:"0",onChange:$=>M(E,A,$.target.value)})},A)),c.jsx("td",{children:c.jsx("button",{onClick:()=>H(E),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},E))]}),c.jsx("tfoot",{children:(()=>{const T={};for(const E of P){const A=K.reduce((J,U)=>{var ce;return J+(((ce=U.days[E])==null?void 0:ce.hours)||0)},0),$=u.reduce((J,U)=>J+(U.hours[E]||0),0);T[E]=A+$}return c.jsxs(c.Fragment,{children:[c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.78rem",color:"var(--text-mid)"},children:"Total Available"}),P.map(E=>c.jsx("td",{children:Xe(T[E])},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"vs Booked"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"with Recurring Tasks"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked+s[E].general,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.72rem",color:"var(--text-mid)"},children:"with Pickup"}),P.map(E=>c.jsx("td",{children:c.jsx(Ss,{available:T[E],required:s[E].booked+s[E].general+s[E].pickup,warnOverAmber:p,warnUnderAmber:h})},E)),c.jsx("td",{})]}),c.jsxs("tr",{style:{background:"#e2e8f0"},children:[c.jsx("td",{className:"col-label",style:{fontWeight:700},children:"with Adjustments"}),P.map(E=>c.jsx("td",{children:c.jsx(dm,{available:T[E],required:s[E].total,warnOverRed:d,warnOverAmber:p,warnUnderAmber:h,warnUnderRed:g})},E)),c.jsx("td",{})]})]})})()})]})}function Ss({available:i,required:u,warnOverAmber:s,warnUnderAmber:d}){if(i===0&&u===0)return c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"});const p=i-u,h=p>s?"⚠":p<-d?"✗":"✓";return c.jsxs("span",{style:{color:"var(--text-mid)",fontSize:"0.82rem"},children:[h," ",p>=0?"+":"",Xe(p)]})}function dm({available:i,required:u,warnOverRed:s,warnOverAmber:d,warnUnderAmber:p,warnUnderRed:h}){if(i===0&&u===0)return c.jsx("span",{style:{color:"var(--text-mid)"},children:"—"});const g=i-u;return g>s?c.jsxs("span",{className:"diff-over-red",children:["⚠ ",Xe(g)," spare"]}):g>d?c.jsxs("span",{className:"diff-over",children:["⚠ ",Xe(g)," spare"]}):g<-h?c.jsxs("span",{className:"diff-under",children:["✗ ",Xe(Math.abs(g))," short"]}):g<-p?c.jsxs("span",{className:"diff-under-amber",children:["✗ ",Xe(Math.abs(g))," short"]}):c.jsxs("span",{className:"diff-ok",children:["✓ ",g>=0?"+":"",Xe(g)]})}function fm({bookings:i,adjustments:u,onChange:s}){const{dates:d}=i;function p(j,k){s(u.map((_,P)=>P===j?{..._,label:k}:_))}function h(j,k,_){const P=parseFloat(_);s(u.map((N,M)=>{if(M!==j)return N;const H={...N.hours};return isNaN(P)?delete H[k]:H[k]=P,{...N,hours:H}}))}function g(j){s(u.filter((k,_)=>_!==j))}return u.length===0?c.jsx("p",{style:{color:"var(--text-mid)",fontSize:"0.82rem",padding:"0.5rem 0"},children:'No adjustments — use "+ Add adjustment" above to add a one-off hour offset for a specific date.'}):c.jsxs("table",{className:"hk-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Label"}),d.map(j=>c.jsx("th",{children:To(j)},j)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsx("tbody",{children:u.map((j,k)=>c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:j.label,placeholder:"e.g. Rooms from Sunday",onChange:_=>p(k,_.target.value)})}),d.map(_=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",step:.25,value:j.hours[_]??"",placeholder:"0",onChange:P=>h(k,_,P.target.value)})},_)),c.jsx("td",{children:c.jsx("button",{onClick:()=>g(k),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},k))}),c.jsx("tfoot",{children:c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",style:{fontSize:"0.78rem",color:"var(--text-mid)"},children:"Total"}),d.map(j=>{const k=u.reduce((_,P)=>_+(P.hours[j]||0),0);return c.jsx("td",{style:{color:k<0?"var(--danger)":k>0?"#1a7a4a":"var(--text-mid)",fontWeight:k!==0?600:void 0},children:k===0?"—":(k>0?"+":"")+Xe(k)},j)}),c.jsx("td",{})]})})]})}const Uc=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];function pm(){const[i,u]=S.useState([]),[s,d]=S.useState(!0),[p,h]=S.useState(!1),[g,j]=S.useState(!1),[k,_]=S.useState(""),[P,N]=S.useState(""),[M,H]=S.useState(null),[K,T]=S.useState([]),[E,A]=S.useState([]),[$,J]=S.useState(!0),[U,ce]=S.useState(""),[de,Ce]=S.useState(!1),[xe,qe]=S.useState(!1),[We,Ke]=S.useState(""),[Ee,Te]=S.useState([]),[Ae,be]=S.useState(!1),[Oe,ve]=S.useState(""),[W,X]=S.useState(""),[F,y]=S.useState({}),[R,ee]=S.useState(""),[re,le]=S.useState(""),[oe,ae]=S.useState(4),[se,fe]=S.useState(1),[Fe,At]=S.useState(1),[En,$t]=S.useState(2),[Vt,_n]=S.useState(!1),[en,Ht]=S.useState(""),[Nn,tn]=S.useState("");S.useEffect(()=>{Promise.all([Qh(),td(),Gh().catch(I=>(I.message.includes("503")||I.message.toLowerCase().includes("not configured")?Ce(!0):ce(I.message),null))]).then(([I,ne,ie])=>{u(I.categories),Te(ne.general_tasks||[]),y(ne.time_requirements||{}),A(ne.workforce_departments||[]),ae(ne.warn_over_red_hrs??4),fe(ne.warn_over_amber_hrs??1),At(ne.warn_under_amber_hrs??1),$t(ne.warn_under_red_hrs??2),ie&&T(ie),d(!1),J(!1)}).catch(I=>{_(I.message),d(!1),J(!1)})},[]);function _t(I){u(i.map((ne,ie)=>ie===I?{...ne,excluded:!ne.excluded}:ne))}function Dt(I,ne){H(ne),I.dataTransfer.effectAllowed="move"}function Pn(I,ne){if(I.preventDefault(),M===null||M===ne)return;const ie=[...i],[Me]=ie.splice(M,1);ie.splice(ne,0,Me),u(ie),H(ne)}function te(){H(null)}async function pe(){h(!0),_(""),N("");try{await Kh(i.map(I=>I.id),i.filter(I=>I.excluded).map(I=>I.id)),N("Saved"),setTimeout(()=>N(""),2500)}catch(I){_(I instanceof Error?I.message:"Save failed")}finally{h(!1)}}async function De(){j(!0),_(""),N("");try{const I=await Yh();N(I.ok?`Connection OK: ${I.message||""}`:`Failed: ${I.error||"unknown"}`)}catch(I){_(I instanceof Error?I.message:"Test failed")}finally{j(!1)}}function Ye(I){A(ne=>ne.includes(I)?ne.filter(ie=>ie!==I):[...ne,I])}async function ft(){qe(!0),ce(""),Ke("");try{await Jh(E),Ke("Departments saved"),setTimeout(()=>Ke(""),2500)}catch(I){ce(I instanceof Error?I.message:"Save failed")}finally{qe(!1)}}function pt(I,ne){Te(Ee.map((ie,Me)=>Me===I?{...ie,name:ne}:ie))}function $e(I,ne,ie){const Me=parseInt(ie,10);Te(Ee.map((xt,wr)=>wr!==I?xt:{...xt,hours:{...xt.hours,[ne]:isNaN(Me)?0:Math.max(0,Me)}}))}function ke(I){Te(Ee.filter((ne,ie)=>ie!==I))}async function yt(){be(!0),X(""),ve("");try{await Vh(Ee),ve("Saved"),setTimeout(()=>ve(""),2500)}catch(I){X(I instanceof Error?I.message:"Save failed")}finally{be(!1)}}async function Tn(I,ne,ie){const Me={...F,[I]:{...F[I]||{depart:0,stay:0,arrive:0},[ne]:ie}};y(Me);try{await Bh(I,ne,ie),ee("Saved"),setTimeout(()=>ee(""),1500)}catch(xt){le(xt instanceof Error?xt.message:"Save failed")}}async function Vn(){_n(!0),tn(""),Ht("");try{await bh({warn_over_red_hrs:oe,warn_over_amber_hrs:se,warn_under_amber_hrs:Fe,warn_under_red_hrs:En}),Ht("Saved"),setTimeout(()=>Ht(""),2500)}catch(I){tn(I instanceof Error?I.message:"Save failed")}finally{_n(!1)}}return s?c.jsx("div",{style:{padding:"2rem",color:"var(--text-mid)"},children:"Loading…"}):c.jsxs("div",{style:{padding:"1.5rem",maxWidth:"680px"},children:[c.jsx("h1",{style:{fontSize:"1.1rem",fontWeight:700,color:"var(--text-dark)",marginBottom:"1.5rem"},children:"Settings"}),c.jsx(ol,{title:"Room Categories"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Drag to reorder. Toggle to exclude categories from the planner."}),k&&c.jsx(Bt,{type:"error",children:k}),P&&c.jsx(Bt,{type:"ok",children:P}),c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem",marginBottom:"1rem"},children:i.map((I,ne)=>c.jsxs("div",{draggable:!0,onDragStart:ie=>Dt(ie,ne),onDragOver:ie=>Pn(ie,ne),onDragEnd:te,style:{display:"flex",alignItems:"center",gap:"0.75rem",padding:"0.625rem 0.875rem",border:"1px solid var(--card-border)",borderRadius:"8px",background:I.excluded?"#f8fafc":"var(--card-bg)",opacity:M===ne?.5:1},children:[c.jsx(Eh,{size:16,color:"var(--text-mid)",style:{cursor:"grab",flexShrink:0}}),c.jsx("span",{style:{flex:1,fontSize:"0.9rem",color:I.excluded?"var(--text-mid)":"var(--text-dark)",textDecoration:I.excluded?"line-through":"none"},children:I.name}),c.jsxs("span",{style:{fontSize:"0.75rem",color:"var(--text-mid)",marginRight:"0.5rem"},children:[I.room_count," rooms"]}),c.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"0.375rem",fontSize:"0.8rem",color:"var(--text-mid)"},children:[c.jsx("input",{type:"checkbox",checked:!I.excluded,onChange:()=>_t(ne)}),"Active"]})]},I.id))}),c.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",marginBottom:"2.5rem"},children:[c.jsx(mr,{onClick:pe,disabled:p,primary:!0,children:p?"Saving…":"Save Order & Visibility"}),c.jsx(mr,{onClick:De,disabled:g,children:g?"Testing…":"Test Newbook Connection"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Workforce Departments"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Select the department(s) whose shifts should appear in the HK staff rota."}),de&&c.jsxs("div",{style:{padding:"0.75rem",borderRadius:"8px",background:"#f8fafc",border:"1px solid var(--card-border)",color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"2rem"},children:["Workforce integration not configured — add the bearer token in ",c.jsx("strong",{children:"Settings → Integrations → Workforce"}),"."]}),!de&&$&&c.jsx("div",{style:{color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"2rem"},children:"Loading departments…"}),!de&&!$&&c.jsxs("div",{style:{marginBottom:"2.5rem"},children:[U&&c.jsx(Bt,{type:"error",children:U}),We&&c.jsx(Bt,{type:"ok",children:We}),K.length===0?c.jsx("div",{style:{color:"var(--text-mid)",fontSize:"0.85rem",marginBottom:"1rem"},children:"No departments found for this location."}):c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.4rem",marginBottom:"1rem"},children:K.map(I=>c.jsxs("label",{style:{display:"flex",alignItems:"center",gap:"0.625rem",padding:"0.5rem 0.75rem",border:"1px solid var(--card-border)",borderRadius:"7px",background:E.includes(I.id)?"rgba(42,100,72,0.05)":"var(--card-bg)",cursor:"pointer",fontSize:"0.9rem",color:"var(--text-dark)"},children:[c.jsx("input",{type:"checkbox",checked:E.includes(I.id),onChange:()=>Ye(I.id)}),I.name]},I.id))}),E.length===0&&K.length>0&&c.jsx("div",{style:{marginBottom:"0.75rem",fontSize:"0.8rem",color:"#b45309"},children:"Select at least one department to enable Workforce sync."}),c.jsx(mr,{onClick:ft,disabled:xe,primary:!0,children:xe?"Saving…":"Save Departments"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Recurring General Tasks"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Tasks that recur every week. Enter minutes per day. These add to the total required hours every week."}),W&&c.jsx(Bt,{type:"error",children:W}),Oe&&c.jsx(Bt,{type:"ok",children:Oe}),c.jsx("div",{style:{overflowX:"auto",marginBottom:"1rem"},children:c.jsxs("table",{className:"hk-table",style:{minWidth:"560px"},children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Task"}),Uc.map(I=>c.jsx("th",{children:I},I)),c.jsx("th",{style:{width:"32px"}})]})}),c.jsxs("tbody",{children:[Ee.length===0&&c.jsx("tr",{children:c.jsx("td",{colSpan:9,style:{color:"var(--text-mid)",textAlign:"center",padding:"1rem"},children:"No tasks yet"})}),Ee.map((I,ne)=>c.jsxs("tr",{children:[c.jsx("td",{style:{padding:"0.3rem 0.5rem"},children:c.jsx("input",{className:"hk-text-input",value:I.name,placeholder:"Task name",onChange:ie=>pt(ne,ie.target.value)})}),Uc.map(ie=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:999,value:I.hours[ie]||"",placeholder:"0",onChange:Me=>$e(ne,ie,Me.target.value)})},ie)),c.jsx("td",{children:c.jsx("button",{onClick:()=>ke(ne),style:{background:"none",border:"none",color:"var(--text-mid)",fontSize:"1rem",padding:"0.2rem 0.4rem"},children:"×"})})]},ne))]})]})}),c.jsxs("div",{style:{display:"flex",gap:"0.75rem",flexWrap:"wrap",marginBottom:"2.5rem"},children:[c.jsx(mr,{onClick:()=>Te([...Ee,{name:"",hours:{}}]),children:"+ Add task"}),c.jsx(mr,{onClick:yt,disabled:Ae,primary:!0,children:Ae?"Saving…":"Save Tasks"})]}),c.jsx(jo,{}),c.jsx(ol,{title:"Time Requirements (minutes per room)"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"How many minutes each room type takes depending on guest status."}),re&&c.jsx(Bt,{type:"error",children:re}),R&&c.jsx(Bt,{type:"ok",children:R}),c.jsx("div",{style:{overflowX:"auto",marginBottom:"2rem"},children:c.jsxs("table",{className:"hk-table",style:{maxWidth:"500px"},children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{className:"col-label",children:"Category"}),c.jsx("th",{children:"Depart (mins)"}),c.jsx("th",{children:"Stay (mins)"}),c.jsx("th",{children:"Arrive (mins)"})]})}),c.jsx("tbody",{children:i.filter(I=>!I.excluded).map(I=>{const ne=F[I.id]||{depart:0,stay:0,arrive:0};return c.jsxs("tr",{children:[c.jsx("td",{className:"col-label",children:I.name}),["depart","stay","arrive"].map(ie=>c.jsx("td",{style:{padding:"0.3rem 0.4rem"},children:c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:999,value:ne[ie]||"",placeholder:"0",onChange:Me=>Tn(I.id,ie,parseInt(Me.target.value,10)||0),onBlur:Me=>Tn(I.id,ie,parseInt(Me.target.value,10)||0)})},ie))]},I.id)})})]})}),c.jsx(jo,{}),c.jsx(ol,{title:"Warning Thresholds"}),c.jsx("p",{style:{fontSize:"0.82rem",color:"var(--text-mid)",marginBottom:"1rem"},children:'Controls the colour and icon shown on the "with Adjustments" row in the staff rota. All values are in hours.'}),Nn&&c.jsx(Bt,{type:"error",children:Nn}),en&&c.jsx(Bt,{type:"ok",children:en}),c.jsx("div",{style:{display:"flex",flexDirection:"column",gap:"0.625rem",marginBottom:"1.25rem",maxWidth:"420px"},children:[{label:"⚠ Red warning — spare over",value:oe,set:ae,help:"default 4h"},{label:"⚠ Amber warning — spare over",value:se,set:fe,help:"default 1h"},{label:"✗ Amber cross — short over",value:Fe,set:At,help:"default 1h"},{label:"✗ Red cross — short over",value:En,set:$t,help:"default 2h"}].map(({label:I,value:ne,set:ie,help:Me})=>c.jsxs("div",{style:{display:"flex",alignItems:"center",gap:"0.75rem"},children:[c.jsx("span",{style:{flex:1,fontSize:"0.875rem",color:"var(--text-dark)"},children:I}),c.jsx("input",{type:"number",className:"hk-num-input",min:0,max:24,step:.5,value:ne,onChange:xt=>ie(parseFloat(xt.target.value)||0),style:{width:"68px"}}),c.jsx("span",{style:{fontSize:"0.75rem",color:"var(--text-mid)",minWidth:"52px"},children:Me})]},I))}),c.jsx("p",{style:{fontSize:"0.78rem",color:"var(--text-mid)",marginBottom:"1rem"},children:"Green ✓ is automatic — shown when the difference is within the amber spare and short thresholds above."}),c.jsx("div",{style:{marginBottom:"2.5rem"},children:c.jsx(mr,{onClick:Vn,disabled:Vt,primary:!0,children:Vt?"Saving…":"Save Thresholds"})})]})}function ol({title:i}){return c.jsx("h2",{style:{fontSize:"0.95rem",fontWeight:700,color:"var(--text-dark)",marginBottom:"0.25rem"},children:i})}function jo(){return c.jsx("div",{style:{height:"1px",background:"var(--card-border)",margin:"0.5rem 0 2rem"}})}function Bt({type:i,children:u}){return c.jsx("div",{style:{marginBottom:"0.75rem",padding:"0.75rem",borderRadius:"8px",fontSize:"0.875rem",background:i==="ok"?"#dcfce7":"#fee2e2",color:i==="ok"?"var(--success)":"var(--danger)"},children:u})}function mr({children:i,onClick:u,disabled:s,primary:d}){return c.jsx("button",{onClick:u,disabled:s,style:{background:d?"var(--hk-green)":"var(--card-bg)",color:d?"#fff":"var(--text-dark)",border:`1px solid ${d?"var(--hk-green)":"var(--card-border)"}`,borderRadius:"6px",padding:"0.5rem 1.25rem",fontSize:"0.875rem",fontWeight:600},children:i})}function hm({user:i}){return c.jsx(Wh,{user:i,children:c.jsxs(qp,{children:[c.jsx(il,{path:"/",element:c.jsx(ks,{to:"/planner",replace:!0})}),c.jsx(il,{path:"/planner",element:c.jsx(lm,{})}),c.jsx(il,{path:"/settings",element:ed(i,"settings")?c.jsx(pm,{}):c.jsx(ks,{to:"/planner",replace:!0})}),c.jsx(il,{path:"*",element:c.jsx(ks,{to:"/planner",replace:!0})})]})})}function mm(){const i=Oh("/hk-planner/health");return c.jsxs(c.Fragment,{children:[c.jsx(ih,{basename:"/hk-planner",children:c.jsx(vh,{children:u=>c.jsx(hm,{user:u})})}),c.jsx(zh,{visible:i})]})}new URLSearchParams(window.location.search).has("install")&&window.addEventListener("beforeinstallprompt",i=>{i.preventDefault(),i.prompt()},{once:!0});ap.createRoot(document.getElementById("root")).render(c.jsx(S.StrictMode,{children:c.jsx(mm,{})})); diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 9d10c49..f1ed10c 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -5,7 +5,7 @@ HK Planner - + diff --git a/frontend/dist/sw.js b/frontend/dist/sw.js index 9ec617f..c06c422 100644 --- a/frontend/dist/sw.js +++ b/frontend/dist/sw.js @@ -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),l={module:{uri:o},exports:t,require:c};i[o]=Promise.all(s.map(e=>l[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:"46079111fca52b3927026fe671b56a2f"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-BM62SqjA.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 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:"69c09c935ebcd7adcba85b212773b6f6"},{url:"icons/icon-512.png",revision:"c32202b9deed67ef38331f63dec9d1c8"},{url:"icons/icon-192.png",revision:"46ece317d50d10b8f5e225e073b3221d"},{url:"assets/index-C1fxqzme.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\//]}))}); diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..d91900d --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,16 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthGate } from './components/AuthGate'; +import { UpdateBanner } from './components/UpdateBanner'; +import { useVersionCheck } from './hooks/useVersionCheck'; +import { Layout } from './components/Layout'; +import { Planner } from './pages/Planner'; +import { Settings } from './pages/CategorySettings'; +import { can } from './types'; +function AppRoutes({ user }) { + return (_jsx(Layout, { user: user, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: "/planner", replace: true }) }), _jsx(Route, { path: "/planner", element: _jsx(Planner, {}) }), _jsx(Route, { path: "/settings", element: can(user, 'settings') ? _jsx(Settings, {}) : _jsx(Navigate, { to: "/planner", replace: true }) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/planner", replace: true }) })] }) })); +} +export default function App() { + const updateAvailable = useVersionCheck('/hk-planner/health'); + return (_jsxs(_Fragment, { children: [_jsx(BrowserRouter, { basename: "/hk-planner", children: _jsx(AuthGate, { children: user => _jsx(AppRoutes, { user: user }) }) }), _jsx(UpdateBanner, { visible: updateAvailable })] })); +} diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..fc06e47 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,108 @@ +const BASE = '/hk-planner/api'; +async function request(path, opts) { + const res = await fetch(BASE + path, { credentials: 'include', ...opts }); + if (res.status === 401) { + ; + (window.top ?? window).location.href = '/login'; + throw new Error('Unauthenticated'); + } + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error || `HTTP ${res.status}`); + } + return res.json(); +} +export function getBookings(weekStart, lastViewed, forceRefresh = false) { + const p = new URLSearchParams(); + if (weekStart) + p.set('week_start', weekStart); + if (lastViewed) + p.set('last_viewed', lastViewed); + if (forceRefresh) + p.set('force_refresh', '1'); + return request(`/bookings?${p}`); +} +export function getConfig() { + return request('/config'); +} +export function putTimeReq(cat, action, value) { + return request('/config/time-requirements', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cat, action, value }), + }); +} +export function putStaff(staff_data) { + return request('/config/staff', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ staff_data }), + }); +} +export function putPickup(pickup_data) { + return request('/config/pickup', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pickup_data }), + }); +} +export function putGeneralTasks(general_tasks) { + return request('/config/general-tasks', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ general_tasks }), + }); +} +export function putLastReviewed(date) { + return request('/config/last-reviewed', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date }), + }); +} +export function getCategories() { + return request('/categories'); +} +export function putCategories(order, excluded) { + return request('/categories', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order, excluded }), + }); +} +export function testNewbook() { + return request('/newbook/test', { method: 'POST' }); +} +export function getWorkforceDepartments() { + return request('/workforce/departments'); +} +export function putWorkforceDepartments(dept_ids) { + return request('/config/workforce-departments', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dept_ids }), + }); +} +export function syncWorkforce() { + return request('/workforce/sync', { method: 'POST' }); +} +export function getWorkforceShifts(start, end) { + return request(`/workforce/shifts?start=${start}&end=${end}`); +} +export function getWorkforceStaff() { + return request('/workforce/staff'); +} +export function putAdjustments(adjustments) { + return request('/config/adjustments', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ adjustments }), + }); +} +export function putWarningThresholds(thresholds) { + return request('/config/warning-thresholds', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(thresholds), + }); +} diff --git a/frontend/src/components/Layout.js b/frontend/src/components/Layout.js index 291ada6..332e99e 100644 --- a/frontend/src/components/Layout.js +++ b/frontend/src/components/Layout.js @@ -2,20 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { NavLink } from 'react-router-dom'; import { CalendarClock, Settings, LogOut } from 'lucide-react'; import { can } from '../types'; +import { useFrameViewport } from '../hooks/useFrameViewport'; export function Layout({ user, children }) { + useFrameViewport('desktop'); async function logout() { - await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); - window.location.href = '/'; + await fetch('/hk-planner/api/auth/logout', { method: 'POST', credentials: 'include' }); + window.location.reload(); } return (_jsxs("div", { style: { display: 'flex', height: '100dvh', overflow: 'hidden' }, children: [_jsxs("nav", { style: { width: '200px', flexShrink: 0, background: 'var(--navy)', display: 'flex', flexDirection: 'column', padding: '1rem 0', borderRight: '1px solid var(--surface-2)', - }, children: [_jsxs("div", { style: { padding: '0 1rem 1rem', borderBottom: '1px solid var(--surface-2)' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [_jsx(CalendarClock, { size: 20, strokeWidth: 1.75, color: "#74c69d" }), _jsx("span", { style: { color: '#74c69d', fontWeight: 700, fontSize: '0.95rem' }, children: "HK Planner" })] }), _jsx("p", { style: { color: 'var(--text-muted)', fontSize: '0.72rem', marginTop: '0.2rem' }, children: user.name })] }), _jsxs("div", { className: "nav-scroll", style: { flex: 1, padding: '0.5rem 0', overflowY: 'auto' }, children: [_jsx(NavItem, { to: "/planner", icon: CalendarClock, label: "Planner" }), can(user, 'settings') && _jsx(NavItem, { to: "/settings", icon: Settings, label: "Settings" })] }), _jsx("div", { style: { padding: '0.75rem 1rem', borderTop: '1px solid var(--surface-2)' }, children: _jsxs("button", { onClick: logout, style: { - display: 'flex', alignItems: 'center', gap: '0.5rem', - background: 'none', border: 'none', color: 'var(--text-muted)', - fontSize: '0.875rem', padding: '0.375rem 0', width: '100%', cursor: 'pointer', - }, children: [_jsx(LogOut, { size: 14, strokeWidth: 1.75 }), "Sign out"] }) })] }), _jsx("main", { style: { flex: 1, overflow: 'auto', background: 'var(--body-bg)' }, children: children })] })); + }, children: [_jsx("div", { style: { padding: '0 1rem 1rem', borderBottom: '1px solid var(--surface-2)' }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [_jsx(CalendarClock, { size: 20, strokeWidth: 1.75, color: "#74c69d" }), _jsx("span", { style: { color: '#74c69d', fontWeight: 700, fontSize: '0.95rem' }, children: "HK Planner" })] }) }), _jsxs("div", { className: "nav-scroll", style: { flex: 1, padding: '0.5rem 0', overflowY: 'auto' }, children: [_jsx(NavItem, { to: "/planner", icon: CalendarClock, label: "Planner" }), can(user, 'settings') && _jsx(NavItem, { to: "/settings", icon: Settings, label: "Settings" })] }), _jsxs("div", { style: { padding: '0.75rem 1rem', borderTop: '1px solid var(--surface-2)' }, children: [_jsxs("div", { style: { marginBottom: '0.5rem' }, children: [_jsx("div", { style: { color: 'var(--text)', fontSize: '0.8rem', fontWeight: 600 }, children: user.name }), _jsx("div", { style: { color: 'var(--text-muted)', fontSize: '0.72rem' }, children: user.email })] }), _jsxs("button", { onClick: logout, style: { + display: 'flex', alignItems: 'center', gap: '0.5rem', + background: 'none', border: 'none', color: 'var(--text-muted)', + fontSize: '0.8rem', padding: '0.375rem 0', width: '100%', cursor: 'pointer', + }, children: [_jsx(LogOut, { size: 14, strokeWidth: 1.75 }), "Sign out"] })] })] }), _jsx("main", { style: { flex: 1, overflow: 'auto', background: 'var(--body-bg)' }, children: children })] })); } function NavItem({ to, icon: Icon, label }) { return (_jsxs(NavLink, { to: to, style: ({ isActive }) => ({ diff --git a/frontend/src/components/UpdateBanner.js b/frontend/src/components/UpdateBanner.js new file mode 100644 index 0000000..0620851 --- /dev/null +++ b/frontend/src/components/UpdateBanner.js @@ -0,0 +1,34 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { RefreshCw } from 'lucide-react'; +export function UpdateBanner({ visible }) { + if (!visible) + return null; + return (_jsxs("div", { style: { + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + zIndex: 9999, + background: 'var(--sidebar)', + color: 'var(--text-light)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '12px', + padding: '10px 16px', + fontSize: '14px', + boxShadow: '0 -2px 8px rgba(0,0,0,0.3)', + }, children: [_jsx("span", { children: "A new version is available." }), _jsxs("button", { onClick: () => window.location.reload(), style: { + display: 'flex', + alignItems: 'center', + gap: '6px', + background: 'var(--accent)', + color: 'var(--sidebar)', + border: 'none', + borderRadius: '4px', + padding: '6px 14px', + fontWeight: 600, + cursor: 'pointer', + fontSize: '13px', + }, children: [_jsx(RefreshCw, { size: 14, strokeWidth: 1.75 }), "Reload"] })] })); +} diff --git a/frontend/src/hooks/useFrameViewport.js b/frontend/src/hooks/useFrameViewport.js new file mode 100644 index 0000000..c478e52 --- /dev/null +++ b/frontend/src/hooks/useFrameViewport.js @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +const DESKTOP = 'width=1280'; +const RESPONSIVE = 'width=device-width, initial-scale=1.0'; +function setMetaViewport(content) { + const meta = document.querySelector('meta[name="viewport"]'); + if (meta) + meta.content = content; +} +export function useFrameViewport(mode) { + useEffect(() => { + setMetaViewport(mode === 'desktop' ? DESKTOP : RESPONSIVE); + window.parent.postMessage({ type: 'hnf:viewport', mode }, '*'); + return () => { + setMetaViewport(RESPONSIVE); + window.parent.postMessage({ type: 'hnf:viewport', mode: 'responsive' }, '*'); + }; + }, [mode]); +} diff --git a/frontend/src/hooks/useVersionCheck.js b/frontend/src/hooks/useVersionCheck.js new file mode 100644 index 0000000..839dd71 --- /dev/null +++ b/frontend/src/hooks/useVersionCheck.js @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +const POLL_MS = 2 * 60 * 1000; +export function useVersionCheck(healthUrl) { + const [updateAvailable, setUpdateAvailable] = useState(false); + useEffect(() => { + let seenVersion = null; + async function check() { + try { + const res = await fetch(healthUrl, { cache: 'no-store' }); + if (!res.ok) + return; + const data = await res.json(); + const v = data.version; + if (!v) + return; + if (seenVersion === null) { + seenVersion = v; + } + else if (v !== seenVersion) { + setUpdateAvailable(true); + } + } + catch { + // network error — skip silently + } + } + check(); + const interval = setInterval(check, POLL_MS); + function onVisible() { + if (document.visibilityState === 'visible') + check(); + } + document.addEventListener('visibilitychange', onVisible); + return () => { + clearInterval(interval); + document.removeEventListener('visibilitychange', onVisible); + }; + }, [healthUrl]); + return updateAvailable; +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..d78bd98 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,14 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App'; +// When opened from the portal's install button (?install=1), trigger the +// PWA install prompt as soon as the browser offers it (Chrome/Edge only). +if (new URLSearchParams(window.location.search).has('install')) { + window.addEventListener('beforeinstallprompt', e => { + e.preventDefault(); + e.prompt(); + }, { once: true }); +} +createRoot(document.getElementById('root')).render(_jsx(StrictMode, { children: _jsx(App, {}) })); diff --git a/frontend/src/pages/CategorySettings.js b/frontend/src/pages/CategorySettings.js new file mode 100644 index 0000000..983f334 --- /dev/null +++ b/frontend/src/pages/CategorySettings.js @@ -0,0 +1,263 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useState, useEffect } from 'react'; +import { GripVertical } from 'lucide-react'; +import { getCategories, putCategories, testNewbook, getConfig, getWorkforceDepartments, putWorkforceDepartments, putGeneralTasks, putTimeReq, putWarningThresholds, } from '../api'; +const WEEKDAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +export function Settings() { + // ── Category state ───────────────────────────────────────────────────────── + const [cats, setCats] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [testing, setTesting] = useState(false); + const [error, setError] = useState(''); + const [msg, setMsg] = useState(''); + const [dragIdx, setDragIdx] = useState(null); + // ── Workforce state ──────────────────────────────────────────────────────── + const [wfDepts, setWfDepts] = useState([]); + const [wfSelected, setWfSelected] = useState([]); + const [wfLoading, setWfLoading] = useState(true); + const [wfError, setWfError] = useState(''); + const [wfNotConfigured, setWfNotConfigured] = useState(false); + const [wfSaving, setWfSaving] = useState(false); + const [wfMsg, setWfMsg] = useState(''); + // ── Recurring general tasks state ────────────────────────────────────────── + const [tasks, setTasks] = useState([]); + const [taskSaving, setTaskSaving] = useState(false); + const [taskMsg, setTaskMsg] = useState(''); + const [taskError, setTaskError] = useState(''); + // ── Time requirements state ──────────────────────────────────────────────── + const [timeReqs, setTimeReqs] = useState({}); + const [timeMsg, setTimeMsg] = useState(''); + const [timeError, setTimeError] = useState(''); + // ── Warning threshold state ──────────────────────────────────────────────── + const [warnOverRed, setWarnOverRed] = useState(4); + const [warnOverAmber, setWarnOverAmber] = useState(1); + const [warnUnderAmber, setWarnUnderAmber] = useState(1); + const [warnUnderRed, setWarnUnderRed] = useState(2); + const [warnSaving, setWarnSaving] = useState(false); + const [warnMsg, setWarnMsg] = useState(''); + const [warnError, setWarnError] = useState(''); + useEffect(() => { + Promise.all([ + getCategories(), + getConfig(), + getWorkforceDepartments().catch((e) => { + if (e.message.includes('503') || e.message.toLowerCase().includes('not configured')) { + setWfNotConfigured(true); + } + else { + setWfError(e.message); + } + return null; + }), + ]).then(([catResp, cfg, depts]) => { + setCats(catResp.categories); + setTasks(cfg.general_tasks || []); + setTimeReqs(cfg.time_requirements || {}); + setWfSelected(cfg.workforce_departments || []); + setWarnOverRed(cfg.warn_over_red_hrs ?? 4); + setWarnOverAmber(cfg.warn_over_amber_hrs ?? 1); + setWarnUnderAmber(cfg.warn_under_amber_hrs ?? 1); + setWarnUnderRed(cfg.warn_under_red_hrs ?? 2); + if (depts) + setWfDepts(depts); + setLoading(false); + setWfLoading(false); + }).catch(e => { + setError(e.message); + setLoading(false); + setWfLoading(false); + }); + }, []); + // ── Category handlers ────────────────────────────────────────────────────── + function toggleExcluded(i) { + setCats(cats.map((c, idx) => idx === i ? { ...c, excluded: !c.excluded } : c)); + } + function onDragStart(e, i) { + setDragIdx(i); + e.dataTransfer.effectAllowed = 'move'; + } + function onDragOver(e, i) { + e.preventDefault(); + if (dragIdx === null || dragIdx === i) + return; + const next = [...cats]; + const [moved] = next.splice(dragIdx, 1); + next.splice(i, 0, moved); + setCats(next); + setDragIdx(i); + } + function onDragEnd() { setDragIdx(null); } + async function saveCats() { + setSaving(true); + setError(''); + setMsg(''); + try { + await putCategories(cats.map(c => c.id), cats.filter(c => c.excluded).map(c => c.id)); + setMsg('Saved'); + setTimeout(() => setMsg(''), 2500); + } + catch (e) { + setError(e instanceof Error ? e.message : 'Save failed'); + } + finally { + setSaving(false); + } + } + async function handleTest() { + setTesting(true); + setError(''); + setMsg(''); + try { + const res = await testNewbook(); + setMsg(res.ok ? `Connection OK: ${res.message || ''}` : `Failed: ${res.error || 'unknown'}`); + } + catch (e) { + setError(e instanceof Error ? e.message : 'Test failed'); + } + finally { + setTesting(false); + } + } + // ── Workforce handlers ───────────────────────────────────────────────────── + function toggleWfDept(id) { + setWfSelected(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); + } + async function saveWfDepts() { + setWfSaving(true); + setWfError(''); + setWfMsg(''); + try { + await putWorkforceDepartments(wfSelected); + setWfMsg('Departments saved'); + setTimeout(() => setWfMsg(''), 2500); + } + catch (e) { + setWfError(e instanceof Error ? e.message : 'Save failed'); + } + finally { + setWfSaving(false); + } + } + // ── Recurring tasks handlers ─────────────────────────────────────────────── + function setTaskName(i, name) { + setTasks(tasks.map((t, idx) => idx === i ? { ...t, name } : t)); + } + function setTaskHours(i, day, val) { + const mins = parseInt(val, 10); + setTasks(tasks.map((t, idx) => { + if (idx !== i) + return t; + return { ...t, hours: { ...t.hours, [day]: isNaN(mins) ? 0 : Math.max(0, mins) } }; + })); + } + function removeTask(i) { + setTasks(tasks.filter((_, idx) => idx !== i)); + } + async function saveTasks() { + setTaskSaving(true); + setTaskError(''); + setTaskMsg(''); + try { + await putGeneralTasks(tasks); + setTaskMsg('Saved'); + setTimeout(() => setTaskMsg(''), 2500); + } + catch (e) { + setTaskError(e instanceof Error ? e.message : 'Save failed'); + } + finally { + setTaskSaving(false); + } + } + // ── Time requirements handlers ───────────────────────────────────────────── + async function handleTimeReqChange(catId, action, value) { + const next = { + ...timeReqs, + [catId]: { ...(timeReqs[catId] || { depart: 0, stay: 0, arrive: 0 }), [action]: value }, + }; + setTimeReqs(next); + try { + await putTimeReq(catId, action, value); + setTimeMsg('Saved'); + setTimeout(() => setTimeMsg(''), 1500); + } + catch (e) { + setTimeError(e instanceof Error ? e.message : 'Save failed'); + } + } + // ── Warning threshold handlers ───────────────────────────────────────────── + async function saveWarnThresholds() { + setWarnSaving(true); + setWarnError(''); + setWarnMsg(''); + try { + await putWarningThresholds({ + warn_over_red_hrs: warnOverRed, + warn_over_amber_hrs: warnOverAmber, + warn_under_amber_hrs: warnUnderAmber, + warn_under_red_hrs: warnUnderRed, + }); + setWarnMsg('Saved'); + setTimeout(() => setWarnMsg(''), 2500); + } + catch (e) { + setWarnError(e instanceof Error ? e.message : 'Save failed'); + } + finally { + setWarnSaving(false); + } + } + if (loading) { + return _jsx("div", { style: { padding: '2rem', color: 'var(--text-mid)' }, children: "Loading\u2026" }); + } + return (_jsxs("div", { style: { padding: '1.5rem', maxWidth: '680px' }, children: [_jsx("h1", { style: { fontSize: '1.1rem', fontWeight: 700, color: 'var(--text-dark)', marginBottom: '1.5rem' }, children: "Settings" }), _jsx(SectionHeading, { title: "Room Categories" }), _jsx("p", { style: { fontSize: '0.82rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "Drag to reorder. Toggle to exclude categories from the planner." }), error && _jsx(Banner, { type: "error", children: error }), msg && _jsx(Banner, { type: "ok", children: msg }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem' }, children: cats.map((cat, i) => (_jsxs("div", { draggable: true, onDragStart: e => onDragStart(e, i), onDragOver: e => onDragOver(e, i), onDragEnd: onDragEnd, style: { + display: 'flex', alignItems: 'center', gap: '0.75rem', + padding: '0.625rem 0.875rem', + border: '1px solid var(--card-border)', borderRadius: '8px', + background: cat.excluded ? '#f8fafc' : 'var(--card-bg)', + opacity: dragIdx === i ? 0.5 : 1, + }, children: [_jsx(GripVertical, { size: 16, color: "var(--text-mid)", style: { cursor: 'grab', flexShrink: 0 } }), _jsx("span", { style: { + flex: 1, fontSize: '0.9rem', + color: cat.excluded ? 'var(--text-mid)' : 'var(--text-dark)', + textDecoration: cat.excluded ? 'line-through' : 'none', + }, children: cat.name }), _jsxs("span", { style: { fontSize: '0.75rem', color: 'var(--text-mid)', marginRight: '0.5rem' }, children: [cat.room_count, " rooms"] }), _jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '0.375rem', fontSize: '0.8rem', color: 'var(--text-mid)' }, children: [_jsx("input", { type: "checkbox", checked: !cat.excluded, onChange: () => toggleExcluded(i) }), "Active"] })] }, cat.id))) }), _jsxs("div", { style: { display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '2.5rem' }, children: [_jsx(Btn, { onClick: saveCats, disabled: saving, primary: true, children: saving ? 'Saving…' : 'Save Order & Visibility' }), _jsx(Btn, { onClick: handleTest, disabled: testing, children: testing ? 'Testing…' : 'Test Newbook Connection' })] }), _jsx(Divider, {}), _jsx(SectionHeading, { title: "Workforce Departments" }), _jsx("p", { style: { fontSize: '0.82rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "Select the department(s) whose shifts should appear in the HK staff rota." }), wfNotConfigured && (_jsxs("div", { style: { padding: '0.75rem', borderRadius: '8px', background: '#f8fafc', border: '1px solid var(--card-border)', color: 'var(--text-mid)', fontSize: '0.85rem', marginBottom: '2rem' }, children: ["Workforce integration not configured \u2014 add the bearer token in ", _jsx("strong", { children: "Settings \u2192 Integrations \u2192 Workforce" }), "."] })), !wfNotConfigured && wfLoading && (_jsx("div", { style: { color: 'var(--text-mid)', fontSize: '0.85rem', marginBottom: '2rem' }, children: "Loading departments\u2026" })), !wfNotConfigured && !wfLoading && (_jsxs("div", { style: { marginBottom: '2.5rem' }, children: [wfError && _jsx(Banner, { type: "error", children: wfError }), wfMsg && _jsx(Banner, { type: "ok", children: wfMsg }), wfDepts.length === 0 ? (_jsx("div", { style: { color: 'var(--text-mid)', fontSize: '0.85rem', marginBottom: '1rem' }, children: "No departments found for this location." })) : (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '0.4rem', marginBottom: '1rem' }, children: wfDepts.map(dept => (_jsxs("label", { style: { + display: 'flex', alignItems: 'center', gap: '0.625rem', + padding: '0.5rem 0.75rem', + border: '1px solid var(--card-border)', borderRadius: '7px', + background: wfSelected.includes(dept.id) ? 'rgba(42,100,72,0.05)' : 'var(--card-bg)', + cursor: 'pointer', fontSize: '0.9rem', color: 'var(--text-dark)', + }, children: [_jsx("input", { type: "checkbox", checked: wfSelected.includes(dept.id), onChange: () => toggleWfDept(dept.id) }), dept.name] }, dept.id))) })), wfSelected.length === 0 && wfDepts.length > 0 && (_jsx("div", { style: { marginBottom: '0.75rem', fontSize: '0.8rem', color: '#b45309' }, children: "Select at least one department to enable Workforce sync." })), _jsx(Btn, { onClick: saveWfDepts, disabled: wfSaving, primary: true, children: wfSaving ? 'Saving…' : 'Save Departments' })] })), _jsx(Divider, {}), _jsx(SectionHeading, { title: "Recurring General Tasks" }), _jsx("p", { style: { fontSize: '0.82rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "Tasks that recur every week. Enter minutes per day. These add to the total required hours every week." }), taskError && _jsx(Banner, { type: "error", children: taskError }), taskMsg && _jsx(Banner, { type: "ok", children: taskMsg }), _jsx("div", { style: { overflowX: 'auto', marginBottom: '1rem' }, children: _jsxs("table", { className: "hk-table", style: { minWidth: '560px' }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "col-label", children: "Task" }), WEEKDAYS.map(d => _jsx("th", { children: d }, d)), _jsx("th", { style: { width: '32px' } })] }) }), _jsxs("tbody", { children: [tasks.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 9, style: { color: 'var(--text-mid)', textAlign: 'center', padding: '1rem' }, children: "No tasks yet" }) })), tasks.map((task, i) => (_jsxs("tr", { children: [_jsx("td", { style: { padding: '0.3rem 0.5rem' }, children: _jsx("input", { className: "hk-text-input", value: task.name, placeholder: "Task name", onChange: e => setTaskName(i, e.target.value) }) }), WEEKDAYS.map(day => (_jsx("td", { style: { padding: '0.3rem 0.4rem' }, children: _jsx("input", { type: "number", className: "hk-num-input", min: 0, max: 999, value: task.hours[day] || '', placeholder: "0", onChange: e => setTaskHours(i, day, e.target.value) }) }, day))), _jsx("td", { children: _jsx("button", { onClick: () => removeTask(i), style: { + background: 'none', border: 'none', color: 'var(--text-mid)', fontSize: '1rem', padding: '0.2rem 0.4rem', + }, children: "\u00D7" }) })] }, i)))] })] }) }), _jsxs("div", { style: { display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '2.5rem' }, children: [_jsx(Btn, { onClick: () => setTasks([...tasks, { name: '', hours: {} }]), children: "+ Add task" }), _jsx(Btn, { onClick: saveTasks, disabled: taskSaving, primary: true, children: taskSaving ? 'Saving…' : 'Save Tasks' })] }), _jsx(Divider, {}), _jsx(SectionHeading, { title: "Time Requirements (minutes per room)" }), _jsx("p", { style: { fontSize: '0.82rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "How many minutes each room type takes depending on guest status." }), timeError && _jsx(Banner, { type: "error", children: timeError }), timeMsg && _jsx(Banner, { type: "ok", children: timeMsg }), _jsx("div", { style: { overflowX: 'auto', marginBottom: '2rem' }, children: _jsxs("table", { className: "hk-table", style: { maxWidth: '500px' }, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "col-label", children: "Category" }), _jsx("th", { children: "Depart (mins)" }), _jsx("th", { children: "Stay (mins)" }), _jsx("th", { children: "Arrive (mins)" })] }) }), _jsx("tbody", { children: cats.filter(c => !c.excluded).map(cat => { + const req = timeReqs[cat.id] || { depart: 0, stay: 0, arrive: 0 }; + return (_jsxs("tr", { children: [_jsx("td", { className: "col-label", children: cat.name }), ['depart', 'stay', 'arrive'].map(action => (_jsx("td", { style: { padding: '0.3rem 0.4rem' }, children: _jsx("input", { type: "number", className: "hk-num-input", min: 0, max: 999, value: req[action] || '', placeholder: "0", onChange: e => handleTimeReqChange(cat.id, action, parseInt(e.target.value, 10) || 0), onBlur: e => handleTimeReqChange(cat.id, action, parseInt(e.target.value, 10) || 0) }) }, action)))] }, cat.id)); + }) })] }) }), _jsx(Divider, {}), _jsx(SectionHeading, { title: "Warning Thresholds" }), _jsx("p", { style: { fontSize: '0.82rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "Controls the colour and icon shown on the \"with Adjustments\" row in the staff rota. All values are in hours." }), warnError && _jsx(Banner, { type: "error", children: warnError }), warnMsg && _jsx(Banner, { type: "ok", children: warnMsg }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '0.625rem', marginBottom: '1.25rem', maxWidth: '420px' }, children: [ + { label: '⚠ Red warning — spare over', value: warnOverRed, set: setWarnOverRed, help: 'default 4h' }, + { label: '⚠ Amber warning — spare over', value: warnOverAmber, set: setWarnOverAmber, help: 'default 1h' }, + { label: '✗ Amber cross — short over', value: warnUnderAmber, set: setWarnUnderAmber, help: 'default 1h' }, + { label: '✗ Red cross — short over', value: warnUnderRed, set: setWarnUnderRed, help: 'default 2h' }, + ].map(({ label, value, set, help }) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.75rem' }, children: [_jsx("span", { style: { flex: 1, fontSize: '0.875rem', color: 'var(--text-dark)' }, children: label }), _jsx("input", { type: "number", className: "hk-num-input", min: 0, max: 24, step: 0.5, value: value, onChange: e => set(parseFloat(e.target.value) || 0), style: { width: '68px' } }), _jsx("span", { style: { fontSize: '0.75rem', color: 'var(--text-mid)', minWidth: '52px' }, children: help })] }, label))) }), _jsx("p", { style: { fontSize: '0.78rem', color: 'var(--text-mid)', marginBottom: '1rem' }, children: "Green \u2713 is automatic \u2014 shown when the difference is within the amber spare and short thresholds above." }), _jsx("div", { style: { marginBottom: '2.5rem' }, children: _jsx(Btn, { onClick: saveWarnThresholds, disabled: warnSaving, primary: true, children: warnSaving ? 'Saving…' : 'Save Thresholds' }) })] })); +} +// ── Small reusable helpers ──────────────────────────────────────────────────── +function SectionHeading({ title }) { + return (_jsx("h2", { style: { fontSize: '0.95rem', fontWeight: 700, color: 'var(--text-dark)', marginBottom: '0.25rem' }, children: title })); +} +function Divider() { + return _jsx("div", { style: { height: '1px', background: 'var(--card-border)', margin: '0.5rem 0 2rem' } }); +} +function Banner({ type, children }) { + return (_jsx("div", { style: { + marginBottom: '0.75rem', padding: '0.75rem', borderRadius: '8px', fontSize: '0.875rem', + background: type === 'ok' ? '#dcfce7' : '#fee2e2', + color: type === 'ok' ? 'var(--success)' : 'var(--danger)', + }, children: children })); +} +function Btn({ children, onClick, disabled, primary }) { + return (_jsx("button", { onClick: onClick, disabled: disabled, style: { + background: primary ? 'var(--hk-green)' : 'var(--card-bg)', + color: primary ? '#fff' : 'var(--text-dark)', + border: `1px solid ${primary ? 'var(--hk-green)' : 'var(--card-border)'}`, + borderRadius: '6px', padding: '0.5rem 1.25rem', fontSize: '0.875rem', fontWeight: 600, + }, children: children })); +} diff --git a/frontend/src/pages/Planner.js b/frontend/src/pages/Planner.js new file mode 100644 index 0000000..2277351 --- /dev/null +++ b/frontend/src/pages/Planner.js @@ -0,0 +1,538 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { RefreshCw } from 'lucide-react'; +import { getBookings, getConfig, putStaff, putPickup, putLastReviewed, putAdjustments, syncWorkforce, getWorkforceShifts, getWorkforceStaff, } from '../api'; +// ── Date helpers ────────────────────────────────────────────────────────────── +function todayStr() { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} +function offsetDate(s, days) { + const [y, m, d] = s.split('-').map(Number); + const dt = new Date(y, m - 1, d); + dt.setDate(dt.getDate() + days); + return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`; +} +function fmtDayHeader(date) { + const d = new Date(date + 'T00:00:00'); + return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + d.getDate() + ' ' + + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()]; +} +function getDayName(date) { + const d = new Date(date + 'T00:00:00'); + return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][(d.getDay() + 6) % 7]; +} +function fullDayName(date) { + const d = new Date(date + 'T00:00:00'); + return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][d.getDay()]; +} +function isWeekend(date) { + const day = new Date(date + 'T00:00:00').getDay(); + return day === 0 || day === 6; +} +function priorLineFor(date, d) { + const hint = d.pickup_hint ?? 0; + const lead = d.pickup_lead ?? 0; + const late = hint > 0 + ? `${hint} room${hint === 1 ? '' : 's'} picked up within ${lead} day${lead === 1 ? '' : 's'} of arrival` + : 'no late pickups'; + return `Last ${fullDayName(date)}: ${d.prior_occ} occ (${d.prior_vac} vac) — ${late}`; +} +function fmtDate(date) { + return new Date(date + 'T00:00:00').toLocaleDateString('en-GB', { + weekday: 'short', day: 'numeric', month: 'short', + }); +} +function fmtH(n) { return n.toFixed(2) + 'h'; } +// ── Pickup calculation ──────────────────────────────────────────────────────── +function getDisplayPickup(pickup, catId, date, occupiedNow, totalRooms) { + const vacant = Math.max(0, totalRooms - occupiedNow); + const saved = pickup[catId]?.[date]; + if (!saved || !saved.count) + return 0; + const savedBooked = saved.total - saved.count; + const count = occupiedNow > savedBooked + ? Math.max(0, saved.total - occupiedNow) + : saved.count; + return Math.min(count, vacant); +} +// ── Required hours calculation ──────────────────────────────────────────────── +function calcRequired(bookings, timeReqs, pickup, generalTasks, adjustments) { + const result = {}; + for (const date of bookings.dates) { + const dayName = getDayName(date); + const genHrs = generalTasks.reduce((s, t) => s + ((t.hours[dayName] || 0) / 60), 0); + const adjHrs = adjustments.reduce((s, a) => s + (a.hours[date] || 0), 0); + result[date] = { booked: 0, pickup: 0, general: genHrs, adjustments: adjHrs, total: genHrs + adjHrs, by_cat: {}, pickup_by_cat: {} }; + } + for (const cat of bookings.categories) { + const req = timeReqs[cat.id] || { depart: 0, stay: 0, arrive: 0 }; + bookings.dates.forEach((date, di) => { + const d = cat.days[date]; + const occupied = d.stays + d.arrivals; + const pickupCount = getDisplayPickup(pickup, cat.id, date, occupied, cat.total_rooms); + const bookedHrs = (d.departs * (req.depart || 0) + d.stays * (req.stay || 0) + d.arrivals * (req.arrive || 0)) / 60; + const pickupArrHrs = pickupCount * (req.arrive || 0) / 60; + result[date].by_cat[cat.id] = bookedHrs; + result[date].pickup_by_cat[cat.id] = (result[date].pickup_by_cat[cat.id] || 0) + pickupArrHrs; + result[date].booked += bookedHrs; + result[date].pickup += pickupArrHrs; + result[date].total += bookedHrs + pickupArrHrs; + const nextDate = bookings.dates[di + 1]; + if (nextDate && pickupCount > 0) { + const pickupDepHrs = pickupCount * (req.depart || 0) / 60; + result[nextDate].pickup_by_cat[cat.id] = (result[nextDate].pickup_by_cat[cat.id] || 0) + pickupDepHrs; + result[nextDate].pickup += pickupDepHrs; + result[nextDate].total += pickupDepHrs; + } + }); + } + return result; +} +// ── Main Planner component ──────────────────────────────────────────────────── +export function Planner() { + const [bookings, setBookings] = useState(null); + const [timeReqs, setTimeReqs] = useState({}); + const [staff, setStaff] = useState([]); + const [pickup, setPickup] = useState({}); + const [generalTasks, setGeneralTasks] = useState([]); + const [adjustments, setAdjustments] = useState([]); + const [warnOverRed, setWarnOverRed] = useState(4); + const [warnOverAmber, setWarnOverAmber] = useState(1); + const [warnUnderAmber, setWarnUnderAmber] = useState(1); + const [warnUnderRed, setWarnUnderRed] = useState(2); + const [wfShifts, setWfShifts] = useState({}); + const [wfStaff, setWfStaff] = useState([]); + const [syncing, setSyncing] = useState(false); + const [weekStart, setWeekStart] = useState(null); + const [lastViewed, setLastViewed] = useState(''); + const [savedLastReviewed, setSavedLastReviewed] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [saveMsg, setSaveMsg] = useState(null); + const timers = useRef({}); + const saveMsgTimer = useRef(null); + const wfStaffLoaded = useRef(false); + const weekStartRef = useRef(null); + weekStartRef.current = weekStart; // always current, updated every render + function debounce(key, fn, delay = 400) { + clearTimeout(timers.current[key]); + timers.current[key] = setTimeout(fn, delay); + } + function flash(text, err = false) { + if (saveMsgTimer.current) + clearTimeout(saveMsgTimer.current); + setSaveMsg({ text, err }); + saveMsgTimer.current = setTimeout(() => setSaveMsg(null), err ? 15000 : 2500); + } + function stampLastReviewed() { + const today = todayStr(); + if (today === savedLastReviewed) + return; + setSavedLastReviewed(today); + putLastReviewed(today).catch(() => { }); + } + const loadAll = useCallback(async (force = false, lvArg) => { + setLoading(true); + setError(''); + const today = todayStr(); + const ws = weekStartRef.current || today; + const wsEnd = offsetDate(ws, 6); + const lv = lvArg !== undefined ? (lvArg || offsetDate(today, -1)) : (lastViewed || savedLastReviewed || offsetDate(today, -1)); + try { + const [b, cfg, shifts] = await Promise.all([ + getBookings(ws, lv, force), + getConfig(), + getWorkforceShifts(ws, wsEnd).catch(() => ({})), + ]); + setBookings(b); + setTimeReqs(cfg.time_requirements || {}); + setStaff(cfg.staff_data || []); + setPickup(cfg.pickup_data || {}); + setGeneralTasks(cfg.general_tasks || []); + setAdjustments(cfg.adjustments || []); + setWarnOverRed(cfg.warn_over_red_hrs ?? 4); + setWarnOverAmber(cfg.warn_over_amber_hrs ?? 1); + setWarnUnderAmber(cfg.warn_under_amber_hrs ?? 1); + setWarnUnderRed(cfg.warn_under_red_hrs ?? 2); + setWfShifts(shifts); + if (cfg.last_reviewed) { + setSavedLastReviewed(cfg.last_reviewed); + if (!lastViewed) + setLastViewed(cfg.last_reviewed); + } + } + catch (e) { + setError(e instanceof Error ? e.message : 'Failed to load data'); + } + finally { + setLoading(false); + } + }, [lastViewed, savedLastReviewed]); + useEffect(() => { loadAll(false); }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Lazily load WF staff list for datalist once any shifts exist + useEffect(() => { + if (Object.keys(wfShifts).length > 0 && !wfStaffLoaded.current) { + wfStaffLoaded.current = true; + getWorkforceStaff().then(setWfStaff).catch(() => { }); + } + }, [wfShifts]); + // Keepalive save on unload (sendBeacon only supports POST but routes are PUT) + useEffect(() => { + function keepalivePut(path, body) { + fetch(path, { + method: 'PUT', keepalive: true, credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } + function onUnload() { + if (staff.length) + keepalivePut('/hk-planner/api/config/staff', { staff_data: staff }); + if (Object.keys(pickup).length) + keepalivePut('/hk-planner/api/config/pickup', { pickup_data: pickup }); + if (adjustments.length) + keepalivePut('/hk-planner/api/config/adjustments', { adjustments }); + } + window.addEventListener('beforeunload', onUnload); + return () => window.removeEventListener('beforeunload', onUnload); + }, [staff, pickup, adjustments]); + function handleWeekOffset(days) { + const today = todayStr(); + const newStart = offsetDate(weekStartRef.current || today, days); + weekStartRef.current = newStart; + setWeekStart(newStart); + stampLastReviewed(); + loadAll(true); + } + function handleWeekToday() { + weekStartRef.current = null; + setWeekStart(null); + stampLastReviewed(); + loadAll(true); + } + function handleRefresh() { + stampLastReviewed(); + loadAll(true); + } + // ── Pickup ────────────────────────────────────────────────────────────────── + function handlePickupChange(catId, date, delta, cat) { + const d = cat.days[date]; + const occupied = d.stays + d.arrivals; + const vacant = Math.max(0, cat.total_rooms - occupied); + const current = getDisplayPickup(pickup, catId, date, occupied, cat.total_rooms); + const newVal = Math.max(0, Math.min(current + delta, vacant)); + const next = { + ...pickup, + [catId]: { ...(pickup[catId] || {}), [date]: { count: newVal, total: occupied + newVal } }, + }; + setPickup(next); + stampLastReviewed(); + debounce('pickup', () => putPickup(next).then(() => flash('Pickup saved')).catch(e => flash(e.message, true))); + } + // ── Adjustments ───────────────────────────────────────────────────────────── + function handleAdjustmentsChange(next) { + setAdjustments(next); + debounce('adjustments', () => putAdjustments(next).then(() => flash('Adjustments saved')).catch(e => flash(e.message, true))); + } + function addAdjustmentRow() { + handleAdjustmentsChange([...adjustments, { label: '', hours: {} }]); + } + // ── Staff ──────────────────────────────────────────────────────────────────── + function handleStaffChange(next) { + setStaff(next); + stampLastReviewed(); + debounce('staff', () => putStaff(next).then(() => flash('Staff hours saved')).catch(e => flash(e.message, true))); + } + function addStaffRow() { + handleStaffChange([...staff, { name: '', hours: {} }]); + } + // ── Workforce sync ─────────────────────────────────────────────────────────── + async function syncRota() { + setSyncing(true); + try { + await syncWorkforce(); + if (bookings) { + const shifts = await getWorkforceShifts(bookings.dates[0], bookings.dates[bookings.dates.length - 1]); + setWfShifts(shifts); + } + flash('Rota synced from Workforce'); + } + catch (e) { + flash(e instanceof Error ? e.message : 'Sync failed', true); + } + finally { + setSyncing(false); + } + } + function wfSyncLabel() { + if (!bookings) + return ''; + const viewDates = bookings.dates.filter(d => wfShifts[d]); + if (!viewDates.length) + return 'Not synced'; + const oldest = viewDates.reduce((min, d) => new Date(wfShifts[d].synced_at) < new Date(wfShifts[min].synced_at) ? d : min); + const dt = new Date(wfShifts[oldest].synced_at); + const day = dt.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }); + const time = dt.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); + const missing = bookings.dates.filter(d => !wfShifts[d]).length; + return missing ? `Partial sync — from ${day} ${time}` : `From ${day} ${time}`; + } + // ── Required hours (memoised on state changes) ──────────────────────────── + const required = bookings ? calcRequired(bookings, timeReqs, pickup, generalTasks, adjustments) : null; + // ── Render ────────────────────────────────────────────────────────────────── + const today = todayStr(); + const displayWeekStart = weekStart || today; + return (_jsxs("div", { style: { padding: '1.5rem', maxWidth: '1600px' }, children: [_jsxs("div", { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.75rem', marginBottom: '1.25rem' }, children: [_jsxs("div", { children: [_jsx("h1", { style: { fontSize: '1.15rem', fontWeight: 700, color: 'var(--text-dark)' }, children: "Housekeeping Planner" }), bookings && (_jsxs("p", { style: { fontSize: '0.8rem', color: 'var(--text-mid)', marginTop: '0.1rem' }, children: [fmtDate(bookings.dates[0]), " \u2013 ", fmtDate(bookings.dates[bookings.dates.length - 1])] }))] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.4rem', marginLeft: 'auto', flexWrap: 'wrap' }, children: [_jsx(CtrlBtn, { onClick: () => handleWeekOffset(-7), children: "\u2039 Prev" }), _jsx(CtrlBtn, { onClick: handleWeekToday, primary: true, children: "Today" }), _jsx(CtrlBtn, { onClick: () => handleWeekOffset(7), children: "Next \u203A" }), _jsx("input", { type: "date", value: displayWeekStart, onChange: e => { if (e.target.value) { + const v = e.target.value; + weekStartRef.current = v; + setWeekStart(v); + stampLastReviewed(); + debounce('weekStart', () => loadAll(true), 600); + } }, style: { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.35rem 0.5rem', fontSize: '0.82rem', color: 'var(--text-dark)' } }), _jsx("div", { style: { width: '1px', height: '24px', background: 'var(--card-border)' } }), _jsxs("label", { style: { fontSize: '0.78rem', color: 'var(--text-mid)', display: 'flex', alignItems: 'center', gap: '0.3rem' }, children: [_jsx("span", { children: "Since" }), _jsx("input", { type: "date", value: lastViewed, onChange: e => { if (e.target.value) { + const v = e.target.value; + setLastViewed(v); + debounce('lastViewed', () => loadAll(true, v), 600); + } }, style: { border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.3rem 0.5rem', fontSize: '0.78rem', color: 'var(--text-dark)' } })] }), _jsx(CtrlBtn, { onClick: () => { stampLastReviewed(); loadAll(true); }, children: "Update to now" }), _jsx("div", { style: { width: '1px', height: '24px', background: 'var(--card-border)' } }), _jsx("button", { onClick: handleRefresh, title: "Refresh", style: { + background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '6px', + padding: '0.35rem 0.5rem', display: 'flex', alignItems: 'center', gap: '0.3rem', + fontSize: '0.82rem', color: 'var(--text-dark)', + }, children: _jsx(RefreshCw, { size: 13, strokeWidth: 1.75, style: { animation: loading ? 'spin 1s linear infinite' : 'none' } }) })] })] }), _jsx("style", { children: `@keyframes spin { from { transform: rotate(0deg) } to { transform: rotate(360deg) } }` }), saveMsg && (_jsx("div", { style: { + display: 'inline-block', marginBottom: '0.75rem', padding: '0.35rem 0.75rem', + borderRadius: '6px', fontSize: '0.8rem', fontWeight: 500, + background: saveMsg.err ? '#fee2e2' : '#dcfce7', + color: saveMsg.err ? 'var(--danger)' : 'var(--success)', + }, children: saveMsg.text })), error && (_jsxs("div", { style: { + marginBottom: '1rem', padding: '0.75rem 1rem', borderRadius: '8px', + background: '#fee2e2', color: 'var(--danger)', fontSize: '0.875rem', + }, children: ["Error: ", error] })), loading && !bookings && (_jsx("div", { style: { color: 'var(--text-mid)', padding: '2rem 0', textAlign: 'center' }, children: "Loading bookings\u2026" })), bookings && required && (_jsxs(_Fragment, { children: [_jsx(Section, { title: "7-Day Occupancy", children: _jsx("div", { className: "table-scroll", children: _jsx(SummaryTable, { bookings: bookings, pickup: pickup, onPickupChange: handlePickupChange }) }) }), _jsx(Section, { title: "Required Hours", children: _jsx("div", { className: "table-scroll", children: _jsx(RequiredTable, { bookings: bookings, required: required }) }) }), _jsx(Section, { title: "Adjustments", action: _jsx(CtrlBtn, { onClick: addAdjustmentRow, children: "+ Add adjustment" }), children: _jsx("div", { className: "table-scroll", children: _jsx(AdjustmentsTable, { bookings: bookings, adjustments: adjustments, onChange: handleAdjustmentsChange }) }) }), _jsx(Section, { title: "Staff Rota", action: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }, children: [_jsxs("button", { onClick: syncRota, disabled: syncing, style: { + display: 'flex', alignItems: 'center', gap: '0.3rem', + background: 'var(--card-bg)', color: 'var(--text-dark)', + border: '1px solid var(--card-border)', borderRadius: '6px', + padding: '0.3rem 0.65rem', fontSize: '0.78rem', fontWeight: 600, + }, children: [_jsx(RefreshCw, { size: 11, strokeWidth: 1.75, style: { animation: syncing ? 'spin 1s linear infinite' : 'none' } }), syncing ? 'Syncing…' : 'Sync from Workforce'] }), _jsx("span", { style: { fontSize: '0.72rem', color: 'var(--text-mid)' }, children: wfSyncLabel() }), _jsx(CtrlBtn, { onClick: addStaffRow, children: "+ Add staff" })] }), children: _jsx("div", { className: "table-scroll", children: _jsx(StaffTable, { bookings: bookings, staff: staff, required: required, warnOverRed: warnOverRed, warnOverAmber: warnOverAmber, warnUnderAmber: warnUnderAmber, warnUnderRed: warnUnderRed, onChange: handleStaffChange, wfShifts: wfShifts, wfStaff: wfStaff }) }) })] }))] })); +} +// ── Section wrapper ─────────────────────────────────────────────────────────── +function Section({ title, children, action }) { + return (_jsxs("div", { style: { marginBottom: '1.5rem' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem' }, children: [_jsx("h2", { style: { fontSize: '0.72rem', fontWeight: 700, color: 'var(--text-mid)', textTransform: 'uppercase', letterSpacing: '0.08em' }, children: title }), action] }), children] })); +} +function CtrlBtn({ children, onClick, primary }) { + return (_jsx("button", { onClick: onClick, style: { + background: primary ? 'var(--hk-green)' : 'var(--card-bg)', + color: primary ? '#fff' : 'var(--text-dark)', + border: `1px solid ${primary ? 'var(--hk-green)' : 'var(--card-border)'}`, + borderRadius: '6px', padding: '0.35rem 0.75rem', fontSize: '0.82rem', fontWeight: 600, + }, children: children })); +} +function SummaryTable({ bookings, pickup, onPickupChange }) { + const { dates, categories: cats } = bookings; + return (_jsxs("table", { className: "hk-table", children: [_jsxs("thead", { children: [_jsxs("tr", { children: [_jsx("th", { className: "col-label", rowSpan: 2, children: "Category" }), dates.map(d => (_jsx("th", { colSpan: 2, style: { + borderLeft: '2px solid rgba(255,255,255,0.3)', + ...(isWeekend(d) ? { background: '#253555' } : {}), + }, children: fmtDayHeader(d) }, d)))] }), _jsx("tr", { children: dates.map(d => (_jsxs(_Fragment, { children: [_jsx("th", { style: { + fontSize: '0.72rem', fontWeight: 400, minWidth: '72px', + background: isWeekend(d) ? '#2a3d5e' : '#1e2d42', + borderLeft: '2px solid rgba(255,255,255,0.3)', + }, children: "Rooms" }, d + '-r'), _jsx("th", { style: { + fontSize: '0.72rem', fontWeight: 400, minWidth: '56px', textAlign: 'center', + background: isWeekend(d) ? '#2a3d5e' : '#1e2d42', + borderLeft: '1px solid rgba(255,255,255,0.1)', + }, children: "D / S / A" }, d + '-d')] }))) })] }), _jsx("tbody", { children: cats.map((cat, ci) => { + const isLast = ci === cats.length - 1; + return (_jsx(CatRows, { cat: cat, dates: dates, pickup: pickup, onPickupChange: (date, delta) => onPickupChange(cat.id, date, delta, cat), isLast: isLast }, cat.id)); + }) }), _jsx("tfoot", { children: _jsx(TotalRows, { dates: dates, cats: cats, pickup: pickup }) })] })); +} +function CatRows({ cat, dates, pickup, onPickupChange, isLast }) { + const bbStyle = isLast ? '2px solid var(--card-border)' : undefined; + return (_jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { borderBottom: bbStyle }, children: cat.name }), dates.map((date, di) => { + const d = cat.days[date]; + const occupied = d.stays + d.arrivals; + const vacant = Math.max(0, cat.total_rooms - occupied); + const pu = getDisplayPickup(pickup, cat.id, date, occupied, cat.total_rooms); + const prevDate = dates[di - 1]; + const prevOcc = prevDate ? (cat.days[prevDate].stays + cat.days[prevDate].arrivals) : 0; + const prevPu = prevDate ? getDisplayPickup(pickup, cat.id, prevDate, prevOcc, cat.total_rooms) : 0; + const dNew = d.delta_new || 0; + const dCanc = d.delta_cancelled || 0; + const wknd = isWeekend(date); + const saved = pickup[cat.id]?.[date]; + const priorLine = priorLineFor(date, d); + let occTooltip = ''; + if (saved && saved.count) { + const savedBooked = saved.total - saved.count; + const changeDesc = occupied > savedBooked ? 'changed' : 'unchanged'; + const pickupDesc = occupied > savedBooked ? 'decreased' : 'remains'; + occTooltip = `+${saved.count} pickup set when booked was ${savedBooked} (target ${saved.total}). Booked ${changeDesc}, so pickup ${pickupDesc}.`; + } + occTooltip = (occTooltip ? occTooltip + ' | ' : '') + priorLine; + return (_jsxs(_Fragment, { children: [_jsxs("td", { style: { + textAlign: 'center', verticalAlign: 'middle', borderBottom: bbStyle, + padding: '0.4rem 0.3rem', + borderLeft: '2px solid var(--card-border)', + ...(wknd ? { background: 'rgba(100,120,160,0.07)' } : {}), + }, children: [(dNew > 0 || dCanc > 0) && (_jsxs("div", { style: { marginBottom: '2px', fontSize: '0.72rem' }, children: [dNew > 0 && _jsxs("span", { className: "delta-new", children: ["\u25B2", dNew] }), dCanc > 0 && _jsxs("span", { className: "delta-canc", children: [" \u25BC", dCanc] })] })), _jsxs("div", { style: { fontWeight: 700, fontSize: '1.1rem' }, title: occTooltip, children: [occupied, pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", pu] })] }), _jsxs("div", { style: { fontSize: '0.72rem', color: 'var(--text-mid)', marginBottom: '4px' }, children: [vacant, " vac", pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" (", vacant - pu, ")"] })] }), _jsxs("div", { className: "pickup-ctrl", children: [_jsx("button", { className: "pickup-btn", disabled: pu <= 0, onClick: () => onPickupChange(date, -1), children: "\u2212" }), _jsx("span", { className: "pickup-num", children: pu }), _jsx("button", { className: "pickup-btn", disabled: pu >= vacant, title: priorLine, onClick: () => onPickupChange(date, 1), children: "+" })] })] }, date + '-r'), _jsxs("td", { style: { + verticalAlign: 'middle', borderLeft: '1px solid var(--card-border)', + borderBottom: bbStyle, padding: '0.4rem 0.3rem', textAlign: 'center', + ...(wknd ? { background: 'rgba(100,120,160,0.07)' } : {}), + }, children: [_jsxs("div", { style: { fontSize: '0.8rem', color: '#c2502e', fontWeight: 600 }, children: [d.departs, "d", prevPu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", prevPu] })] }), _jsxs("div", { style: { fontSize: '0.8rem', color: '#1d6fb8', fontWeight: 600 }, children: [d.stays, "s"] }), _jsxs("div", { style: { fontSize: '0.8rem', color: '#1a7a4a', fontWeight: 600 }, children: [d.arrivals, "a", pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", pu] })] })] }, date + '-dsa')] })); + })] })); +} +function TotalRows({ dates, cats, pickup }) { + return (_jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontStyle: 'italic' }, children: "Total" }), dates.map((date, di) => { + let occupied = 0, vacant = 0, pu = 0, prevPu = 0, departs = 0, stays = 0, arrivals = 0; + let dNew = 0, dCanc = 0; + for (const cat of cats) { + const d = cat.days[date]; + const occ = d.stays + d.arrivals; + const vac = Math.max(0, cat.total_rooms - occ); + const p = getDisplayPickup(pickup, cat.id, date, occ, cat.total_rooms); + const prev = dates[di - 1]; + const pOcc = prev ? (cat.days[prev].stays + cat.days[prev].arrivals) : 0; + const pPu = prev ? getDisplayPickup(pickup, cat.id, prev, pOcc, cat.total_rooms) : 0; + occupied += occ; + vacant += vac; + pu += p; + prevPu += pPu; + departs += d.departs; + stays += d.stays; + arrivals += d.arrivals; + dNew += d.delta_new || 0; + dCanc += d.delta_cancelled || 0; + } + const wknd = isWeekend(date); + return (_jsxs(_Fragment, { children: [_jsxs("td", { style: { + textAlign: 'center', verticalAlign: 'middle', padding: '0.4rem 0.3rem', + borderLeft: '2px solid var(--card-border)', + ...(wknd ? { background: 'rgba(100,120,160,0.07)' } : {}), + }, children: [(dNew > 0 || dCanc > 0) && (_jsxs("div", { style: { marginBottom: '2px', fontSize: '0.72rem' }, children: [dNew > 0 && _jsxs("span", { className: "delta-new", children: ["\u25B2", dNew] }), dCanc > 0 && _jsxs("span", { className: "delta-canc", children: [" \u25BC", dCanc] })] })), _jsxs("div", { style: { fontWeight: 700, fontSize: '1.1rem' }, children: [occupied, pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", pu] })] }), _jsxs("div", { style: { fontSize: '0.72rem', color: 'var(--text-mid)' }, children: [vacant, " vac", pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" (", vacant - pu, ")"] })] })] }, date + '-r'), _jsxs("td", { style: { + verticalAlign: 'middle', borderLeft: '1px solid var(--card-border)', + padding: '0.4rem 0.3rem', textAlign: 'center', + ...(wknd ? { background: 'rgba(100,120,160,0.07)' } : {}), + }, children: [_jsxs("div", { style: { fontSize: '0.8rem', color: '#c2502e', fontWeight: 600 }, children: [departs, "d", prevPu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", prevPu] })] }), _jsxs("div", { style: { fontSize: '0.8rem', color: '#1d6fb8', fontWeight: 600 }, children: [stays, "s"] }), _jsxs("div", { style: { fontSize: '0.8rem', color: '#1a7a4a', fontWeight: 600 }, children: [arrivals, "a", pu > 0 && _jsxs("span", { className: "pickup-tag", children: [" +", pu] })] })] }, date + '-dsa')] })); + })] })); +} +// ── Required Hours Table ────────────────────────────────────────────────────── +function RequiredTable({ bookings, required }) { + const { dates, categories: cats } = bookings; + return (_jsxs("table", { className: "hk-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "col-label", children: "Category" }), dates.map(d => _jsx("th", { children: fmtDayHeader(d) }, d))] }) }), _jsx("tbody", { children: cats.map(cat => (_jsxs("tr", { children: [_jsx("td", { className: "col-label", children: cat.name }), dates.map(date => { + const booked = required[date].by_cat[cat.id] || 0; + const pu = required[date].pickup_by_cat[cat.id] || 0; + return (_jsxs("td", { children: [booked === 0 && pu === 0 ? '—' : fmtH(booked), pu > 0.001 && _jsxs("span", { className: "pickup-tag", children: [" (+", fmtH(pu), ")"] })] }, date)); + })] }, cat.id))) }), _jsxs("tfoot", { children: [_jsxs("tr", { style: { background: '#f1f5f9' }, children: [_jsx("td", { className: "col-label", style: { color: 'var(--text-mid)', fontSize: '0.78rem' }, children: "Booked hrs" }), dates.map(d => _jsx("td", { children: fmtH(required[d].booked) }, d))] }), _jsxs("tr", { style: { background: '#f1f5f9' }, children: [_jsx("td", { className: "col-label", style: { color: 'var(--text-mid)', fontSize: '0.78rem' }, children: "Pickup hrs" }), dates.map(d => _jsx("td", { children: required[d].pickup > 0.001 ? fmtH(required[d].pickup) : '—' }, d))] }), _jsxs("tr", { style: { background: '#f1f5f9' }, children: [_jsx("td", { className: "col-label", style: { color: 'var(--text-mid)', fontSize: '0.78rem' }, children: "Recurring tasks" }), dates.map(d => _jsx("td", { children: required[d].general > 0.001 ? fmtH(required[d].general) : '—' }, d))] }), dates.some(d => required[d].adjustments !== 0) && (_jsxs("tr", { style: { background: '#f1f5f9' }, children: [_jsx("td", { className: "col-label", style: { color: 'var(--text-mid)', fontSize: '0.78rem' }, children: "Adjustments" }), dates.map(d => { + const adj = required[d].adjustments; + return (_jsx("td", { style: { color: adj < 0 ? 'var(--danger)' : adj > 0 ? '#1a7a4a' : 'var(--text-mid)' }, children: adj === 0 ? '—' : (adj > 0 ? '+' : '') + fmtH(adj) }, d)); + })] })), _jsxs("tr", { style: { background: '#e2e8f0' }, children: [_jsx("td", { className: "col-label", children: "Total Required" }), dates.map(d => (_jsxs("td", { style: { fontWeight: 700 }, children: [fmtH(required[d].total), required[d].pickup > 0.001 && (_jsxs("span", { className: "pickup-tag", style: { display: 'block', fontSize: '0.7rem' }, children: ["inc ", fmtH(required[d].pickup), " pickup"] }))] }, d)))] })] })] })); +} +// ── Staff Table ─────────────────────────────────────────────────────────────── +function pivotWfShifts(wfShifts, dates) { + const members = {}; + for (const date of dates) { + const day = wfShifts[date]; + if (!day) + continue; + for (const s of day.staff) { + if (!members[s.id]) + members[s.id] = { id: s.id, name: s.name, days: {} }; + members[s.id].days[date] = { hours: s.hours, times: s.times }; + } + } + return Object.values(members).sort((a, b) => a.name.localeCompare(b.name)); +} +function StaffTable({ bookings, staff, required, warnOverRed, warnOverAmber, warnUnderAmber, warnUnderRed, onChange, wfShifts, wfStaff }) { + const { dates } = bookings; + function setMemberName(i, name) { + const next = staff.map((m, idx) => idx === i ? { ...m, name } : m); + onChange(next); + } + function setMemberHours(i, date, val) { + const hrs = parseFloat(val); + const next = staff.map((m, idx) => { + if (idx !== i) + return m; + const hours = { ...m.hours }; + if (!isNaN(hrs) && hrs >= 0) + hours[date] = hrs; + else + delete hours[date]; + return { ...m, hours }; + }); + onChange(next); + } + function removeRow(i) { + onChange(staff.filter((_, idx) => idx !== i)); + } + const rotaMembers = pivotWfShifts(wfShifts, dates); + return (_jsxs("table", { className: "hk-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "col-label", children: "Staff Member" }), dates.map(d => _jsx("th", { children: fmtDayHeader(d) }, d)), _jsx("th", { style: { width: '32px' } })] }) }), _jsxs("tbody", { children: [rotaMembers.map(member => (_jsxs("tr", { style: { background: 'rgba(42,100,72,0.05)' }, children: [_jsxs("td", { className: "col-label", children: [_jsx("span", { style: { + display: 'inline-block', fontSize: '0.67rem', fontWeight: 700, + background: 'rgba(42,100,72,0.18)', color: '#1a7a4a', + borderRadius: '3px', padding: '0 4px', marginRight: '0.4rem', lineHeight: '1.5', + }, children: "WF" }), member.name] }), dates.map(date => { + const shift = member.days[date]; + return (_jsx("td", { style: { textAlign: 'center', padding: '0.3rem 0.4rem', verticalAlign: 'middle' }, children: shift ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { fontSize: '0.68rem', color: 'var(--text-mid)', lineHeight: 1.25 }, children: shift.times }), _jsx("div", { style: { fontWeight: 600 }, children: fmtH(shift.hours) })] })) : _jsx("span", { style: { color: 'var(--text-mid)' }, children: "\u2014" }) }, date)); + }), _jsx("td", {})] }, 'wf-' + member.id))), wfStaff.length > 0 && (_jsx("datalist", { id: "wf-staff-datalist", children: wfStaff.map(s => _jsx("option", { value: s.name }, s.id)) })), staff.map((member, i) => (_jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { padding: '0.3rem 0.5rem' }, children: _jsx("input", { className: "hk-text-input", value: member.name, placeholder: "Staff name", list: wfStaff.length > 0 ? 'wf-staff-datalist' : undefined, onChange: e => setMemberName(i, e.target.value) }) }), dates.map(date => (_jsx("td", { style: { padding: '0.3rem 0.4rem' }, children: _jsx("input", { type: "number", className: "hk-num-input", min: 0, max: 24, step: 0.5, value: member.hours[date] ?? '', placeholder: "0", onChange: e => setMemberHours(i, date, e.target.value) }) }, date))), _jsx("td", { children: _jsx("button", { onClick: () => removeRow(i), style: { + background: 'none', border: 'none', color: 'var(--text-mid)', + fontSize: '1rem', padding: '0.2rem 0.4rem', + }, children: "\u00D7" }) })] }, i)))] }), _jsx("tfoot", { children: (() => { + const avail = {}; + for (const date of dates) { + const rotaHrs = rotaMembers.reduce((s, m) => s + (m.days[date]?.hours || 0), 0); + const manualHrs = staff.reduce((s, m) => s + (m.hours[date] || 0), 0); + avail[date] = rotaHrs + manualHrs; + } + return (_jsxs(_Fragment, { children: [_jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontSize: '0.78rem', color: 'var(--text-mid)' }, children: "Total Available" }), dates.map(date => _jsx("td", { children: fmtH(avail[date]) }, date)), _jsx("td", {})] }), _jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontSize: '0.72rem', color: 'var(--text-mid)' }, children: "vs Booked" }), dates.map(date => (_jsx("td", { children: _jsx(PlainDiffCell, { available: avail[date], required: required[date].booked, warnOverAmber: warnOverAmber, warnUnderAmber: warnUnderAmber }) }, date))), _jsx("td", {})] }), _jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontSize: '0.72rem', color: 'var(--text-mid)' }, children: "with Recurring Tasks" }), dates.map(date => (_jsx("td", { children: _jsx(PlainDiffCell, { available: avail[date], required: required[date].booked + required[date].general, warnOverAmber: warnOverAmber, warnUnderAmber: warnUnderAmber }) }, date))), _jsx("td", {})] }), _jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontSize: '0.72rem', color: 'var(--text-mid)' }, children: "with Pickup" }), dates.map(date => (_jsx("td", { children: _jsx(PlainDiffCell, { available: avail[date], required: required[date].booked + required[date].general + required[date].pickup, warnOverAmber: warnOverAmber, warnUnderAmber: warnUnderAmber }) }, date))), _jsx("td", {})] }), _jsxs("tr", { style: { background: '#e2e8f0' }, children: [_jsx("td", { className: "col-label", style: { fontWeight: 700 }, children: "with Adjustments" }), dates.map(date => (_jsx("td", { children: _jsx(DiffCell, { available: avail[date], required: required[date].total, warnOverRed: warnOverRed, warnOverAmber: warnOverAmber, warnUnderAmber: warnUnderAmber, warnUnderRed: warnUnderRed }) }, date))), _jsx("td", {})] })] })); + })() })] })); +} +function PlainDiffCell({ available, required, warnOverAmber, warnUnderAmber }) { + if (available === 0 && required === 0) + return _jsx("span", { style: { color: 'var(--text-mid)' }, children: "\u2014" }); + const diff = available - required; + const icon = diff > warnOverAmber ? '⚠' : diff < -warnUnderAmber ? '✗' : '✓'; + return _jsxs("span", { style: { color: 'var(--text-mid)', fontSize: '0.82rem' }, children: [icon, " ", diff >= 0 ? '+' : '', fmtH(diff)] }); +} +function DiffCell({ available, required, warnOverRed, warnOverAmber, warnUnderAmber, warnUnderRed }) { + if (available === 0 && required === 0) + return _jsx("span", { style: { color: 'var(--text-mid)' }, children: "\u2014" }); + const diff = available - required; + if (diff > warnOverRed) + return _jsxs("span", { className: "diff-over-red", children: ["\u26A0 ", fmtH(diff), " spare"] }); + if (diff > warnOverAmber) + return _jsxs("span", { className: "diff-over", children: ["\u26A0 ", fmtH(diff), " spare"] }); + if (diff < -warnUnderRed) + return _jsxs("span", { className: "diff-under", children: ["\u2717 ", fmtH(Math.abs(diff)), " short"] }); + if (diff < -warnUnderAmber) + return _jsxs("span", { className: "diff-under-amber", children: ["\u2717 ", fmtH(Math.abs(diff)), " short"] }); + return _jsxs("span", { className: "diff-ok", children: ["\u2713 ", diff >= 0 ? '+' : '', fmtH(diff)] }); +} +// ── Adjustments Table ───────────────────────────────────────────────────────── +function AdjustmentsTable({ bookings, adjustments, onChange }) { + const { dates } = bookings; + function setLabel(i, label) { + onChange(adjustments.map((a, idx) => idx === i ? { ...a, label } : a)); + } + function setHours(i, date, val) { + const hrs = parseFloat(val); + onChange(adjustments.map((a, idx) => { + if (idx !== i) + return a; + const hours = { ...a.hours }; + if (!isNaN(hrs)) + hours[date] = hrs; + else + delete hours[date]; + return { ...a, hours }; + })); + } + function removeRow(i) { + onChange(adjustments.filter((_, idx) => idx !== i)); + } + if (adjustments.length === 0) { + return (_jsx("p", { style: { color: 'var(--text-mid)', fontSize: '0.82rem', padding: '0.5rem 0' }, children: "No adjustments \u2014 use \"+ Add adjustment\" above to add a one-off hour offset for a specific date." })); + } + return (_jsxs("table", { className: "hk-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { className: "col-label", children: "Label" }), dates.map(d => _jsx("th", { children: fmtDayHeader(d) }, d)), _jsx("th", { style: { width: '32px' } })] }) }), _jsx("tbody", { children: adjustments.map((adj, i) => (_jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { padding: '0.3rem 0.5rem' }, children: _jsx("input", { className: "hk-text-input", value: adj.label, placeholder: "e.g. Rooms from Sunday", onChange: e => setLabel(i, e.target.value) }) }), dates.map(date => (_jsx("td", { style: { padding: '0.3rem 0.4rem' }, children: _jsx("input", { type: "number", className: "hk-num-input", step: 0.25, value: adj.hours[date] ?? '', placeholder: "0", onChange: e => setHours(i, date, e.target.value) }) }, date))), _jsx("td", { children: _jsx("button", { onClick: () => removeRow(i), style: { + background: 'none', border: 'none', color: 'var(--text-mid)', + fontSize: '1rem', padding: '0.2rem 0.4rem', + }, children: "\u00D7" }) })] }, i))) }), _jsx("tfoot", { children: _jsxs("tr", { children: [_jsx("td", { className: "col-label", style: { fontSize: '0.78rem', color: 'var(--text-mid)' }, children: "Total" }), dates.map(date => { + const total = adjustments.reduce((s, a) => s + (a.hours[date] || 0), 0); + return (_jsx("td", { style: { color: total < 0 ? 'var(--danger)' : total > 0 ? '#1a7a4a' : 'var(--text-mid)', fontWeight: total !== 0 ? 600 : undefined }, children: total === 0 ? '—' : (total > 0 ? '+' : '') + fmtH(total) }, date)); + }), _jsx("td", {})] }) })] })); +} diff --git a/frontend/src/pages/Planner.tsx b/frontend/src/pages/Planner.tsx index bbcb644..1cccfd3 100644 --- a/frontend/src/pages/Planner.tsx +++ b/frontend/src/pages/Planner.tsx @@ -157,7 +157,7 @@ export function Planner() { function flash(text: string, err = false) { if (saveMsgTimer.current) clearTimeout(saveMsgTimer.current) setSaveMsg({ text, err }) - saveMsgTimer.current = setTimeout(() => setSaveMsg(null), err ? 5000 : 2500) + saveMsgTimer.current = setTimeout(() => setSaveMsg(null), err ? 15000 : 2500) } function stampLastReviewed() { @@ -212,18 +212,19 @@ export function Planner() { } }, [wfShifts]) - // Beacon save on unload + // Keepalive save on unload (sendBeacon only supports POST but routes are PUT) useEffect(() => { + function keepalivePut(path: string, body: unknown) { + fetch(path, { + method: 'PUT', keepalive: true, credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + } function onUnload() { - if (staff.length) { - navigator.sendBeacon('/hk-planner/api/config/staff', JSON.stringify({ staff_data: staff })) - } - if (Object.keys(pickup).length) { - navigator.sendBeacon('/hk-planner/api/config/pickup', JSON.stringify({ pickup_data: pickup })) - } - if (adjustments.length) { - navigator.sendBeacon('/hk-planner/api/config/adjustments', JSON.stringify({ adjustments })) - } + if (staff.length) keepalivePut('/hk-planner/api/config/staff', { staff_data: staff }) + if (Object.keys(pickup).length) keepalivePut('/hk-planner/api/config/pickup', { pickup_data: pickup }) + if (adjustments.length) keepalivePut('/hk-planner/api/config/adjustments', { adjustments }) } window.addEventListener('beforeunload', onUnload) return () => window.removeEventListener('beforeunload', onUnload) diff --git a/frontend/src/types.js b/frontend/src/types.js new file mode 100644 index 0000000..977b662 --- /dev/null +++ b/frontend/src/types.js @@ -0,0 +1,3 @@ +export function can(user, cap) { + return user.is_admin || user.caps.includes(cap); +} diff --git a/workforce-api-docs.md b/workforce-api-docs.md new file mode 100644 index 0000000..c6fd244 --- /dev/null +++ b/workforce-api-docs.md @@ -0,0 +1,10096 @@ +# Workforce.com API v2 Docs + +Paste relevant sections below. + +Endpoints needed: +- Auth / how bearer token is used in requests +- List employees (ideally filterable by department/location) +- Get scheduled shifts by date range (fields: employee, date, start time, end time or shift duration) + +--- + + + +🚀 Workforce API v2 +Getting Started +Welcome to the Workforce API! This documentation steps you through authenticating to the API, as well as the various endpoints and methods supported. + +Not sure where to start? Check out the quick start guide, or browse some code samples on GitHub. + +There’s two ways to connect to the API. Use Authorization Code if you’re making an add-on for other Workforce users to use, or use Password if you’re building an integration just for your own account. + +Authentication (Authorization Code) +Use Authorization Code authentication if you’re making an add-on for other Workforce customers to use. For example, if you are building an integration that connects your custom reporting platform to Workforce data, you’ll probably want to let lots of Workforce customers use it. This is the approach for you. + +If you have a log in for the Workforce platform you have access to an Authorization code. This code is linked to your organisation and permisson levels. Just like using an account in Workforce the permission levels will give you the same access in the API, e.g. if you don’t have permission to approve a timesheet in Workforce, you won’t be able to do so via the API. + +Once you have permission an a working Workforce account, you can get an application ID, secret, and redirect URI. You can get all of these from the applications page. With those details, follow these steps to authenticate a user using the Authorization Code authentication flow. + +The Authorization Code approach uses the OAuth 2 authorization code grant type. If you aren’t familiar with it, this is a good, practical introduction. + +Tokens are encrypted, and the encryption key is different between environments. If you are testing in an environment that is not production, the tokens will not work. +1. Redirect the user to the authorize endpoint +Allow users on your website to authenticate themselves with Workforce by redirecting them to the following URL. Where APPLICATION_ID and REDIRECT_URI are the values specific to your app, and the scope parameter is the relevant Workforce OAuth2 scopes your want access to (more information: Scopes). + +https://my.workforce.com/api/oauth/authorize?scope=SCOPE1+SCOPE2&client_id=YOUR_APPLICATION_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code + +2. Catch the request to your redirect URI +The Workforce server will then redirect the request back to your redirect URI, with a request code in the URL parameters. So if your redirect URI is https://mysite.com/callback then the request will be made to https://mysite.com/callback?code=AUTHORIZATION_CODE. + +For local testing, as the browser gets redirected to the URI, it is possible to set it to a local address (i.e. http://localhost:3000/callback) to allow you to test your OAuth web app before deploying your code. + +3. Make a POST request to the token endpoint +Now that you’ve got your authorization code, you can finally make the POST request to get your access token (don’t worry, this is the last step). + +From your server/application make a POST request to https://my.workforce.com/api/oauth/token. + +The response should look something like this: + +{ "access_token": "6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91", "token_type": "bearer", "expires_in": 7200, "refresh_token": "ddeb6480f06247e3635826dd5e3875ece6f64a27044516731a914897431ab446", "scope": "me", "created_at": 1457304578 } + +expires_in is in seconds, so your token will last 2 hours. To learn how to refresh your token, see Refreshing your Token. +curl https://my.workforce.com/api/oauth/token -X POST -H "Cache-Control: no-cache" \ -F "client_id=YOUR_APPLICATION_ID" \ -F "client_secret=YOUR_SECRET" \ -F "code=AUTHORIZATION_CODE" \ -F "redirect_uri=YOUR_REDIRECT_URI" \ -F "grant_type=authorization_code" + +Refreshing your Token +If you got your token using the Authorization Code authentication flow, and it expires, then using the the refresh_token you got above, you are able to refresh it by making a POST request to https://my.workforce.com/api/oauth/token. + +The response should look something like this: + +{ "access_token": "ddeb6480f06247e3635826dd5e3875ece6f64a27044516731a914897431ab446", "token_type": "bearer", "expires_in": 7200, "refresh_token": "d0692903972ff6559ec5f0e3165cabd5b87f47e3613431ad53805b5397268206", "scope": "me", "created_at": 1457311778 } + +You can now go on using this new access token for another 2 hours. + +If your application’s access is revoked by the user, then you will need to run through the authentication process again to obtain a new access token. +Although the tokens only last for 2 hours, you can refresh your token as many times as you want. The refresh token has no expiry but can only be used once. +curl https://my.workforce.com/api/oauth/token -X POST -H "Cache-Control: no-cache" \ -F "client_id=YOUR_APPLICATION_ID" \ -F "client_secret=YOUR_SECRET" \ -F "refresh_token=REFRESH_TOKEN" \ -F "redirect_uri=YOUR_REDIRECT_URI" \ -F "grant_type=refresh_token" + +Authentication (Password) +Use Password authentication if you’re building an integration just for your own account. For example, if you’re building an integration for a specific customer - either as an in-house developer or as a consultant - this is the the approach for you. + +If you’re using the API for a single account, the Password authentication flow is an easier way to get set up. You can authenticate via the command line using your Workforce login details, or you can just set up an access token through the Workforce API management page. + +Access tokens generated using Password authentication never expire, but can be revoked from the API management page. + +1. Make a POST request to the token endpoint +From your server/application make a POST request to https://my.workforce.com/api/oauth/token with the user’s email and password. + +The response should look something like this: + +{ "access_token": "6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91", "token_type": "bearer", "scope": "me", "created_at": 1457304578 } + +Password authenticated tokens have no expiry. They can also be created and revoked from the access tokens page. +curl https://my.workforce.com/api/oauth/token -X POST -H "Cache-Control: no-cache" \ -F "username=USER_EMAIL" \ -F "password=USER_PASSWORD" \ -F "scope=SCOPE1 SCOPE2" \ -F "grant_type=password" + +Making Requests +Once either of the above authentication methods have been completed, you’ll have an access token (in the case of Authorization Code authentication, the token will only last for 2 hours). + +Your access token will look something like this: 6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91. From here on out all requests you make to the Workforce v2 API must include the token in the header like so: + +Authorization: bearer 6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91 + +See the right hand sidebar for an example of a request that gets information about the user that just authenticated. + +curl --header "Authorization: bearer 6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91" \ https://my.workforce.com/api/v2/users/me + +Invalid Subscriptions +If you receive a 402 response status with this message: + +Your account is locked out for billing purposes, and cannot use the API! + +This indicates that the customer’s account does not have a valid credit card or other billing method set, and their free trial has ended. Therefore they will not be able to access some components of Workforce either through the website or the API. + +You can query the current user endpoint to check if a user has a valid subscription. + +Rate Limiting +Requests to the API are rate limited to 200 requests, per 1 minute for each requester. + +A requester is defined through the following workflow: + +Is the request using a Password Access Token? +Yes? Then the requester is the current user (meaning all requests made though any of my password tokens are counted together). +No? The the requester is the combination of the OAuth app and current user. +When making requests to the API, the following headers will be added to the response: X-RateLimit-Limit: 200 X-RateLimit-Remaining: 52 X-RateLimit-Reset: 1461211225 + +X-RateLimit-Limit is the your rate limit +X-RateLimit-Remaining is the remaining number of requests you can make before you will be rate limited +X-RateLimit-Reset is when the rate limit will be reset +If have reached the rate limit, then an additional header will be present: X-RateLimit-RateLimited: true + +Here’s an example to make things a little clearer: + +I have multiple Password Access Tokens with different scopes, token_1, token_2. +I have an OAuth app, which is used by user_1, user_2 and myself. +user_2 has also created an OAuth app which I am using. +This is the maximum number of requests that can be made in a single minute: + +199 requests with token_1 +1 request with token_2 (I have 200 requests/min for all my Password Access Tokens) +200 requests on my behalf through user_2’s OAuth app +200 requests on my behalf through my OAuth app +200 requests on user_1’s behalf through my OAuth app +200 requests on user_2’s behalf through my OAuth app +In summary the distinct requesters in this example are: + +Me through my Password Access Tokens +Me through user_2’s OAuth app +Me through my OAuth app +user_1 through my OAuth app +user_2 through my OAuth app +If you reach the rate limit, you’ll get the following 429 response: + +Headers X-RateLimit-Limit: 200 X-RateLimit-RateLimited: true X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1461211225 + +Body { "error": "API rate limit exceeded!" } + +Caching +The Workforce API supports the If-None-Match HTTP header. All API responses will include an ETag response header. You can use the value of this header in future requests to the same endpoint by passing it to the If-None-Match request header. If the content the API would return has not changed, you will get back a 304 Not Modified response. + +The right sidebar has an example of an ETag being included in a request. For more information about ETags and If-None-Match, see Mozilla’s guide. + +curl --header "If-None-Match: 17f1542065ae37d0963a608fd632b4615a288ceb" --header "Authorization: bearer 6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91" \ https://my.workforce.com/api/v2/users/me + +TLS Version +The Workforce API only supports TLS version 1.2 or higher. If your API client attempts to connect with a lower version of TLS or SSL then you will recieve a TLS handshake error. + +Scopes +In both the Authorization Code and Password authentication methods, you are required to include a scope parameter. Here is a complete list of the scopes in the Workforce API, additionally, in this documentation each endpoint specifies which scope/s it requires. + +Scope Endpoints Description +me Users, Shift Reminders Access information about the current user +roster Rosters, Schedules, Schedule Swap Plans, Custom Events Manage roster, schedule information, shift swaps and custom events +timesheet Timesheets, Shifts Manage timesheet and shift information +department Locations, Teams (Departments), Shift Details, Custom Events Manage location, department (team), and shift detail information +user Users, Award Tags, Clock Ins Manage personal information about your employees (without wages) +cost Users, Timesheets, Shifts, Rosters, Schedules, WageComparisons Mange information about your employee’s wage, and see costs for timesheets and rosters +leave Leave Requests, Leave Balances Manage leave requests and balances +unavailability Availability, Unavailability Manage availability and unavailability +datastream Data Streams, Data Stream Joins, Store Stats Manage your data streams and store stats +device Devices, Clock Ins Manage timeclock information, and read clock ins for devices +qualifications Qualifications Manage qualifications +settings Settings, Award Templates Manage account settings and award related configuration +organisation Organisations View & create client organisations +sms SMS Send SMS +personal Personal Details Manage your personal details (without Bank Details, TFN, etc) +financial Personal Details, Users Access financial data about your employees (Bank Details, TFN, etc) +communication Announcements Manage announcements +platform Platform Get platform data +Not only will you need the relevant scopes to access the endpoints, but also the correct system permissions in Workforce. For more information see each endpoint group. +By selecting only the bare minimum scopes that your application needs, users will feel more comfortable authorizing your application. +For example: If you had all scopes selected but your application simply collects information about users (using the User endpoint, and user scope) people will question why your application needs to have access to timesheet data (timesheet scope) and will be less likely to approve it. + +Dates and Times +All dates in the Workforce API are rendered in YYYY-MM-DD format. Any request you make must use YYYY-MM-DD format for all dates. + +All times in the Workforce API are rendered as Unix time, this is to avoid confusion as there can be users with different timezones in the same organisation. A user’s timezone can be retrieved from the both the User endpoint and the Current User endpoint. Any request you make must use Unix time for all times. + +Our user guide has more information on working with unix times through the API. +Update Timestamps +As well as the fields listed in the API examples below, every API response will also include an updated_at timestamp. This represents the last time that an object was updated. Like all other times returned by the API, it will be formatted as a Unix timestamp. + +Many GET endpoints support an updated_after querystring parameter. Use this to filter results API-side by the updated_at field. Some endpoints also support a show_costs parameter, and return a last_costed_at field. If you pass both show_costs and updated_after, we will compare to both the updated_at and last_costed_at field when filtering. + +An example may illustrate this better. Say you have a shift, and at 10am you change its start time. Then, at 11am, costing details for the shift’s employee are changed, which causes the cost of the shift to update. The shift’s updated_at will be 10am, while its last_costed_at will be 11am. If you make a call to the shift GET endpoint with updated_after of 10:30am, the shift will not be returned. But if you include the show_costs parameter when making the call, the shift will be returned since it was last costed after 10:30am. + +Audit Trails +Every change made in Workforce through the API is logged and added to the in-Workforce audit trail. By default, changes will be marked as being made by “API v2”, with the user ID that corresponds to the authenticated user. If you’re building an integration that requires more fine tuned auditing (for example, a token is shared by multiple users, but you’re able to identify which user is responsible for a particular action), you can customise the user ID and audit trail name that are displayed. Email developers@tanda.co and we can help you set this up. + +Code Samples +We have a repository of code samples available on GitHub. Here’s some guides you might find helpful: + +Getting started +Building a connection to Salesforce +Show us what you built! +If you’ve built something neat using the API, we’d love to see it. If you’ve already been chatting to someone from our team, odds are they’d love to see a demo of your integration. Or if you haven’t met any of our team before, a good way to introduce yourself is to email developers@tanda.co. + +General +Current User [/api/v2/users/me] +GETGet Current User +Gets information about the current user (i.e. the user that has been authenticated). + +The following fields are returned: + +id - the user’s unique ID. +name - the user’s preferred name. +legal_first_name - the user’s legal first name. +legal_middle_names - the user’s legal middle name/s. +legal_last_name - the user’s legal last name. +email - the user’s email. +photo - if present, a link to a photo of the user. +time_zone - the name of the time zone the user is in - uses zone names from the IANA Time Zone Database. See our user guide for more on using times with the Workforce API. +utc_offset - the UTC offset for the current user, this can be used as a compliment to the time zone name. See our user guide for more on using times with the Workforce API. +organisation - the name of the currently authenticated organisation. A user can belong to one or more organisations. Unless otherwise specified on the individual endpoint, only data from this organisation will be returned by any API calls using the current access token. +organisation_id - the ID of the currently authenticated organisation. A user can belong to one or more organisations. Unless otherwise specified on the individual endpoint, only data from this organisation will be returned by any API calls using the current access token. +locale - the locale of the user. +country - the country the user’s organisation is based in. +permissions - The user’s privilege levels in the system, from the set of ['organisation_admin', 'payroll_officer', 'roster_manager', 'manager', 'employee', 'partner']. +valid_subscription - If true, the user will be able to access other API endpoints. +user_ids - User IDs associated with this user. This array will contain more than one item if the user has multiple profiles. If you want to get all data relating to a person across all their workplaces, use these. If you need to restrict your API call to a specific user, you can use the X-User-Id header and one of these IDs. +organisations - Organisations at which this user has profiles, based on user_ids. If your application needs to deal with people who may work at more than one Workforce organisation, you should use this. If you need to restrict your API call to a specific organisation, you can use the X-Organisation-Id header and one of these IDs. Note that a user may have multiple profiles within the same organisation. +This method requires the me scope (see Scopes for more information). +200 +OK +(application/json) +Body + + { + "id": 123456, + "name": "Lenny Leonard", + "legal_first_name": "Lenny", + "legal_middle_names: "James", + "legal_last_name": "Leonard", + "email": "lenny.leonard@springfieldpowerplant.com", + "photo": "http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png", + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "organisation": "Moe's Tavern", + "organisation_id": 126546, + "locale": "en-US", + "country": "United States", + "permissions": [ + "employee", + "manager" + ], + "valid_subscription": true, + "user_ids": [ + 1, + 2, + 123456 + ], + "organisations": [ + { + "name": "Moe's Tavern", + "id": 1, + "user_id": 1, + "locale": "en-US", + "country": "United States" + }, + { + "name": "Bob's Burgers", + "id": 2, + "user_id": 1, + "locale": "en-US", + "country": "United States" + }, + { + "name": "Dave's Doors", + "id": 3, + "user_id": 123456, + "locale": "en-AU", + "country": "Australia" + } + ], + "updated_at": 1478650040 + } + +Rosters +Rosters are the container for scheduled shifts, each roster runs for 1 week and contains Schedules. For actual times worked, please see Timesheets or Shifts. + +Rosters are not created directly. Instead a roster will be created when creating through the Schedules endpoint. + +No special Workforce permissions are required to use the roster endpoint. +On-cost amounts are estimates +On-cost amounts are estimates. They are calculated by applying the organisation’s on-cost configurations to award-interpreted wage costs, consistent with Workforce’s cost reports. They are not statutory payroll calculations. For example, employer National Insurance is estimated as the configured percentage of all eligible cost rather than HMRC’s banded calculation on earnings above the threshold, and superannuation rules that depend on age or contribution caps are not applied. Use payroll data for exact figures. + +Some on-cost configurations only apply once an employee’s earnings pass a threshold (such as employer National Insurance). Eligibility can only be assessed against the employee’s full pay period, so these configurations are not included in on-cost amounts on the rosters endpoint for UK organisations. Use the timesheets endpoint for threshold-aware on-costs. + +Roster [/api/v2/rosters/{id}] +GETGet Roster +Get a roster by id. + +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true, the cost scope is also required. + +Parameters +id: 70074 (number) - The id of the roster +show_costs: true (optional, boolean) - Whether to show the costs for the roster (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each schedule, and total_oncosts_breakdown and cost_with_oncosts to the roster total (defaults to false) +200 +OK +(application/json) +Attributes: +id: 70074 (required) (number) - The id of the roster +schedules (required) (array of DailyScheduleData) - The roster broken down by days +start: 2016-02-29 (required) (string) - The date of the first day of the roster +finish: 2016-03-07 (required) (string) - The date of the last day of the roster +cost: 1200.2 (number) - The total cost of the roster, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts for the returned schedules (e.g. [{"name": "Superannuation", "amount": 144.02}, {"name": "Workcover", "amount": 28.80}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover a schedule’s employee and location contribute entries. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint. Like cost, this reflects only the current page when pagination parameters are used. +cost_with_oncosts: 1344.22 (number) - The total roster cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +meta (MetaData) - The pagination metadata, if page and page_size parameters are set +Current Roster [/api/v2/rosters/current] +GETGet the Current Roster +Get the current roster. + +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true, the cost scope is also required. + +Parameters +show_costs: true (boolean) - Whether to show the costs for the roster (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions alongside existing costs. Requires show_costs=true (defaults to false) +200 +OK +(application/json) +Attributes: +id: 70074 (required) (number) - The id of the roster +schedules (required) (array of DailyScheduleData) - The roster broken down by days +start: 2016-02-29 (required) (string) - The date of the first day of the roster +finish: 2016-03-07 (required) (string) - The date of the last day of the roster +cost: 1200.2 (number) - The total cost of the roster, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts for the returned schedules (e.g. [{"name": "Superannuation", "amount": 144.02}, {"name": "Workcover", "amount": 28.80}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover a schedule’s employee and location contribute entries. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint. Like cost, this reflects only the current page when pagination parameters are used. +cost_with_oncosts: 1344.22 (number) - The total roster cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +meta (MetaData) - The pagination metadata, if page and page_size parameters are set +Roster that Contains Date [/api/v2/rosters/on/{date}] +GETGet Roster that Contains Date +Get the roster that contains a date, will return a 204 response if there is no roster. + +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true, the cost scope is also required. + +Parameters +date: 2016-03-02 (string) - The date that’s contained in the roster you are looking for. +show_costs: true (optional, boolean) - Whether to show the costs for the roster (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each schedule, and total_oncosts_breakdown and cost_with_oncosts to the roster total (defaults to false) +page: 1 (optional, number) - The page number for your nested schedule list +page_size: 50 (optional, number) - The number of nested schedules retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes: +id: 70074 (required) (number) - The id of the roster +schedules (required) (array of DailyScheduleData) - The roster broken down by days +start: 2016-02-29 (required) (string) - The date of the first day of the roster +finish: 2016-03-07 (required) (string) - The date of the last day of the roster +cost: 1200.2 (number) - The total cost of the roster, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts for the returned schedules (e.g. [{"name": "Superannuation", "amount": 144.02}, {"name": "Workcover", "amount": 28.80}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover a schedule’s employee and location contribute entries. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint. Like cost, this reflects only the current page when pagination parameters are used. +cost_with_oncosts: 1344.22 (number) - The total roster cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +meta (MetaData) - The pagination metadata, if page and page_size parameters are set +Sales Targets [/api/v2/rosters/sales_targets/{type}/{date}] +GETGet Sales Target +Get the sales target for a given roster with no filter. + +This method requires the roster scope (see Scopes for more information). +Parameters +type: (string) - There are two possible values: day and week. +date: 2018-02-26 (string) - A date within the roster you are looking for. +200 +OK +(application/json) +Attributes: +target: 1234.56 (string) - Value of the sales target. null if there is no roster for the given date. +created_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was first created, if there is one. Otherwise null. +updated_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was last updated, if there is one. Otherwise null. +user_entered: true (required) (boolean) - Whether or not the default projection was overridden with a user-entered value. +Sales Targets for Team (Department) [/api/v2/rosters/sales_targets/{type}/{date}] +GETGet Target for Team +Get the sales target for a given roster filtered to a team. + +You must either be an Admin or General Manager, or have read access to the team you are requesting. +This method requires the roster scope (see Scopes for more information). +Parameters +type: (string) - There are two possible values: day and week. +date: 2018-02-26 (string) - A date within the roster you are looking for. +department_id: 123 (optional, number) - The ID of a team to filter the roster to. +200 +OK +(application/json) +Attributes: +target: 1234.56 (string) - Value of the sales target. null if there is no roster for the given date. +created_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was first created, if there is one. Otherwise null. +updated_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was last updated, if there is one. Otherwise null. +user_entered: true (required) (boolean) - Whether or not the default projection was overridden with a user-entered value. +Sales Targets for Location [/api/v2/rosters/sales_targets/{type}/{date}] +GETGet Target for Location +Get the sales target for a given roster filtered to a location. + +You must either be an Admin or General Manager, or have read access to the location you are requesting. +This method requires the roster scope (see Scopes for more information). +Parameters +type: (string) - There are two possible values: day and week. +date: 2018-02-26 (string) - A date within the roster you are looking for. +location_id: 456 (optional, number) - The ID of a location to filter the roster to. +200 +OK +(application/json) +Attributes: +target: 1234.56 (string) - Value of the sales target. null if there is no roster for the given date. +created_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was first created, if there is one. Otherwise null. +updated_at: 1519621685 (required) (number, nullable) - When the user-entered sales target was last updated, if there is one. Otherwise null. +user_entered: true (required) (boolean) - Whether or not the default projection was overridden with a user-entered value. +Schedules +Schedules represent planned shifts for a user and are contained in a Roster, for actual times worked please see Timesheets or Shifts. + +No special Workforce permissions are required to view schedules, however only the schedules that would be visible through the UI are visible through the API. +To create or change schedules, the user must be a team manager, roster manager, or admin. In the case of the team manager, the shift must be for someone in the user’s managed teams. + +Employees can only see published schedules. Specifying ids of unpublished schedules, or using the published_only flag will not change this. + +Looking to just get the roster for an individual employee? Check out our user guide for a workflow. +Schedules [/api/v2/schedules] +GETGet Schedules +Get multiple schedules by id. + +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true or show_award_interpretation=true, the cost scope is also required. + +Parameters +ids: 1,2,31337157 (required, string) - Comma separated list of schedule ids +show_costs: true (optional, boolean) - Whether to show the costs for the schedule (defaults to false). +show_award_interpretation: true (optional, boolean) - Whether to show costs breakdown for the schedule (defaults to false). +include_names: false (optional, boolean) - If true, the department name and shift detail name (not just ID) will be included in the response. Defaults to false. +show_activities: false (optional, boolean) - If true, the activities within each schedule will be included in the response, each with its activity_code/activity_code_id and prorated worked hours. With show_costs=true (and the cost scope) each activity also includes its derived cost. With include_names=true, each activity also includes the activity code’s name. Defaults to false. +include_colleagues: false (optional, boolean) - If true, shifts for colleagues will be returned. If false, only shifts for the current user will be returned. Defaults to false. +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified schedules +published_only: false (optional, boolean) - Only retrieve schedules that have been published. Useful for managers to see only published schedules, where they would usually see any schedule. Defaults to false. +location_id: 111 (optional, number) - The id of the schedule’s location, for filtering. +location_ids: “111,112” (optional, string) - Comma delimited location ids, for filtering. +show_vacant_only: false (optional, boolean) - Only retrieve schedules that are vacant. Defaults to false. +page: 1 (optional, number) - The page number for your schedules list +page_size: 50 (optional, number) - The number of schedules retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +GETGet Schedules by User +Get multiple schedules by user IDs, from, and to date. From and to date are required, while user IDs are optional. If no user IDs are provided, all visible schedules (based on the current user’s permission levels) will be returned. + +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true, the cost scope is also required, and you must have access to see wages for all user IDs provided. + +Parameters +user_ids: 1,2,123456 (required, string) - Comma separated list of user ids. You can pass user IDs from multiple different organisations, for example if you want to get all schedules for a particular person across companies. +from: 2016-03-02 (required, string) - From date to lookup schedules in. If provided, from and to can be at most 7 days apart. +to: 2016-03-02 (required, string) - To date to lookup schedules in. If provided, from and to can be at most 7 days apart. +show_costs: true (optional, boolean) - Whether to show the costs for the schedule (defaults to false). +show_award_interpretation: true (optional, boolean) - Whether to show costs breakdown for the schedule (defaults to false). +include_names: false (optional, boolean) - If true, the department name and shift detail name (not just ID) will be included in the response. Defaults to false. +show_activities: false (optional, boolean) - If true, the activities within each schedule will be included in the response, each with its activity_code/activity_code_id and prorated worked hours. With show_costs=true (and the cost scope) each activity also includes its derived cost. With include_names=true, each activity also includes the activity code’s name. Defaults to false. +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified schedules +location_id: 111 (optional, number) - The id of the schedule’s location, for filtering. +location_ids: “111,112” (optional, string) - Comma delimited location ids, for filtering. +200 +OK +(application/json) +Attributes (array): +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +POSTCreate Schedule +This will create a Roster for the given date, if it does not already exist. + +Only one of breaks and automatic_break_length can be set to non-null values. +The optional activities array creates ScheduleActivityData activities within the schedule. Each entry has a start, finish, and optional activity_code_id (which must belong to the same organisation). See Update Schedule for how activities are reconciled on update. +The optional top-level activity_code_id sets the schedule’s own (fallback) activity code — applied to worked time not covered by any activity. The code must belong to the organisation, be unarchived, and apply to the schedule’s department. On update, pass null to clear it. +This method requires the roster scope (see Scopes for more information). +Request Create Schedule (application/json) +Body + + { + "user_id": 123456, + "start": 1456903800, + "finish": 1456911000, + "department_id": 111, + "automatic_break_length": 30, + "activities": [ + { + "start": 1456905600, + "finish": 1456909200, + "activity_code_id": 7 + } + ] + } + +200 +OK +(application/json) +Body + + { + "id": 31337158, + "roster_id": 70074, + "user_id": 123456, + "start": 1456903800, + "finish": 1456911000, + "breaks": [], + "automatic_break_length": 30, + "department_id": 111, + "shift_detail_id": null, + "last_published_at": null, + "last_acknowledged_at": null, + "acceptance_status": "not_accepted" + } + +Schema + +Attributes: +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +id: (number) +Request Create vacant Schedule (application/json) +Body + + { + "start": 1456903800 + } + +200 +OK +(application/json) +Body + + { + "id": 31337158, + "roster_id": 70074, + "user_id": null, + "start": 1456903800, + "finish": null, + "breaks": [], + "automatic_break_length": null, + "department_id": null, + "shift_detail_id": null, + "last_published_at": null, + "last_acknowledged_at": null, + "acceptance_status": "not_accepted" + } + +Schema + +Attributes: +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +id: (number) +Schedule [/api/v2/schedules/{id}] +GETGet Schedule +Get a schedule by id. + +If you are not able to see the schedule through the UI then you will not be able to see it through the API. +This method requires the roster scope (see Scopes for more information). +If you are using show_costs=true, the cost scope is also required. + +Parameters +id: 31337157 (number) - The id of the schedule +show_costs: true (optional, boolean) - Whether to show the costs for the schedule (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show costs breakdown for the schedule (defaults to false). +include_names: false (optional, boolean) - If true, the department name and shift detail name (not just ID) will be included in the response. Defaults to false. +show_activities: false (optional, boolean) - If true, the activities within each schedule will be included in the response, each with its activity_code/activity_code_id and prorated worked hours. With show_costs=true (and the cost scope) each activity also includes its derived cost. With include_names=true, each activity also includes the activity code’s name. Defaults to false. +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +200 +OK +(application/json) +Attributes: +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +PUTUpdate Schedule +Only one of breaks and automatic_break_length can be set to non-null values. If automatic_break_length or breaks is already set, you will not be able to change the other unless the set field is cleared (can be done in the same request by setting the currently set field to null). +The optional activities array replaces the schedule’s ScheduleActivityData activities. Activities are reconciled by their time window (start and finish): an entry matching an existing activity updates that activity’s activity_code_id in place (the activity is kept, not recreated), existing activities absent from the array are deleted, and entries with a new window are created. Omitting activities leaves the schedule’s activities untouched; passing an empty array [] deletes all of them. Each entry’s activity_code_id must belong to the same organisation. +The top-level activity_code_id sets the schedule’s own (fallback) activity code; pass null to clear it, or omit it to leave it untouched. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 31337157 (number) - The id of the schedule +Request Update Schedule (application/json) +Body + + { + "start": 1456898400, + "breaks": [ + { + "start": 1456903800, + "finish": 1456905600, + "paid": false, + "paid_meal_breaks": false, + "length": 30 + }, + { + "start": 1456909200, + "finish": 1456911000, + "paid": true, + "paid_meal_breaks": false, + "length": 30 + } + ], + "automatic_break_length": null, + "department_id": null, + "activities": [ + { + "start": 1456905600, + "finish": 1456909200, + "activity_code_id": 7 + } + ] + } + +200 +OK +(application/json) +Body + + { + "id": 31337157, + "roster_id": 70074, + "user_id": 123456, + "start": 1456898400, + "finish": 1456916400, + "breaks": [ + { + "start": 1456903800, + "finish": 1456905600, + "paid": false, + "paid_meal_breaks": false, + "length": 30 + }, + { + "start": 1456909200, + "finish": 1456911000, + "paid": false, + "paid_meal_breaks": false, + "length": 30 + } + ], + "automatic_break_length": null, + "department_id": null, + "shift_detail_id": null, + "last_published_at": null, + "last_acknowledged_at": null, + "acceptance_status": "not_accepted" + } + +Request Make Schedule vacant (application/json) +Body + + { + "user_id": nil + } + +200 +OK +(application/json) +Body + + { + "id": 31337157, + "roster_id": 70074, + "user_id": null, + "start": 1456902000, + "finish": 1456916400, + "breaks": [ + { + "start": 1456909200, + "finish": 1456911000, + "paid": false, + "paid_meal_break": false, + "length": 30 + } + ], + "department_id": 111, + "shift_detail_id": null, + "last_published_at": null, + "last_acknowledged_at": null, + "acceptance_status": "not_accepted" + } + +Schema + +Attributes: +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +id: (number) +Request Give Schedule automatic break (application/json) +Body + + { + "automatic_break_length": 25, + "breaks": null + } + +200 +OK +(application/json) +Body + + { + "id": 31337157, + "roster_id": 70074, + "user_id": 123456, + "start": 1456902000, + "finish": 1456916400, + "breaks": [], + "automatic_break_length": 25, + "department_id": 111, + "shift_detail_id": null, + "last_published_at": null, + "last_acknowledged_at": null, + "acceptance_status": "not_accepted" + } + +Schema + +Attributes: +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +id: (number) +DELETEDelete Schedule +This method requires the roster scope (see Scopes for more information). +Parameters +id: 31337157 (number) - The id of the schedule +200 +OK +(application/json) +Bulk Publish Schedules [/api/v2/schedules/publish] +POSTPublish Multiple Schedules +Publish multiple schedules at once. Push notifications are sent to users whose schedules have changed since last publish. + +Only occupied schedules (schedules with a user assigned) will be published. Vacant schedules will be filtered out. +Schedules you do not have permission to update will be filtered out and not published. + +This method requires the roster scope (see Scopes for more information). +Request Publish Schedules (application/json) +Body + + { + "schedule_ids": [123, 456, 789] + } + +201 +Created +(application/json) +Attributes (array): +id: 31337157 (required) (number) - The id of the schedule +roster_id: 70074 (required) (number) - The id of the roster +user_id: 123456 (number) - The id of the user who is assigned the schedule +start: 1456902000 (number) - The start of the schedule +finish: 1456916400 (number) - The end of the schedule +breaks (array of ScheduleBreakData) - The breaks taken in the schedule +automatic_break_length: 30 (number) - Length of automatic break in minutes +department_id: 111 (number) - The department (team) id for the schedule +shift_detail_id: 36 (number) - The shift detail id for the schedule +activity_code_id: 7 (number) - The schedule’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +cost: 20.19 (number) - The cost of the schedule, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this schedule (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee and location are included, so the array can be empty. For UK organisations, earnings-threshold on-costs (such as employer National Insurance) are not included on this endpoint and are only available through the timesheets endpoint. +cost_with_oncosts: 22.61 (number) - The cost of the schedule including on-costs, if show_costs=true and include_oncosts=true parameters are set +last_published_at: 1457002800 (number) - When the schedule was last published to its given employee +acceptance_status: not_accepted (string) - The current status of the schedule +record_id: 532432 (number) - the record ID, for use with Platform endpoints +needs_acceptance: true (boolean) - Has the schedule been accepted by the employee? +creation_method: manual (string) - How the schedule was created. +creation_platform: web (string) - Where the schedule was created. +activities (array of ScheduleActivityData) - The activities within the schedule, if show_activities=true parameter is set +400 +Bad Request +(application/json) +Body + + { + "error": "Cannot publish more than 100 schedules at once" + } + +403 +Forbidden +(application/json) +Body + + { + "error": "You do not have permission to publish schedules!" + } + +404 +Not Found +(application/json) +Body + + { + "error": "No valid schedules found!" + } + +Schedule Versions [/api/v2/schedules/{id}/versions] +GETGet Schedule Versions +If you are not able to see the schedule through the UI then you will not be able to see it through the API. +This method requires the roster scope (see Scopes for more information). +Parameters +id: 31337157 (number) - The id of the schedule +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified schedule versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Schedule” (string) +changes (array) +(object) +field: start +previous: 1456898400 (number) +updated: 1456902000 (number) +(object) +field: finish +previous: 1456912800 (number) +updated: 1456916400 (number) +Timesheets +Timesheets are the container for actual shifts worked, a timesheet can be for a period of 1 or 2 weeks depending on how the organisation pays their staff. Each timesheet contains many Shifts. For rostered times, please see Rosters or Schedules. + +Timesheets are not created directly. Instead a timesheet will be created when creating through the Shifts endpoint. + +Regular employees can only see their own timesheets, roster managers and admins can see all timesheets, and department managers can only see timesheets for staff that they manage. +All methods require the timesheet scope (see Scopes for more information). +Exporting Timesheets +You can download a timesheet in an export format to be imported into a payroll system by providing the export_format URI parameter. This works with any of the timesheet endpoints. + +For example, making a call to /api/v2/timesheets/7007?export_format=myob will return a MYOB-compatible export of the timesheet, and /api/v2/timesheets/on/2016-03-02?export_format=myob will return a MYOB-compatible export of all timesheets for that date range. + +By default all timesheets will be returned. Provide the approved_only parameter to limit your export to approved timesheets (this is the default behaviour in the Workforce UI). For example, /api/v2/timesheets/7007?export_format=myob&approved_only=true. + +The content-type of these responses will not be application/json as they are for other API calls - it will depend on the export format requested, but will generally be text/plain or text/csv. + +Timesheets will have their status set to exported when accessed through this endpoint. If you don’t want this to happen, include the skip_status_update parameter - eg. /api/v2/timesheets/7007?export_format=myob&skip_status_update=true. Alternatively, use the update timesheeet endpoint to reset their status. + +On-cost amounts are estimates +On-cost amounts are estimates. They are calculated by applying the organisation’s on-cost configurations to award-interpreted wage costs, consistent with Workforce’s cost reports. They are not statutory payroll calculations. For example, employer National Insurance is estimated as the configured percentage of all eligible cost rather than HMRC’s banded calculation on earnings above the threshold, and superannuation rules that depend on age or contribution caps are not applied. Use payroll data for exact figures. + +For UK organisations, some on-cost configurations (such as employer National Insurance) only apply when an employee’s projected monthly earnings exceed the applicable HMRC Secondary Threshold. When include_oncosts=true is set, the timesheets endpoint determines threshold eligibility from the employee’s total pay-period earnings normalised to a 28-day monthly equivalent, matching the logic used in Workforce’s cost reports. This works for all pay period lengths. The shifts and rosters endpoints cannot assess threshold eligibility, so they do not include these configurations for UK organisations. + +On-cost configurations restricted to ordinary hours return 0.0 for leave shifts. Configurations that are not restricted still apply to leave shifts. + +For non-UK organisations, on-costs are applied at the flat rate regardless of earnings. + +Current Timesheets [/api/v2/timesheets/current] +GETGet Current Timesheets +If you are using show_costs=true or show_award_interpretation=true, the cost scope is required. +Parameters +show_costs: true (optional, boolean) - Whether to show the costs for the timesheet (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift, and total_oncosts_breakdown and total_cost_with_oncosts to the timesheet. On-costs are calculated from the organisation’s on-cost configurations. For UK organisations, earnings-threshold rules (e.g. employer National Insurance) are applied using the full pay-period earnings - see the note above for details (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the timesheet (defaults to false) +show_notes: true (optional, boolean) - Whether to show notes for the timesheet (defaults to false). Note that notes can exist at the timesheet level or the shift level, and these can be found through notes arrays on the respective objects. +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified timesheets +report_location_id: 111 (optional, number) - The ID of a location for which to filter timesheets to. If provided, only returns timesheets if the timesheet’s user’s default team (report_department_id) is set, and it is a team within this location. +export_format: myob (optional, string) - The format in which timesheet data should be exported. See the Exporting Timesheets section above for more information. +skip_status_update: true (optional, boolean) - If true, timesheets will not have their status set to exported as part of the export. Default is false. Only used if export_format is provided. +approved_only: true (optional, boolean) - If true, only approved timesheets will be returned. Default is false. +200 +OK +(application/json) +Attributes (array): +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +Timesheets on Date [/api/v2/timesheets/on/{date}] +GETGet Timesheets on Date +If you are using show_costs=true or show_award_interpretation=true, the cost scope is required. +Parameters +date: 2016-03-02 (date) - The date included in the timesheet to find +show_costs: true (optional, boolean) - Whether to show the costs for the timesheet (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift, and total_oncosts_breakdown and total_cost_with_oncosts to the timesheet. On-costs are calculated from the organisation’s on-cost configurations. For UK organisations, earnings-threshold rules (e.g. employer National Insurance) are applied using the full pay-period earnings - see the note above for details (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the timesheet (defaults to false) +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified timesheets +report_location_id: 111 (optional, number) - The ID of a location for which to filter timesheets to. If provided, only returns timesheets if the timesheet’s user’s default team (report_department_id) is set, and it is a team within this location. +export_format: myob (optional, string) - The format in which timesheet data should be exported. See the Exporting Timesheets section above for more information. +skip_status_update: true (optional, boolean) - If true, timesheets will not have their status set to exported as part of the export. Default is false. Only used if export_format is provided. +approved_only: true (optional, boolean) - If true, only approved timesheets will be returned. Default is false. +page: 1 (optional, number) - The page number for your timesheet list +page_size: 50 (optional, number) - The number of timesheets retrieved per page. The maximum is 100 timesheets per request. +200 +OK +(application/json) +Attributes (array): +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +Current Timesheet for User [/api/v2/timesheets/for/{user_id}/current] +GETGet Current Timesheet for User +If you are using show_costs=true or show_award_interpretation=true, the cost scope is required. +Parameters +user_id: 123456 (number) - The user’s id, me can also be used to reference the current user +show_costs: true (optional, boolean) - Whether to show the costs for the timesheet (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift, and total_oncosts_breakdown and total_cost_with_oncosts to the timesheet. On-costs are calculated from the organisation’s on-cost configurations. For UK organisations, earnings-threshold rules (e.g. employer National Insurance) are applied using the full pay-period earnings - see the note above for details (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the timesheet (defaults to false) +approved_only: true (optional, boolean) - If true, only approved timesheets will be returned. Default is false. +200 +OK +(application/json) +Attributes: +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +Timesheet for User on Date [/api/v2/timesheets/for/{user_id}/on/{date}] +GETGet Timesheet for User on Date +If you are using show_costs=true or show_award_interpretation=true, the cost scope is required. +Parameters +user_id: 123456 (number) - The user’s id, me can also be used to reference the current user +date: 2016-03-02 (date) - The date included in the timesheet to find +show_costs: true (optional, boolean) - Whether to show the costs for the timesheet (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift, and total_oncosts_breakdown and total_cost_with_oncosts to the timesheet. On-costs are calculated from the organisation’s on-cost configurations. For UK organisations, earnings-threshold rules (e.g. employer National Insurance) are applied using the full pay-period earnings - see the note above for details (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the timesheet (defaults to false) +approved_only: true (optional, boolean) - If true, only approved timesheets will be returned. Default is false. +200 +OK +(application/json) +Attributes: +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +Timesheet [/api/v2/timesheets/{id}] +GETGet Timesheet +If you are using show_costs=true or show_award_interpretation=true, the cost scope is required. +Parameters +id: 7007 (number) - The id of the timesheet +show_costs: true (optional, boolean) - Whether to show the costs for the timesheet (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift, and total_oncosts_breakdown and total_cost_with_oncosts to the timesheet. On-costs are calculated from the organisation’s on-cost configurations. For UK organisations, earnings-threshold rules (e.g. employer National Insurance) are applied using the full pay-period earnings - see the note above for details (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the timesheet (defaults to false) +show_notes: true (optional, boolean) - Whether to show notes for the timesheet (defaults to false) +approved_only: true (optional, boolean) - If true, only approved timesheets will be returned. Default is false. +200 +OK +(application/json) +Attributes: +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +PUTUpdate Timesheet +Use this endpoint to change a timesheet’s status. Three values are accepted: pending, approved, or exported. + +If a timesheet is exported it cannot be edited and its costs are locked. If you are exporting data out of timesheets through the API to be costed externally, you should mark those timesheets as exported at that time. + +Parameters +id: 7007 (number) - The id of the timesheet +Request Update Timesheet (application/json) +Body + + { + "status": "pending" + } + +200 +OK +(application/json) +Attributes: +id: 7007 (required) (number) - The id of the timesheet +user_id: 123456 (required) (number) - The id of the user who worked the shift +start: 2016-02-29 (required) (string) - The start date of the timesheet +finish: 2016-03-14 (required) (string) - The end date of the timesheet +status: approved (required) (string) - One of pending, approved, exported +shifts (required) (array of ShiftData) - The shifts on the timesheet +total_cost: 102.15 (number) - The total cost of the timesheet, if show_costs=true parameter is set +total_oncosts_breakdown (array) - Per-configuration on-cost amounts across all shifts in the timesheet (e.g. [{"name": "Superannuation", "amount": 10.22}, {"name": "Workcover", "amount": 2.04}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the employee are included, so the array can be empty. For UK organisations, earnings-threshold rules (such as employer National Insurance) are evaluated using the employee’s full pay-period cost normalised to a monthly equivalent and appear with an amount of 0.0 when the employee is below the threshold. +total_cost_with_oncosts: 114.41 (number) - The total timesheet cost including all on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +notes: (optional, array[NoteData]) - Notes on the timesheet +Shifts +Shifts represent actual times worked for a user and are contained in a Timesheet. For rostered times, please see Rosters or Schedules. + +A user can see all the shifts on each timesheet that they can see. Shifts can be created, updated and deleted by staff if they have the ability to see their own timesheets (a system setting). All shifts can be created, updated and deleted by roster managers and admins, and department managers can create, update and delete shifts for staff that they manage. +On-cost amounts are estimates +On-cost amounts are estimates. They are calculated by applying the organisation’s on-cost configurations to award-interpreted wage costs, consistent with Workforce’s cost reports. They are not statutory payroll calculations. For example, employer National Insurance is estimated as the configured percentage of all eligible cost rather than HMRC’s banded calculation on earnings above the threshold, and superannuation rules that depend on age or contribution caps are not applied. Use payroll data for exact figures. + +Some on-cost configurations only apply once an employee’s earnings pass a threshold (such as employer National Insurance). Eligibility can only be assessed against the employee’s full pay period, so these configurations are not included in oncosts_breakdown or cost_with_oncosts on the shifts endpoint for UK organisations. Use the timesheets endpoint for threshold-aware on-costs. + +On-cost configurations restricted to ordinary hours return 0.0 for leave shifts. Configurations that are not restricted still apply to leave shifts. + +Shifts [/api/v2/shifts] +GETGet Shifts +Get multiple shifts by IDs, user IDs, or from/to dates. You need to provide at least one of these parameters (ids, user_ids, or from + to). If you provide more than one, or any of the other parameters below, only shifts that meet all criteria will be returned. + +This method requires the timesheet scope (see Scopes for more information). +If you are using show_costs=true or show_award_interpretation=true, the cost scope is also required. + +Parameters +ids: 1,2,1337 (optional, string) - Comma separated list of shift IDs +user_ids: 1,2,123456 (optional, string) - Comma separated list of user IDs +from: 2016-03-02 (optional, string) - From date to lookup shifts in +to: 2016-03-02 (optional, string) - To date to lookup shifts in +show_costs: true (optional, boolean) - Whether to show the costs for the shift (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift. On-costs are calculated from the organisation’s on-cost configurations (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the shifts (defaults to false) +show_activities: false (optional, boolean) - If true, the activities within each shift will be included in the response, each with its activity_code/activity_code_id and prorated worked hours. With show_costs=true (and the cost scope) each activity also includes its derived cost. With include_names=true, each activity also includes the activity code’s name. Defaults to false. +include_names: false (optional, boolean) - If true, the team name, location name, and user name (not just IDs) will be included in the response. Defaults to false. +show_associated_tag: true (optional, boolean) - Whether to show the associated tag for the shifts (defaults to false) + +show_notes: true (optional, boolean) - Whether to show notes for the shifts (defaults to false) +report_location_id: 111 (optional, number) - The ID of a location for which to filter shifts to. If provided, only returns shifts if the shifts’s user’s default team (report_department_id) is set, and it is a team within this location. Note that it is possible for a shift’s user’s default location to be different to the location at which a shift was worked. This parameter always filters based on the user. For example if a user can work at locations A and B, they default to A, but work at B, a call with report_location_id=A will return that user’s shifts worked at location B. +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +page: 1 (optional, number) - The page number for your shifts list +page_size: 50 (optional, number) - The number of shifts retrieved per page. The maximum is 100 shifts per request. +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified shifts +200 +OK +(application/json) +Attributes (array): +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +POSTCreate Shift +Create a shift for a user, with specified times, on a specified date. + +This will create a timesheet for the user for the given pay period, if one doesn’t exist already. The shift object that is returned will have both an id (to use for Shift lookups) and a timesheet_id (for Timesheet lookups). + +When a shift is created, the times provided for the start, finish, break_start, and break_finish keys will be rounded to the nearest minute. To avoid potentially confusing, you can round them before passing them to the API. + +The optional activities array creates ShiftActivityData activities within the shift. Each entry has a start, finish, and optional activity_code_id (which must belong to the same organisation). See Update Shift for how activities are reconciled on update. +The optional top-level activity_code_id sets the shift’s own (fallback) activity code — applied to worked time not covered by any activity. The code must belong to the organisation, be unarchived, and apply to the shift’s team. On update, pass null to clear it. +This method requires the timesheet scope (see Scopes for more information). +Request Create Shift (application/json) +Body + + { + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "finish": 1457762640, + "department_id": 808, + "metadata": "My special metadata", + "activities": [ + { + "start": 1457743200, + "finish": 1457746800, + "activity_code_id": 7 + } + ] + } + +200 +OK +(application/json) +Body + + { + "id": 9665, + "timesheet_id": 7007, + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "break_start": null, + "break_finish": null, + "break_length": null, + "finish": 1457762640, + "status": "PENDING", + "allowances": [], + "tag": null, + "tag_id": null, + "department_id": 808, + "department_export_name": "abc123", + "metadata": "My special metadata", + "leave_request_id": null + } + +Schema + +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_finish - break_start) / 60, see Shifts section) +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +department_id: 808 (number) - The ID for the team this shift was worked in +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: (number) +Request Create Shift with breaks (application/json) +Body + + { + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "finish": 1457762640, + "break_start": 1457751660, + "break_finish": 1457753280, + "metadata": "My special metadata" + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_finish - break_start) / 60, see Shifts section) +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +department_id: 808 (number) - The ID for the team this shift was worked in +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: 9665 (number) +timesheet_id: 7007 (number) +user_id: 123456 (number) +date: 2016-03-12 +start: 1457741040 (number) +break_start: 1457751660 (number) +break_finish: 1457753280 (number) +break_length: 27 (number) +finish: 1457762640 (number) +status: PENDING +allowances: (array) +metadata: My special metadata +Request Create approved Shift (application/json) +Body + + { + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "finish": 1457762640, + "status": "APPROVED", + "tag": null + } + +200 +OK +(application/json) +Body + + { + "id": 9665, + "timesheet_id": 7007, + "user_id": 123456, + "start": 1457741040, + "break_start": null, + "break_finish": null, + "break_length": null, + "finish": 1457762640, + "status": "APPROVED", + "allowances": [], + "tag": null, + "tag_id": null, + "metadata": null, + "leave_request_id": null + } + +Schema + +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_finish - break_start) / 60, see Shifts section) +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +department_id: 808 (number) - The ID for the team this shift was worked in +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: (number) +Request Create Shift with Allowances (application/json) +Body + + { + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "finish": 1457762640, + "allowances": [ + { + "id": 987654, + "value": 1 + } + ] + } + +200 +OK +(application/json) +Body + + { + "id": 9665, + "timesheet_id": 7007, + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "break_start": null, + "break_finish": null, + "break_length": null, + "finish": 1457762640, + "status": "PENDING", + "allowances": [ + { + "id": 987654, + "value": 1 + } + ], + "tag": null, + "tag_id": null, + "metadata": null, + "leave_request_id": null + } + +Schema + +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_finish - break_start) / 60, see Shifts section) +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +department_id: 808 (number) - The ID for the team this shift was worked in +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: (number) +Shift [/api/v2/shifts/{id}] +GETGet Shift +Get a shift by id. + +This method requires the timesheet scope (see Scopes for more information). +If you are using show_costs=true or show_award_interpretation=true, the cost scope is also required. + +Parameters +id: 1337 (number) - The id of the shift +show_costs: true (optional, boolean) - Whether to show the costs for the shift (defaults to false) +include_oncosts: true (optional, boolean) - Whether to include on-cost additions (e.g. employer pension, levy) alongside existing costs. Requires show_costs=true. Adds oncosts_breakdown and cost_with_oncosts to each shift. On-costs are calculated from the organisation’s on-cost configurations (defaults to false) +show_award_interpretation: true (optional, boolean) - Whether to show award interpretation for the shift (defaults to false) +show_activities: false (optional, boolean) - If true, the activities within the shift will be included in the response, each with its activity_code/activity_code_id and prorated worked hours. With show_costs=true (and the cost scope) each activity also includes its derived cost. With include_names=true, each activity also includes the activity code’s name. Defaults to false. +include_names: false (optional, boolean) - If true, the team name, location name, and user name (not just IDs) will be included in the response. Defaults to false. +show_associated_tag: true (optional, boolean) - Whether to show the associated tag for the shifts (defaults to false) + +show_notes: true (optional, boolean) - Whether to show notes for the shift (defaults to false) +show_export_name: true (optional, boolean) - Whether to show the department’s export code that the employee worked on the shift (defaults to false) +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +PUTUpdate Shift +When a shift is updated, the times provided for the start and finish keys will be rounded to the nearest minute. To avoid potentially confusing, you can round them before passing them to the API. + +After the parent timesheet to which a shift is attached has been exported, you can no longer update the shift. +Passing activities replaces the shift’s activities with the supplied set: entries are matched to existing activities by their time window (start and finish). A matched entry updates that activity’s activity_code_id in place (the activity is kept, not recreated), unmatched existing activities are removed, and entries with a new window are created. Pass [] to clear all activities. Omit the key to leave activities unchanged. +The top-level activity_code_id sets the shift’s own (fallback) activity code, applied to worked time not covered by any activity. Pass null to clear it. +This method requires the timesheet scope (see Scopes for more information). +Parameters +id: 1337 (number) - The id of the shift +Request Approve Shift (application/json) +Body + + { + "status": "APPROVED" + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +status: APPROVED +Request Update Shift times (application/json) +Body + + { + "start": 1456901518, + "finish": 1456916886 + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +start: 1456901518 (number) +finish: 1456916886 (number) +Request Update Shift times on new date (application/json) +Body + + { + "date": "2016-03-03", + "start": 1456987918, + "finish": 1457003286, + "break_start": 1456994543, + "break_finish": 1456996212 + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +date: 2016-03-03 +start: 1456987918 (number) +break_start: 1456994543 (number) +break_finish: 1456996212 (number) +finish: 1457003286 (number) +Request Update Shift allowances (application/json) +Body + + { + "allowances": [ + { + "id": 987654, + "value": 1 + } + ] + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +allowances (array) +(object) +id: 987654 (number) +value: 1 (number) +(object) +id: 456789 (number) +value: 0.5 (number) +cost: 1.75 (number) +Request Update Shift activities (application/json) +Body + + { + "activity_code_id": 7, + "activities": [ + { + "start": 1456905600, + "finish": 1456909200, + "activity_code_id": 12 + } + ] + } + +200 +OK +(application/json) +Attributes: +id: 1337 (required) (number) - The id of the shift +timesheet_id: 7007 (required) (number) - The id of the timesheet that the shift belongs on +user_id: 123456 (required) (number) - The id of the user who worked the shift +date: 2016-03-02 (required) (string) - The date the shift was worked +start: 1456902118 (number) - The start of the shift +break_start: 1456908143 (number) - The start of the break +break_finish: 1456909812 (number) - The end of the break +break_length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +breaks: (optional, array[ShiftBreakData]) - The shift’s breaks +finish: 1456916286 (number) - The end of the shift +status: PENDING (required) (string) - One of PENDING, APPROVED +allowances (required) (array of AllowanceData) - The allowances that apply to the shift +tag: Head Technician (string) - The name of the tag on the shift (can use tag_id instead) +tag_id: 65421 (number) - The id of the tag on the shift (can use tag instead) +cost: 20.1 (number) - The cost of the shift, if show_costs=true parameter is set +oncosts_breakdown (array) - Per-configuration on-cost amounts for this shift (e.g. [{"name": "Superannuation", "amount": 2.01}, {"name": "Workcover", "amount": 0.40}]). Each entry has a name (the on-cost configuration’s name as entered in the organisation’s settings) and an amount. Names can change when a configuration is edited and are not unique, and configurations sharing a name appear as separate entries. Amounts are cost estimates, not statutory payroll calculations. Present if show_costs=true and include_oncosts=true and the organisation has on-cost configurations. Only configurations that cover the shift’s employee and location are included, so the array can be empty. Configurations restricted to ordinary hours return 0.0 for leave shifts. On the shifts endpoint, UK earnings-threshold on-costs (such as employer National Insurance) are not included. The timesheets endpoint assesses these against the whole pay period. +cost_with_oncosts: 22.51 (number) - The cost of the shift including on-costs, if show_costs=true and include_oncosts=true parameters are set +award_interpretation (array of AwardInterpretationData) - Award interpretation output, if show_award_interpretation=true parameter is set +department_id: 808 (number) - The ID for the team this shift was worked in +department_name: Front of House (string) - The name of the team this shift was worked in, included only if include_names=true +location_name: Brisbane CBD (string) - The name of the location this shift was worked at, included only if include_names=true +user_name: Jane Smith (string) - The name of the user who worked the shift, included only if include_names=true +metadata: My special metadata (string) - Custom string field that holds up to 500 characters +leave_request_id: 333 (number) - The ID for the leave request, if leave was taken, otherwise null. +record_id: 532432 (number) - The record ID, for use with Platform endpoints +approved_by: 123455 (number) - The ID of the user that approved the shift +approved_at: 1456988518 (string) - The time the shift was approved +notes: (optional, array[NoteData]) - Notes on the shift +activity_code_id: 7 (number) - The shift’s own (fallback) activity code id, applied to worked time not covered by any activity, or null. Included (with the activity_code string) when show_activities=true; the name is included under include_names +activities (array of ShiftActivityData) - The activities within the shift, if show_activities=true parameter is set +DELETEDelete Shift +This method requires the timesheet scope (see Scopes for more information). +Shifts can’t be deleted if their timesheet has already been exported. +Parameters +id: 1337 (number) - The id of the shift +200 +OK +(application/json) +Active Status [/api/v2/shifts/active] +GETGet Active Shifts +Gets a list of shifts that are taking place now. For each shift, returns the user ID, the shift ID, and the shift’s current status, which could be either clocked_in or on_break. Shifts are returned if they have a start time and no finish time. + +This method requires the timesheet scopes (see Scopes for more information). +200 +OK +(application/json) +Body [ {“user_id”: 123456, “shift_id”: 1337, “status”: “clocked_in”}, {“user_id”: 123457, “shift_id”: 1338, “status”: “on_break”} ] +Shift’s Applicable Allowances [/api/v2/shifts/{id}/applicable_allowances] +GETGet Shift’s Applicable Allowances +Our user guide has more information on working with allowances through the API. +Get list of manually applied allowances that can apply to a shift, along with the current amount currently applied to the given shift. + +The allowance IDs returned will be the same for all shifts within the organisation, but the values may differ from shift to shift. + +Allowances that are configured to apply automatically will not be returned by this endpoint. They will update automatically every time a shift is saved. + +This method requires the timesheet scope (see Scopes for more information). +Parameters +id: 1337 (number) - The id of the shift +200 +OK +(application/json) +Attributes: +id: 456789 (required) (number) - The id of the allowance +name: Hazardous Environment Allowance (required, string) - The name of the allowance +export_name: HE Allowance (string) - The export name of the allowance +current_value: 0.5 (number) - How much of this allowance is currently applied to the shift +Shift Versions [/api/v2/shifts/{id}/versions] +GETGet Shift Versions +This method requires the timesheet scope (see Scopes for more information). +Parameters +id: 1337 (number) - The id of the shift +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified shift versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Shift” (string) +changes (array) +(object) +field: finish +previous: 1456916286 (number) +updated: 1456916300 (number) +(object) +field: status +previous: PENDING +updated: APPROVED +Shift Breaks [/api/v2/shifts/{shift_id}/breaks] +Shift breaks represent a break in a Shift. A shift can have many breaks. + +Shift breaks inherit access permissions from their parent shift. +All shift break actions require the timesheet scope (see Scopes for more information). + +GETGet Shift’s Breaks +Parameters +shift_id: 1337 (number) - The id of the shift +200 +OK +(application/json) +Attributes (array): +id: 42 (required) (number) - The id of the break +shift_id: 1337 (required) (number) - The id of the break’s shift +start: 1456908143 (number) - The start of the break +finish: 1456909812 (number) - The end of the break +length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +paid: false (boolean) - Whether the break is a paid break +selected_automatic_break_id: 73647 (number) - The id of the automatic break rule chosen to correspond to this shift break by the employee when they clocked in +POSTCreate Shift Break +Parameters +shift_id: 1337 (number) - The id of the shift +Request Create with times (application/json) +Body + + { + "start": 1456908143, + "finish": 1456909812, + } + +201 +Created +(application/json) +Attributes: +id: 42 (required) (number) - The id of the break +shift_id: 1337 (required) (number) - The id of the break’s shift +start: 1456908143 (number) - The start of the break +finish: 1456909812 (number) - The end of the break +length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +paid: false (boolean) - Whether the break is a paid break +selected_automatic_break_id: 73647 (number) - The id of the automatic break rule chosen to correspond to this shift break by the employee when they clocked in +Request Create with length (application/json) +Body + + { + "length": 45, + } + +201 +Created +(application/json) +Body + + { + "id": 42, + "shift_id": 1337, + "start": null, + "finish": null, + "length": 45, + "paid": false, + } + +Request Create with paid (application/json) +Body + + { + "paid": true, + } + +201 +Created +(application/json) +Body + + { + "id": 42, + "shift_id": 1337, + "start": null, + "finish": null, + "length": 45, + "paid": true, + } + +Shift Break [/api/v2/shifts/{shift_id}/breaks/{break_id}] +Shift breaks represent a break in a Shift. A shift can have many breaks. + +Shift breaks inherit access permissions from their parent shift. +All shift break actions require the timesheet scope (see Scopes for more information). + +GETGet Shift Break +Parameters +shift_id: 1337 (number) - The id of the shift +break_id: 42 (number) - The id of the break +200 +OK +(application/json) +Attributes: +id: 42 (required) (number) - The id of the break +shift_id: 1337 (required) (number) - The id of the break’s shift +start: 1456908143 (number) - The start of the break +finish: 1456909812 (number) - The end of the break +length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +paid: false (boolean) - Whether the break is a paid break +selected_automatic_break_id: 73647 (number) - The id of the automatic break rule chosen to correspond to this shift break by the employee when they clocked in +PUTUpdate Shift Break +When a break is updated, the times provided for the start and finish keys will be rounded to the nearest minute. To avoid potentially confusing, you can round them before passing them to the API. + +Parameters +shift_id: 1337 (number) - The id of the shift +break_id: 42 (number) - The id of the break +Request update shift break (application/json) +Body + + { + "start": 1456908143 + } + +200 +OK +(application/json) +Attributes: +id: 42 (required) (number) - The id of the break +shift_id: 1337 (required) (number) - The id of the break’s shift +start: 1456908143 (number) - The start of the break +finish: 1456909812 (number) - The end of the break +length: 27 (number) - The length of the break in minutes (NOTE: this can be different to (break_start - break_finish) / 60, see Shifts section) +paid: false (boolean) - Whether the break is a paid break +selected_automatic_break_id: 73647 (number) - The id of the automatic break rule chosen to correspond to this shift break by the employee when they clocked in +Shift Break Versions [/api/v2/shifts/{shift_id}/breaks/{break_id}/versions] +GETGet Shift Break Versions +This method requires the timesheet scope (see Scopes for more information). +Parameters +id: 1337 (number) - The id of the shift +break_id: 42 (number) - The id of the break +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified shift break versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “ShiftBreak” (string) +changes (array) +(object) +field: break_finish +previous: 1456916286 (number) +updated: 1456916300 (number) +Shift Limits [/api/v2/shifts/limits?user_ids=123456,987654] +GETGet Shift Limits +This method requires the timesheet scope (see Scopes for more information). +Gets the first and last shifts (based on start date/time) out of all shifts owned by the given users. + +Parameters +user_ids: 123456,987654 (string) - Comma separated list of user IDs +include_names: false (optional, boolean) - If true, the team name, location name, and user name (not just IDs) will be included in the response. Defaults to false. +Response +Body + + [ + { + "id": 1337, + "timesheet_id": 7007, + "user_id": 123456, + "date": "2016-03-02", + "start": 1456902118, + "break_start": 1456908143, + "break_finish": 1456909812, + "break_length": 27, + "breaks": [ + { + "id": 42, + "shift_id": 1337, + "start": 1456908143, + "finish": 1456909812, + "length": 27 + } + ], + "finish": 1456916286, + "status": "PENDING", + "allowances": [ + { + "id": 456789, + "name": "Hazardous Environment Allowance", + "value": 0.5, + "cost": 1.75 + } + ], + "tag": "Head Technician", + "tag_id": 65421, + "cost": 20.1, + "award_interpretation": [ + { + "units": 6.45, + "date": "2016-02-29", + "name": "Full Time Weekday Ordinary Hours", + "export_name": "ORD 1x", + "secondary_export_name": "PEN10", + "ordinary_hours": true, + "cost": 16.125, + "base_rate_name": "09.232403.06 Base Hourly Level 1 20+ Years", + "base_rate_export_name": "Base Hourly Level 1 20+ Years", + "base_rate": 23.23, + "from": 1456902000, + "to": 1456916400 + } + ], + "department_id": 808, + "department_export_name": "abc123", + "employee_id": "111" + "metadata": "My special metadata", + "leave_request_id": 333 + }, + { + "id": 9956, + "timesheet_id": 14090, + "user_id": 123456, + "date": "2019-01-17", + "start": 1547685971, + "break_start": 1547695971, + "break_finish": 1547697971, + "break_length": 27, + "breaks": [ + { + "id": 42, + "shift_id": 1337, + "start": 1547695971, + "finish": 1547697971, + "length": 27 + } + ], + "finish": 1547697971, + "status": "PENDING", + "allowances": [ + { + "id": 456789, + "name": "Hazardous Environment Allowance", + "value": 0.5, + "cost": 1.75 + } + ], + "tag": "Head Technician", + "tag_id": 65421, + "cost": 20.1, + "award_interpretation": [ + { + "units": 6.45, + "date": "2019-01-17", + "name": "Full Time Weekday Ordinary Hours", + "export_name": "ORD 1x", + "ordinary_hours": true, + "cost": 16.125, + "base_rate_name": "09.232403.06 Base Hourly Level 1 20+ Years", + "base_rate_export_name": "Base Hourly Level 1 20+ Years", + "base_rate": "23.23", + "from": 1456902000, + "to": 1456916400 + } + ], + "department_id": 808, + "department_export_name": "abc123" + "employee_id": "111" + "metadata": "My special metadata", + "leave_request_id": 7889, + } + ] + +Staff (Users) +There are no special system permissions to see information about the current user /users/me. However, to CRUD users in the organisation, the user will need to be a payroll officer, department manager, roster manager, or admin. +User List [/api/v2/users] +GETGet Users +Gets information about all visible users. + +This method requires the user scope (see Scopes for more information). +If you are using show_wages=true, the cost scope is also required. + +If you want to see financial data (including tax declarations, super fund memberships, and bank details), you’ll need the financial scope. Tax file numbers are only included when using the financial scope. + +If your account contains more than 50 Qualifications, per-user qualification data will not be returned by default. Include the show_qualifications=true option to include it in the response. + +Parameters +name: Homer Simpson (optional, string) - The user’s preferred name. Note, you cannot pass both name and email params. +legal_first_name: Homer (optional, string) - The user’s first name. +legal_middle_names: James (optional, string) - The user’s middle name/s. +legal_last_name: Simpson (optional, string) - The user’s last name. +email: homer.simpson@springfieldpowerplant.com (optional, string) - The user’s email. +show_wages: true (optional, boolean) - Whether to show the user’s wage (defaults to false) +show_qualifications: true (optional, boolean) - Should the response include qualifications for each user +show_regular_hours: true (optional, boolean) - Should the response include regular hours for each user +show_inactive: false (optional, boolean) - Include inactive staff in your response +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified users +location_id: 111 (optional, number) - The ID of a location for which to filter users to. If provided, only returns user’s that belong to a department assigned to that location. +report_location_id: 111 (optional, number) - The ID of a location for which to filter users to. If provided, only returns users if the user’s default team (report_department_id) is set, and it is a department, within this location. +role_types: employee (optional, number) - The role type for which to filter users by. This can be a list of role types, separated by a comma (ie. "manager,employee"). +platform_role_ids: 111 (optional, string) - The ID of a role for which to filter users by. This can be a list of ids, separated by a comma (ie. "111,222,333"). +employee_id: “CC123” (optional, string) - Employee ID to filter by. +normalised_phone: “%2B61408123654” (optional, string) - Normalised phone number to filter by. +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +page: 1 (optional, number) - The page number for your user list +page_size: 50 (optional, number) - The number of users retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 123456 (required) (number) - The id of the user +name: Lenny Leonard (required, string) - The user’s preferred name +legal_first_name: Lenny (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Leonard (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: LL1 (string) - The user’s employee id, used for external systems +passcode: 0456 (required) (string) - The user’s system passcode (used for clocking in and out) +platform_role_ids: 18, 19, 22 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award (object) - Information about the user’s award, null if user isn’t on award +name: Fast Food Industry (required, string) - The name of the award +identifier: MA000003 (string) - The award’s identifier (will be the award code for all modern awards) +note: This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. (string) - This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. +award_template_id: 990 (number) - The ID of the award template the user is on +award_template_organisation_id: 5624432 (number) - The internal ID of the award template the user is on +award_tag_ids: 3816 (required) (array[number]) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids: 222 (array[number]) - The department ids that this user manages +active: true (required) (boolean) - Whether the user is active in the system +email: lenny.leonard@springfieldpowerplant.com (string) - The user’s email +payslip_email: lenny.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +salary: 1234.12 (number) - The weekly salary of the user, if show_wages=true parameter is set +hourly_rate: 32.47 (number) - The hourly rate of the user, if show_wages=true parameter is set, will not show if the salary exists +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +expected_hours_in_pay_period: 38 (number) - readonly: for salaried staff, defines expected number of hours worked per pay period +days_overtime_averaged_over: 7 (number) - date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - date from which overtime averaging periods should commence +super_fund (object) - information about the employees super fund +id: 1234 (number) - the unique ID of the employees fund +fund_name: FUND NAME (string) - the name of the fund +product_name: FUND PRODUCT (string) - the name of the fund product +smsf: true, false (boolean) - is the fund a self managed fund +abn: 123456789 (number) - the number of the fund +super_fund_membership (object) - information about the employees super fund membership +super_contribution_type: arpa (string) - the type of the employees super contribution +member_number: 123ACB (string) - the membership number of the employees super fund +occupational_rating: A1 (string) - the occupational rating of the employees super fund +super_fund_id: 1234 (required, number) - the record ID of the employees super fund +employer_preferred: 0 (required, number) - indication of an employers preferred super fund +employer_default: 1 (required, number) - indication of an employers default super fund +employer_generic: 2 (required, number) - indication of an employers generic super fund +bank_details (BankDetails) - the bank details of the employee +address (Address) - the residential address of the employee +tax_declaration (object) - the tax declaration information of the employee +previous_family_name: Smith (string) - the previous family name of the employee +australian_tax_resident: true (boolean) - is the user an Australia resident for tax purposes +australian_tax_residency_status: resident (string) - the normalised Australian tax residency status +tax_free_threshold: true (boolean) - is the employee under the tax free threshold? +senior_tax_offset: false (boolean) - can the employee claim a senior tax offset? +zone_overseas_carer: false (boolean) - can the employee claim a zone or overseas tax offset? +student_loan: true (boolean) - does the employee have a student loan? +financial_supplement_debt: true (boolean) - does the employee have a financial supplement debt? +tax_code: 1150L (string) - the employee’s tax code for tax services +uk_tax_year_status: first_job (string) - For UK residents, the stage of the employee’s career +uk_national_insurance_code: A (string) - For UK residents, the National Insurance category letter (e.g., A, B, C, D, E, F, H, I, J, K, L, M, N, S, V, X, Z) +national_insurance_number: AB123456C (string) - For UK organisations, the employee’s National Insurance number. Returns when the financial scope is in use, to the employee themselves or to a user who can read their sensitive data (such as an admin or payroll officer). This replaces tax_file_number for UK orgs, which is now legacy. +employment_basis: full_time (string) - the employment type the employee has specified on their TFN declaration +tax_scale_type: regular (string) - the tax scale type used for calculations +income_type: salary_and_wages (string) - the type of income for tax purposes +employment_type: employee (string) - the type of employment +senior_marital_status: single (string) - marital status for senior tax offset purposes +home_country: France (string) - the employee’s home country (required for working holiday makers) +nz_tax_code: M (string) - For NZ residents, the tax code +nz_esct_rate: 17.5 (string) - For NZ residents, the ESCT rate +nz_source_of_income: primary_income (string) - For NZ residents, the source of income +nz_varied_tax_rate: 25.0 (number) - For NZ residents, the varied tax rate (required for WT, STC, STC_SL tax codes) +onboarding_status: not_applicable, pending, invited, in_progress, completed, invitation_failed (string) - the status of the employee onboarding progress +kiwi_saver (object) - NZ only. The employee’s KiwiSaver settings. Only present for NZ employees who have KiwiSaver configured +status: active_member, automatically_enrolled, opted_in, not_eligible, opted_out, employed_less_than_28_days, not_required_to_be_automatically_enrolled (string) - the employee’s KiwiSaver enrolment status +ird_status_code: AK (string) - the IRD status code derived from the status (e.g. AK, AE, OK, NK); null when no code applies +employee_deduction_rate: 3.0 (number) - the employee’s contribution rate as a percentage of gross pay +employer_deduction_rate: 3.5 (number) - the employer’s contribution rate as a percentage of gross pay; may be null +savings_suspension: false (boolean) - whether the employee is currently on a savings suspension +opted_out_date: 2024-01-15 (string) - the date the employee opted out, if applicable; null otherwise +qualifications (array of UserQualification) - Information about qualifications the user holds, if Qualifications are enabled +regular_hours (RegularHours) - information about the employees regular hours of work +created_at: 1430715548 (required) (number) - When the user’s Tanda profile was created +record_id: 532432 (number) - the record ID, for use with Platform endpoints +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +position_title: Bartender (string) - Read only +position_template: Bartender (Hourly) (string) +ssn: 123456789 (string) - Read only +emergency_contacts (array of EmergencyContact) - Read only +POSTCreate User +Only the name field is required to create a user. + +To create a manager, you should provide the IDs of Teams they manage in the managed_department_ids field. + +The Workforce API currently requires email addresses to be unique within an organisation when creating a user. However, there is no guarantee that this restriction will exist in the future. If you are creating users via the API you should first GET a list of users and validate that the user you’re creating doesn’t already exist - do not depend on the API to validate this for you. +To skip email validation when creating a user, use skip_validation=true. + +This method requires the user scope (see Scopes for more information). +If you plan to assign wage information you will need the cost scope. This includes the employee_id field. + +To set financial information (tax declarations, super fund memberships, bank details), you will need the financial scope. + +Tax Declaration Requirements: When creating a user with tax declaration information, certain fields are required depending on the organisation’s country: +Australia (default): Must provide either australian_tax_resident or australian_tax_residency_status, plus tax_free_threshold, senior_tax_offset, zone_overseas_carer, student_loan, and financial_supplement_debt +United Kingdom: Must provide tax_code, student_loan, and uk_tax_year_status +Singapore: Must provide tax_file_number (NRIC/FIN format, e.g., S1234567A) and complete singapore_tax_detail with all required fields: legal_status, nationality, race, religion, marital_status. Additionally, issue_date is required for permanent residents and foreign worker passes, while expiry_date is required for foreign worker passes +New Zealand: Must provide tax_file_number (IRD number), nz_tax_code, and nz_esct_rate. Optionally provide nz_source_of_income and nz_varied_tax_rate (required when nz_tax_code is WT, STC, or STC_SL) +Working Holiday Makers: Must provide home_country when income_type or tax_scale_type is set to "working_holiday_maker" +Bank Details Requirements: When creating or updating a user with bank details, required fields vary by organisation country: + +Australia (default): Must provide account_name, account_number, and bsb +IBAN Countries: Must provide account_name, bank_name, and iban +Singapore: Must provide account_name, account_number, and bank_name +Parameters +skip_validation: true (optional, boolean) - Skip’s checking if an employee already exists with this email address (defaults to false) +Request Create User (application/json) +Body + + { + "name": "Carlton Carlson Jr.", + "employee_id": "CC123", + "passcode": "6789", + "phone": "+61408123654", + "date_of_birth": "1956-09-25", + "employment_start_date": "1986-09-24", + "email": "carl.carlson@springfieldpowerplant.com", + "report_department_id": 111, + "preferred_hours": 22, + "award_template_organisation_id": 5324324, + "award_tag_ids": [ + 3816 + ], + "department_ids": [ + 111 + ], + "managed_department_ids": [ + 111 + ], + "platform_role_ids": [ + 3816, + 3817 + ], + "yearly_salary": 40000, + "enable_login": true + } + +Schema + +Attributes: +name: Homer Simpson (required, string) - The user’s preferred name +legal_first_name: Homer (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Simpson (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: HS98 (string) - The user’s employee id, used for external systems +passcode: 1230 (string) - The user’s system passcode (used for clocking in and out) +department_ids: [111] (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (number) - The preferred number of hours to be rostered each week +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. Please use contracted_weekly_hours or regular hours of work instead. Previously used for part time staff, can be used to configure when overtime starts to apply. +award_template_id: 990 (number) - The ID of the award template thte user is on +award_template_organisation_id: 12345 (number) - The ID of the award template organisation +award_tag_ids (array of number) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids (array of number) - The department ids that this user manages +email: homer.simpson@springfieldpowerplant.com (string) - The user’s email +phone: 0404 123 123 (string) - Phone number for the user +days_overtime_averaged_over: 7 (number) - readonly: date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - readonly: date from which overtime averaging periods should commence +can_see_costs (boolean) - can the user see the costs data +`One of +user_levels` (array of UserLevel) +platform_role_ids: [18] (array[number]) - IDs of the user’s roles (see Access & Roles) +hourly_rate: 24.80 (number, optional) - The users hourly rate +yearly_salary: 87450.0 (number, optional) - The users annual salary +position_template_name: ‘Bartender (Hourly)’ (string) - The name of the position template to assign to the user’s current Pay Conditions. +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +address (Address) +tax_declaration (TaxDeclaration) - the tax declaration information of the employee +bank_details (BankDetails) - the bank details of the employee +super_fund_membership (SuperFundMembership) - information about the employees super fund membership +regular_hours (RegularHours) - information about the employees regular hours of work +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +200 +OK +(application/json) +Body + + { + "id": 12345678, + "name": "Carlton Carlson Jr.", + "date_of_birth": "1956-09-25", + "employment_start_date": "1986-09-24", + "employment_end_date": null, + "employee_id": "CC123", + "passcode": "6789", + "platform_role_ids": [ + 3816, + 3817 + ], + "department_ids": [ + 111 + ], + "preferred_hours": 22, + "award_template_organisation_id": 5324324, + "award_tag_ids": [ + 3816 + ], + "report_department_id": 111, + "managed_department_ids": [ + 111 + ], + "active": true, + "payroll_cessation_code": "voluntary_cessation", + "enable_login": true, + "can_see_costs": true, + "email": "carl.carlson@springfieldpowerplant.com", + "photo": null, + "phone": "+61408123654", + "normalised_phone": "+61408123654", + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1459476813, + "contracted_weekly_hours": 0.0, + "part_time_fixed_hours": 0.0, + "expected_hours_in_pay_period": null, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-11-14", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "temporary_employee_type": null, + "tax_declaration": { + "previous_family_name": "Smith", + "australian_tax_resident": true, + "australian_tax_residency_status": "resident", + "tax_free_threshold": true, + "senior_tax_offset": false, + "zone_overseas_carer": false, + "student_loan": false, + "financial_supplement_debt": false, + "tax_code": "123456789", + "uk_tax_year_status": null, + "employment_basis": "full_time", + "tax_scale_type": "regular", + "income_type": "salary_and_wages", + "employment_type": "employee", + "senior_marital_status": null, + "home_country": null, + "singapore_tax_detail": { + "legal_status": "singapore_citizen", + "nationality": "singaporean", + "race": "chinese", + "religion": "buddhism", + "marital_status": "single", + "issue_date": "2020-01-15", + "expiry_date": "2025-01-15" + } + }, + "onboarding_status": "not_applicable", + "qualifications": [], + "regular_hours": { + "start_date": "2021-01-01", + "schedules": [ + { + "week": 1, + "day": "Monday", + "start": "09:00", + "end": "17:00", + "department_id": 1234, + "breaks": "12:00-13:00" + } + ] + }, + "updated_at": 1562272900 + } + +Schema + +Attributes: +id: 1234567 (required) (number) - The id of the user +name: Homer Simpson (required, string) - The user’s preferred name +legal_first_name: Homer (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Simpson (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employment_end_date: 1995-07-12 (string) - The users employment end date +employee_id: HS98 (string) - The user’s employee id, used for external systems +passcode: 1230 (required) (string) - The user’s system passcode (used for clocking in and out) +user_levels (array of UserLevel) +platform_role_ids: 3816, 3817 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award_template_id: 990 (number) - The ID of the award template thte user is on +award_template_organisation_id: 12087 (number) - The ID of the organisation +award_tag_ids (required) (array of number) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids (array of number) - The department ids that this user manages +active: false (required) (boolean) - Whether the user is active in the system +enable_login: true (boolean) - Can the user login +can_see_costs: false (boolean) - Can the user see costing data +email: homer.simpson@springfieldpowerplant.com (string) - The user’s email +photo: http://vignette2.wikia.nocookie.net/simpsons/images/b/bd/Homer_Simpson.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +created_at: 1430715549 (required) (number) - When the user’s Tanda profile was created +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +Request Minimum fields required (application/json) +Body + + { + "name": "Carlton Carlson Jr." + } + +200 +OK +(application/json) +Body + + { + "id": 12345678, + "name": "Carlton Carlson Jr.", + "date_of_birth": null, + "employment_start_date": null, + "employment_end_date": null, + "employee_id": null, + "passcode": null, + "platform_role_ids": [ + 3816 + ], + "department_ids": [], + "preferred_hours": 22, + "award_template_id": null, + "award_tag_ids": [], + "report_department_id": null, + "managed_department_ids": [], + "active": true, + "payroll_cessation_code": null, + "enable_login": true, + "can_see_costs": true, + "email": null, + "photo": null, + "phone": null, + "normalised_phone": null, + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1459476813, + "contracted_weekly_hours": 0.0, + "part_time_fixed_hours": 0.0, + "expected_hours_in_pay_period": null, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-11-14", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "temporary_employee_type": null, + "tax_declaration": null, + "onboarding_status": "not_applicable", + "qualifications": [], + "updated_at": 1562272900 + } + +Schema + +Attributes: +id: 1234567 (required) (number) - The id of the user +name: Homer Simpson (required, string) - The user’s preferred name +legal_first_name: Homer (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Simpson (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employment_end_date: 1995-07-12 (string) - The users employment end date +employee_id: HS98 (string) - The user’s employee id, used for external systems +passcode: 1230 (required) (string) - The user’s system passcode (used for clocking in and out) +user_levels (array of UserLevel) +platform_role_ids: 3816, 3817 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award_template_id: 990 (number) - The ID of the award template thte user is on +award_template_organisation_id: 12087 (number) - The ID of the organisation +award_tag_ids (required) (array of number) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids (array of number) - The department ids that this user manages +active: false (required) (boolean) - Whether the user is active in the system +enable_login: true (boolean) - Can the user login +can_see_costs: false (boolean) - Can the user see costing data +email: homer.simpson@springfieldpowerplant.com (string) - The user’s email +photo: http://vignette2.wikia.nocookie.net/simpsons/images/b/bd/Homer_Simpson.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +created_at: 1430715549 (required) (number) - When the user’s Tanda profile was created +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +Request Create Singapore User with Tax Details (application/json) +This example is only applicable for Singapore organisations. The singapore_tax_detail field will not be available for organisations in other countries. + +Body + + { + "name": "Li Wei", + "employee_id": "LW001", + "email": "li.wei@company.com.sg", + "phone": "+6591234567", + "employment_start_date": "2023-01-01", + "user_levels": ["employee"], + "department_ids": [111], + "tax_declaration": { + "tax_file_number": "S1234567A", + "employment_basis": "full_time", + "singapore_tax_detail": { + "legal_status": "singapore_citizen", + "nationality": "singaporean", + "race": "chinese", + "religion": "buddhism", + "marital_status": "married" + } + } + } + +Request Create New Zealand User with Tax Details (application/json) + +This example is only applicable for New Zealand organisations. The NZ tax detail fields will not be available for organisations in other countries. +Body + + { + "name": "Kane Williamson", + "employee_id": "KW001", + "email": "kane.williamson@company.co.nz", + "phone": "+6421234567", + "employment_start_date": "2023-01-01", + "user_levels": ["employee"], + "department_ids": [111], + "tax_declaration": { + "tax_file_number": "49098576", + "employment_basis": "full_time", + "nz_tax_code": "ME", + "nz_esct_rate": "17.5", + "nz_source_of_income": "primary_income" + } + } + +Request Create UK User with Tax Details (application/json) + +This example is only applicable for UK organisations. The uk_national_insurance_code and uk_tax_year_status fields will not be available for organisations in other countries. +Body + + { + "name": "John Smith", + "employee_id": "JS001", + "email": "john.smith@company.co.uk", + "phone": "+447700900123", + "employment_start_date": "2023-01-01", + "user_levels": ["employee"], + "department_ids": [111], + "tax_declaration": { + "tax_file_number": "AB123456C", + "tax_code": "1150L", + "uk_tax_year_status": "first_job", + "uk_national_insurance_code": "A", + "student_loan": false + } + } + +Request Create Singapore User with Bank Details (application/json) + +This example shows bank details creation for Singapore organisations +Body + + { + "name": "Tan Wei Ming", + "bank_details": { + "bank_name": "DBS Bank Limited", + "account_name": "Tan Wei Ming", + "account_number": "0010572841" + } + } + +Response 200 (application/json) + +Body + + { + "id": 12345679, + "name": "Li Wei", + "employee_id": "LW001", + "email": "li.wei@company.com.sg", + "phone": "+6591234567", + "employment_start_date": "2023-01-01", + "active": true, + "enable_login": true, + "tax_declaration": { + "tax_file_number": "S1234567A", + "employment_basis": "full_time", + "singapore_tax_detail": { + "legal_status": "singapore_citizen", + "nationality": "singaporean", + "race": "chinese", + "religion": "buddhism", + "marital_status": "married", + "issue_date": null, + "expiry_date": null + } + }, + "created_at": 1672531200, + "updated_at": 1672531200 + } + +The regular_hours object supports optional sleepovers, on_calls, and rostered_days_off arrays alongside schedules. These follow the same format as schedules but without breaks. See Sleepovers, On Calls, and Rostered Days Off for standalone CRUD endpoints. +Request Create Regular Hours (application/json) +Body + + { + "name": "Steve Barnett", + "regular_hours": { + "start_date": "2021-01-01", + "schedules": [ + { + "week": 1, + "day": "Monday", + "start": "09:00", + "end": "17:00", + "department_id": 1234, + "breaks": "12:00-13:00" + }, + { + "week": 2, + "day": "Tuesday", + "start": "10:00", + "end": "20:00", + "department_id": 4321, + "breaks": "12:00-13:00" + } + ], + "sleepovers": [ + { + "week": 1, + "day": "Monday", + "start": "22:00", + "end": "06:00", + "department_id": 1234 + } + ], + "on_calls": [ + { + "week": 1, + "day": "Tuesday", + "start": "18:00", + "end": "06:00", + "department_id": 1234 + } + ], + "rostered_days_off": [ + { + "week": 1, + "day": "Wednesday", + "start": "00:00", + "end": "00:00" + } + ] + } + } + +200 +OK +(application/json) +Body + + { + "id": 12345678, + "name": "Steve Barnett", + "date_of_birth": null, + "employment_start_date": null, + "employment_end_date": null, + "employee_id": null, + "passcode": null, + "platform_role_ids": [ + 3816 + ], + "department_ids": [], + "preferred_hours": 22, + "award_template_id": null, + "award_tag_ids": [], + "report_department_id": null, + "managed_department_ids": [], + "active": true, + "payroll_cessation_code": null, + "enable_login": true, + "can_see_costs": true, + "email": null, + "photo": null, + "phone": null, + "normalised_phone": null, + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1459476813, + "contracted_weekly_hours": 0.0, + "part_time_fixed_hours": 0.0, + "expected_hours_in_pay_period": null, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-11-14", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "temporary_employee_type": null, + "tax_declaration": null, + "onboarding_status": "not_applicable", + "qualifications": [], + "regular_hours": { + "start_date": "2021-01-01", + "schedules": [ + { + "week": 1, + "day": "Monday", + "start": "09:00", + "end": "17:00", + "department_id": 1234, + "breaks": "12:00-13:00" + }, + { + "week": 2, + "day": "Tuesday", + "start": "10:00", + "end": "20:00", + "department_id": 4321, + "breaks": "12:00-13:00" + } + ], + "sleepovers": [ + { + "week": 1, + "day": "Monday", + "start": "22:00", + "end": "06:00", + "department_id": 1234 + } + ], + "on_calls": [ + { + "week": 1, + "day": "Tuesday", + "start": "18:00", + "end": "06:00", + "department_id": 1234 + } + ], + "rostered_days_off": [ + { + "week": 1, + "day": "Wednesday", + "start": "00:00", + "end": "00:00" + } + ] + }, + "updated_at": 1562272900 + } + +Inactive User List [/api/v2/users/inactive] +GETGet Inactive Users +This method requires the user scope (see Scopes for more information). +Parameters +name: Homer Simpson (optional, string) - The user’s preferred name. Note, you cannot pass both name and email params. +legal_first_name: Homer (optional, string) - The user’s first name. +legal_middle_names: James (optional, string) - The user’s middle name/s. +legal_last_name: Simpson (optional, string) - The user’s last name. +email: homer.simpson@springfieldpowerplant.com (optional, string) - The user’s email. +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified users +report_location_id: 111 (optional, number) - The ID of a location for which to filter users to. If provided, only returns users if the user’s default team (report_department_id) is set, and it is a team within this location. +page: 1 (optional, number) - The page number for your user list +page_size: 50 (optional, number) - The number of users retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Body + + [ + { + "id": 1234567, + "name": "Homer Simpson", + "legal_first_name": "Homer", + "legal_middle_names": "James", + "legal_last_name": "Simpson", + "date_of_birth": null, + "employment_start_date": null, + "employment_end_date": "2019-06-01", + "employee_id": "HS98", + "passcode": "1230", + "department_ids": [ + 111 + ], + "preferred_hours": 22, + "award_template_id": 58, + "award_tag_ids": [], + "report_department_id": 111, + "managed_department_ids": [], + "active": false, + "payroll_cessation_code": "voluntary_cessation", + "enable_login": true, + "can_see_costs": true, + "email": "homer.simpson@springfieldpowerplant.com", + "photo": "http://vignette2.wikia.nocookie.net/simpsons/images/b/bd/Homer_Simpson.png", + "phone": "+61404123456", + "normalised_phone": "+61404123456", + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1430715549, + "contracted_weekly_hours": 0.0, + "part_time_fixed_hours": 0.0, + "expected_hours_in_pay_period": null, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-11-14", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "temporary_employee_type": null, + "tax_declaration": null, + "onboarding_status": "not_applicable", + "qualifications": [], + "updated_at": 1562272900 + } + ] + +User [/api/v2/users/{id}] +GETGet User +Gets information about a user. + +This method requires the user scope (see Scopes for more information). +If you are using show_wages=true, the cost scope is also required. + +Parameters +id: 123456 (number) - The id of the user +show_wages: true (optional, boolean) - Whether to show the user’s wage (defaults to false) +show_regular_hours: true (optional, boolean) - Whether to show the user’s regular hours (defaults to false) +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false) +200 +OK +(application/json) +Attributes: +id: 123456 (required) (number) - The id of the user +name: Lenny Leonard (required, string) - The user’s preferred name +legal_first_name: Lenny (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Leonard (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: LL1 (string) - The user’s employee id, used for external systems +passcode: 0456 (required) (string) - The user’s system passcode (used for clocking in and out) +platform_role_ids: 18, 19, 22 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award (object) - Information about the user’s award, null if user isn’t on award +name: Fast Food Industry (required, string) - The name of the award +identifier: MA000003 (string) - The award’s identifier (will be the award code for all modern awards) +note: This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. (string) - This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. +award_template_id: 990 (number) - The ID of the award template the user is on +award_template_organisation_id: 5624432 (number) - The internal ID of the award template the user is on +award_tag_ids: 3816 (required) (array[number]) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids: 222 (array[number]) - The department ids that this user manages +active: true (required) (boolean) - Whether the user is active in the system +email: lenny.leonard@springfieldpowerplant.com (string) - The user’s email +payslip_email: lenny.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +salary: 1234.12 (number) - The weekly salary of the user, if show_wages=true parameter is set +hourly_rate: 32.47 (number) - The hourly rate of the user, if show_wages=true parameter is set, will not show if the salary exists +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +expected_hours_in_pay_period: 38 (number) - readonly: for salaried staff, defines expected number of hours worked per pay period +days_overtime_averaged_over: 7 (number) - date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - date from which overtime averaging periods should commence +super_fund (object) - information about the employees super fund +id: 1234 (number) - the unique ID of the employees fund +fund_name: FUND NAME (string) - the name of the fund +product_name: FUND PRODUCT (string) - the name of the fund product +smsf: true, false (boolean) - is the fund a self managed fund +abn: 123456789 (number) - the number of the fund +super_fund_membership (object) - information about the employees super fund membership +super_contribution_type: arpa (string) - the type of the employees super contribution +member_number: 123ACB (string) - the membership number of the employees super fund +occupational_rating: A1 (string) - the occupational rating of the employees super fund +super_fund_id: 1234 (required, number) - the record ID of the employees super fund +employer_preferred: 0 (required, number) - indication of an employers preferred super fund +employer_default: 1 (required, number) - indication of an employers default super fund +employer_generic: 2 (required, number) - indication of an employers generic super fund +bank_details (BankDetails) - the bank details of the employee +address (Address) - the residential address of the employee +tax_declaration (object) - the tax declaration information of the employee +previous_family_name: Smith (string) - the previous family name of the employee +australian_tax_resident: true (boolean) - is the user an Australia resident for tax purposes +australian_tax_residency_status: resident (string) - the normalised Australian tax residency status +tax_free_threshold: true (boolean) - is the employee under the tax free threshold? +senior_tax_offset: false (boolean) - can the employee claim a senior tax offset? +zone_overseas_carer: false (boolean) - can the employee claim a zone or overseas tax offset? +student_loan: true (boolean) - does the employee have a student loan? +financial_supplement_debt: true (boolean) - does the employee have a financial supplement debt? +tax_code: 1150L (string) - the employee’s tax code for tax services +uk_tax_year_status: first_job (string) - For UK residents, the stage of the employee’s career +uk_national_insurance_code: A (string) - For UK residents, the National Insurance category letter (e.g., A, B, C, D, E, F, H, I, J, K, L, M, N, S, V, X, Z) +national_insurance_number: AB123456C (string) - For UK organisations, the employee’s National Insurance number. Returns when the financial scope is in use, to the employee themselves or to a user who can read their sensitive data (such as an admin or payroll officer). This replaces tax_file_number for UK orgs, which is now legacy. +employment_basis: full_time (string) - the employment type the employee has specified on their TFN declaration +tax_scale_type: regular (string) - the tax scale type used for calculations +income_type: salary_and_wages (string) - the type of income for tax purposes +employment_type: employee (string) - the type of employment +senior_marital_status: single (string) - marital status for senior tax offset purposes +home_country: France (string) - the employee’s home country (required for working holiday makers) +nz_tax_code: M (string) - For NZ residents, the tax code +nz_esct_rate: 17.5 (string) - For NZ residents, the ESCT rate +nz_source_of_income: primary_income (string) - For NZ residents, the source of income +nz_varied_tax_rate: 25.0 (number) - For NZ residents, the varied tax rate (required for WT, STC, STC_SL tax codes) +onboarding_status: not_applicable, pending, invited, in_progress, completed, invitation_failed (string) - the status of the employee onboarding progress +kiwi_saver (object) - NZ only. The employee’s KiwiSaver settings. Only present for NZ employees who have KiwiSaver configured +status: active_member, automatically_enrolled, opted_in, not_eligible, opted_out, employed_less_than_28_days, not_required_to_be_automatically_enrolled (string) - the employee’s KiwiSaver enrolment status +ird_status_code: AK (string) - the IRD status code derived from the status (e.g. AK, AE, OK, NK); null when no code applies +employee_deduction_rate: 3.0 (number) - the employee’s contribution rate as a percentage of gross pay +employer_deduction_rate: 3.5 (number) - the employer’s contribution rate as a percentage of gross pay; may be null +savings_suspension: false (boolean) - whether the employee is currently on a savings suspension +opted_out_date: 2024-01-15 (string) - the date the employee opted out, if applicable; null otherwise +qualifications (array of UserQualification) - Information about qualifications the user holds, if Qualifications are enabled +regular_hours (RegularHours) - information about the employees regular hours of work +created_at: 1430715548 (required) (number) - When the user’s Tanda profile was created +record_id: 532432 (number) - the record ID, for use with Platform endpoints +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +position_title: Bartender (string) - Read only +position_template: Bartender (Hourly) (string) +ssn: 123456789 (string) - Read only +emergency_contacts (array of EmergencyContact) - Read only +PUTUpdate User +This method requires the user scope (see Scopes for more information). +If you plan to update wage information you will need the cost scope. This includes the employee_id field. + +To set financial information, you will need the financial scope. + +Notes + +To skip email validation when creating a user, use skip_validation=true. +The UUID for uploading a file_id to a qualification can be found at the create temporary file endpoint. +If you are attempting to remove a user’s department_ids or managed_department_ids, you should pass null as part of your API call. There is an example in the sidebar. +The API currently requires email addresses to be unique within an organisation. However, there is no guarantee that this restriction will exist in the future. If you are updating a user’s email via the API you should first GET a list of users and validate that the email you’re setting isn’t already in use - do not depend on the API to validate this for you. +It’s not possible to remove a user’s passcode. If you pass null as a value for it, the user’s passcode will be regenerated. +Updating a Tax File Number requires the financial scope. Valid values for a Tax File Number are 8 or 9 numeric characters, or an exemption: exempt_underage exempt_applied exempt_allowance exempt_blank (TFN intentionally withheld). Only the current user can view their TFN via the API; that is, you can POST/PUT a TFN but cannot read it except for your own user record. If you POST/PUT an invalid TFN you’ll get a validation error. +Parameters +id: 123456 (number) - The id of the user +skip_validation: true (optional, boolean) - Skip’s checking if an employee already exists with this email address +Request Update User (application/json) +Body + +{ + "name": "Lenford Leonard", + "legal_first_name": "Lenford", + "legal_middle_names": "Leonard", + "legal_last_name": "Leonard", + "employee_id": "Lenford", + "passcode": "0123", + "phone": "0404123122", + "date_of_birth": "1990-12-12", + "employment_start_date": "2016-03-01", + "email": "some_email@test.com", + "hourly_rate": 12.34, + "enable_login": true, + "qualifications": [ + { + "qualification_id": 2345, + "enabled": true, + "training_provider_name": "Acme Training Ltd", + "state_of_completion": "QLD", + "license_number": "BCD234", + "expires": "2012-11-21", + "in_training": false, + "file_id": "2d4ab757-637c-4be2-b2d6-5f89ae211ba4" + } + ] +} + +Schema + +Attributes: +name: Homer Simpson (required, string) - The user’s preferred name +legal_first_name: Homer (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Simpson (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: HS98 (string) - The user’s employee id, used for external systems +passcode: 1230 (string) - The user’s system passcode (used for clocking in and out) +department_ids: [111] (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (number) - The preferred number of hours to be rostered each week +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. Please use contracted_weekly_hours or regular hours of work instead. Previously used for part time staff, can be used to configure when overtime starts to apply. +award_template_id: 990 (number) - The ID of the award template thte user is on +award_template_organisation_id: 12345 (number) - The ID of the award template organisation +award_tag_ids (array of number) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids (array of number) - The department ids that this user manages +email: homer.simpson@springfieldpowerplant.com (string) - The user’s email +payslip_email: homer.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +phone: 0404 123 123 (string) - Phone number for the user +days_overtime_averaged_over: 7 (number) - readonly: date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - readonly: date from which overtime averaging periods should commence +can_see_costs (boolean) - can the user see the costs data +`One of +user_levels` (array of UserLevel) +platform_role_ids: [18] (array[number]) - IDs of the user’s roles (see Access & Roles) +hourly_rate: 24.80 (number, optional) - The users hourly rate +yearly_salary: 87450.0 (number, optional) - The users annual salary +position_template_name: ‘Bartender (Hourly)’ (string) - The name of the position template to assign to the user’s current Pay Conditions. +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +address (Address) +tax_declaration (TaxDeclaration) - the tax declaration information of the employee +bank_details (BankDetails) - the bank details of the employee +super_fund_membership (SuperFundMembership) - information about the employees super fund membership +regular_hours (RegularHours) - information about the employees regular hours of work +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +employment_end_date: 1989-12-17 (string) - The user’s employment end date +active: true (boolean) +qualifications (UserQualification) +date_ranges: (object) +file_id: 123987 (string) +answered_questions (array of Answer) +minimum_base_hours: 15.0 (number) - Deprecated. Please use contracted_weekly_hours instead. +200 +OK +(application/json) +Attributes: +id: 123456 (required) (number) - The id of the user +name: Lenny Leonard (required, string) - The user’s preferred name +legal_first_name: Lenny (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Leonard (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: LL1 (string) - The user’s employee id, used for external systems +passcode: 0456 (required) (string) - The user’s system passcode (used for clocking in and out) +platform_role_ids: 18, 19, 22 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award (object) - Information about the user’s award, null if user isn’t on award +name: Fast Food Industry (required, string) - The name of the award +identifier: MA000003 (string) - The award’s identifier (will be the award code for all modern awards) +note: This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. (string) - This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. +award_template_id: 990 (number) - The ID of the award template the user is on +award_template_organisation_id: 5624432 (number) - The internal ID of the award template the user is on +award_tag_ids: 3816 (required) (array[number]) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids: 222 (array[number]) - The department ids that this user manages +active: true (required) (boolean) - Whether the user is active in the system +email: lenny.leonard@springfieldpowerplant.com (string) - The user’s email +payslip_email: lenny.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +salary: 1234.12 (number) - The weekly salary of the user, if show_wages=true parameter is set +hourly_rate: 32.47 (number) - The hourly rate of the user, if show_wages=true parameter is set, will not show if the salary exists +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +expected_hours_in_pay_period: 38 (number) - readonly: for salaried staff, defines expected number of hours worked per pay period +days_overtime_averaged_over: 7 (number) - date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - date from which overtime averaging periods should commence +super_fund (object) - information about the employees super fund +id: 1234 (number) - the unique ID of the employees fund +fund_name: FUND NAME (string) - the name of the fund +product_name: FUND PRODUCT (string) - the name of the fund product +smsf: true, false (boolean) - is the fund a self managed fund +abn: 123456789 (number) - the number of the fund +super_fund_membership (object) - information about the employees super fund membership +super_contribution_type: arpa (string) - the type of the employees super contribution +member_number: 123ACB (string) - the membership number of the employees super fund +occupational_rating: A1 (string) - the occupational rating of the employees super fund +super_fund_id: 1234 (required, number) - the record ID of the employees super fund +employer_preferred: 0 (required, number) - indication of an employers preferred super fund +employer_default: 1 (required, number) - indication of an employers default super fund +employer_generic: 2 (required, number) - indication of an employers generic super fund +bank_details (BankDetails) - the bank details of the employee +address (Address) - the residential address of the employee +tax_declaration (object) - the tax declaration information of the employee +previous_family_name: Smith (string) - the previous family name of the employee +australian_tax_resident: true (boolean) - is the user an Australia resident for tax purposes +australian_tax_residency_status: resident (string) - the normalised Australian tax residency status +tax_free_threshold: true (boolean) - is the employee under the tax free threshold? +senior_tax_offset: false (boolean) - can the employee claim a senior tax offset? +zone_overseas_carer: false (boolean) - can the employee claim a zone or overseas tax offset? +student_loan: true (boolean) - does the employee have a student loan? +financial_supplement_debt: true (boolean) - does the employee have a financial supplement debt? +tax_code: 1150L (string) - the employee’s tax code for tax services +uk_tax_year_status: first_job (string) - For UK residents, the stage of the employee’s career +uk_national_insurance_code: A (string) - For UK residents, the National Insurance category letter (e.g., A, B, C, D, E, F, H, I, J, K, L, M, N, S, V, X, Z) +national_insurance_number: AB123456C (string) - For UK organisations, the employee’s National Insurance number. Returns when the financial scope is in use, to the employee themselves or to a user who can read their sensitive data (such as an admin or payroll officer). This replaces tax_file_number for UK orgs, which is now legacy. +employment_basis: full_time (string) - the employment type the employee has specified on their TFN declaration +tax_scale_type: regular (string) - the tax scale type used for calculations +income_type: salary_and_wages (string) - the type of income for tax purposes +employment_type: employee (string) - the type of employment +senior_marital_status: single (string) - marital status for senior tax offset purposes +home_country: France (string) - the employee’s home country (required for working holiday makers) +nz_tax_code: M (string) - For NZ residents, the tax code +nz_esct_rate: 17.5 (string) - For NZ residents, the ESCT rate +nz_source_of_income: primary_income (string) - For NZ residents, the source of income +nz_varied_tax_rate: 25.0 (number) - For NZ residents, the varied tax rate (required for WT, STC, STC_SL tax codes) +onboarding_status: not_applicable, pending, invited, in_progress, completed, invitation_failed (string) - the status of the employee onboarding progress +kiwi_saver (object) - NZ only. The employee’s KiwiSaver settings. Only present for NZ employees who have KiwiSaver configured +status: active_member, automatically_enrolled, opted_in, not_eligible, opted_out, employed_less_than_28_days, not_required_to_be_automatically_enrolled (string) - the employee’s KiwiSaver enrolment status +ird_status_code: AK (string) - the IRD status code derived from the status (e.g. AK, AE, OK, NK); null when no code applies +employee_deduction_rate: 3.0 (number) - the employee’s contribution rate as a percentage of gross pay +employer_deduction_rate: 3.5 (number) - the employer’s contribution rate as a percentage of gross pay; may be null +savings_suspension: false (boolean) - whether the employee is currently on a savings suspension +opted_out_date: 2024-01-15 (string) - the date the employee opted out, if applicable; null otherwise +qualifications (array of UserQualification) - Information about qualifications the user holds, if Qualifications are enabled +regular_hours (RegularHours) - information about the employees regular hours of work +created_at: 1430715548 (required) (number) - When the user’s Tanda profile was created +record_id: 532432 (number) - the record ID, for use with Platform endpoints +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +position_title: Bartender (string) - Read only +position_template: Bartender (Hourly) (string) +ssn: 123456789 (string) - Read only +emergency_contacts (array of EmergencyContact) - Read only +name: Lenford Leonard +employee_id: Lenford +passcode: 0123 +phone: 0404123122 +normalised_phone: +61404123122 +date_of_birth: 1990-12-12 +employment_start_date: 2016-03-01 +email: some_email@test.com +hourly_rate: 12.34 (number) +enable_login: true (boolean) +qualifications: [{}] +Request Deactivate User (application/json) +Body + + { + "active": false + } + +200 +OK +(application/json) +Attributes: +id: 123456 (required) (number) - The id of the user +name: Lenny Leonard (required, string) - The user’s preferred name +legal_first_name: Lenny (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Leonard (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: LL1 (string) - The user’s employee id, used for external systems +passcode: 0456 (required) (string) - The user’s system passcode (used for clocking in and out) +platform_role_ids: 18, 19, 22 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award (object) - Information about the user’s award, null if user isn’t on award +name: Fast Food Industry (required, string) - The name of the award +identifier: MA000003 (string) - The award’s identifier (will be the award code for all modern awards) +note: This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. (string) - This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. +award_template_id: 990 (number) - The ID of the award template the user is on +award_template_organisation_id: 5624432 (number) - The internal ID of the award template the user is on +award_tag_ids: 3816 (required) (array[number]) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids: 222 (array[number]) - The department ids that this user manages +active: true (required) (boolean) - Whether the user is active in the system +email: lenny.leonard@springfieldpowerplant.com (string) - The user’s email +payslip_email: lenny.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +salary: 1234.12 (number) - The weekly salary of the user, if show_wages=true parameter is set +hourly_rate: 32.47 (number) - The hourly rate of the user, if show_wages=true parameter is set, will not show if the salary exists +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +expected_hours_in_pay_period: 38 (number) - readonly: for salaried staff, defines expected number of hours worked per pay period +days_overtime_averaged_over: 7 (number) - date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - date from which overtime averaging periods should commence +super_fund (object) - information about the employees super fund +id: 1234 (number) - the unique ID of the employees fund +fund_name: FUND NAME (string) - the name of the fund +product_name: FUND PRODUCT (string) - the name of the fund product +smsf: true, false (boolean) - is the fund a self managed fund +abn: 123456789 (number) - the number of the fund +super_fund_membership (object) - information about the employees super fund membership +super_contribution_type: arpa (string) - the type of the employees super contribution +member_number: 123ACB (string) - the membership number of the employees super fund +occupational_rating: A1 (string) - the occupational rating of the employees super fund +super_fund_id: 1234 (required, number) - the record ID of the employees super fund +employer_preferred: 0 (required, number) - indication of an employers preferred super fund +employer_default: 1 (required, number) - indication of an employers default super fund +employer_generic: 2 (required, number) - indication of an employers generic super fund +bank_details (BankDetails) - the bank details of the employee +address (Address) - the residential address of the employee +tax_declaration (object) - the tax declaration information of the employee +previous_family_name: Smith (string) - the previous family name of the employee +australian_tax_resident: true (boolean) - is the user an Australia resident for tax purposes +australian_tax_residency_status: resident (string) - the normalised Australian tax residency status +tax_free_threshold: true (boolean) - is the employee under the tax free threshold? +senior_tax_offset: false (boolean) - can the employee claim a senior tax offset? +zone_overseas_carer: false (boolean) - can the employee claim a zone or overseas tax offset? +student_loan: true (boolean) - does the employee have a student loan? +financial_supplement_debt: true (boolean) - does the employee have a financial supplement debt? +tax_code: 1150L (string) - the employee’s tax code for tax services +uk_tax_year_status: first_job (string) - For UK residents, the stage of the employee’s career +uk_national_insurance_code: A (string) - For UK residents, the National Insurance category letter (e.g., A, B, C, D, E, F, H, I, J, K, L, M, N, S, V, X, Z) +national_insurance_number: AB123456C (string) - For UK organisations, the employee’s National Insurance number. Returns when the financial scope is in use, to the employee themselves or to a user who can read their sensitive data (such as an admin or payroll officer). This replaces tax_file_number for UK orgs, which is now legacy. +employment_basis: full_time (string) - the employment type the employee has specified on their TFN declaration +tax_scale_type: regular (string) - the tax scale type used for calculations +income_type: salary_and_wages (string) - the type of income for tax purposes +employment_type: employee (string) - the type of employment +senior_marital_status: single (string) - marital status for senior tax offset purposes +home_country: France (string) - the employee’s home country (required for working holiday makers) +nz_tax_code: M (string) - For NZ residents, the tax code +nz_esct_rate: 17.5 (string) - For NZ residents, the ESCT rate +nz_source_of_income: primary_income (string) - For NZ residents, the source of income +nz_varied_tax_rate: 25.0 (number) - For NZ residents, the varied tax rate (required for WT, STC, STC_SL tax codes) +onboarding_status: not_applicable, pending, invited, in_progress, completed, invitation_failed (string) - the status of the employee onboarding progress +kiwi_saver (object) - NZ only. The employee’s KiwiSaver settings. Only present for NZ employees who have KiwiSaver configured +status: active_member, automatically_enrolled, opted_in, not_eligible, opted_out, employed_less_than_28_days, not_required_to_be_automatically_enrolled (string) - the employee’s KiwiSaver enrolment status +ird_status_code: AK (string) - the IRD status code derived from the status (e.g. AK, AE, OK, NK); null when no code applies +employee_deduction_rate: 3.0 (number) - the employee’s contribution rate as a percentage of gross pay +employer_deduction_rate: 3.5 (number) - the employer’s contribution rate as a percentage of gross pay; may be null +savings_suspension: false (boolean) - whether the employee is currently on a savings suspension +opted_out_date: 2024-01-15 (string) - the date the employee opted out, if applicable; null otherwise +qualifications (array of UserQualification) - Information about qualifications the user holds, if Qualifications are enabled +regular_hours (RegularHours) - information about the employees regular hours of work +created_at: 1430715548 (required) (number) - When the user’s Tanda profile was created +record_id: 532432 (number) - the record ID, for use with Platform endpoints +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +position_title: Bartender (string) - Read only +position_template: Bartender (Hourly) (string) +ssn: 123456789 (string) - Read only +emergency_contacts (array of EmergencyContact) - Read only +active: false (boolean) +passcode: 01234568975461 +Request Remove Departments (application/json) +Body + + { + "department_ids": null + } + +200 +OK +(application/json) +Body + +{ + "id": 123456, + "name": "Lenny Leonard", + "date_of_birth": "1955-05-12", + "employment_start_date": "1989-12-17", + "employment_end_date": "2019-06-01", + "employee_id": "LL1", + "passcode": "0456", + "department_ids": [], + "preferred_hours": 20, + "award_template_id": 990, + "award_tag_ids": [ + 3816 + ], + "report_department_id": 111, + "managed_department_ids": [ + 222 + ], + "active": true, + "payroll_cessation_code": null, + "enable_login": true, + "can_see_costs": true, + "email": "lenny.leonard@springfieldpowerplant.com", + "photo": "http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png", + "phone": "0404 123 123", + "normalised_phone": "+61404123123", + "salary": 1234.12, + "hourly_rate": 32.47, + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1430715548 + "contracted_weekly_hours": 20.0, + "part_time_fixed_hours": 20, + "expected_hours_in_pay_period": 38, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-09-12", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "tax_declaration": null, + "qualifications": [], + "updated_at": 1562272900 +} + +Request Update Singapore Tax Details (application/json) +This example is only applicable for Singapore organisations. The singapore_tax_detail field will not be available for organisations in other countries. +Body + + { + "tax_declaration": { + "singapore_tax_detail": { + "legal_status": "permanent_resident", + "nationality": "malaysian", + "race": "chinese", + "religion": "christianity", + "marital_status": "married", + "issue_date": "2022-06-01" + } + } + } + +Request Update New Zealand Tax Details (application/json) + +This example is only applicable for New Zealand organisations. The NZ tax detail fields will not be available for organisations in other countries. +Body + + { + "tax_declaration": { + "nz_tax_code": "ME_SL", + "nz_esct_rate": "30", + "nz_source_of_income": "secondary_income" + } + } + +Request Update UK National Insurance Code (application/json) + +This example is only applicable for UK organisations. The uk_national_insurance_code field will not be available for organisations in other countries. +Body + + { + "tax_declaration": { + "uk_national_insurance_code": "A" + } + } + +Request Update Singapore Bank Details (application/json) + +This example is only applicable for Singapore organisations. The bsb field is not required for Singapore bank details. +Body + + { + "bank_details": { + "bank_name": "OCBC Bank", + "account_name": "Tan Wei Ming", + "account_number": "1234567890" + } + } + +Response 200 (application/json) + +Body + + { + "id": 123456, + "name": "Li Wei", + "tax_declaration": { + "tax_file_number": "S1234567A", + "employment_basis": "full_time", + "singapore_tax_detail": { + "legal_status": "permanent_resident", + "nationality": "malaysian", + "race": "chinese", + "religion": "christianity", + "marital_status": "married", + "issue_date": "2022-06-01", + "expiry_date": null + } + }, + "updated_at": 1672617600 + } + +The regular_hours object supports optional sleepovers, on_calls, and rostered_days_off arrays alongside schedules. These follow the same format as schedules but without breaks. See Sleepovers, On Calls, and Rostered Days Off for standalone CRUD endpoints. +Request Update Regular Hours (application/json) +Body + + { + "regular_hours": { + "start_date": "2021-01-01", + "schedules": [ + { + "week": 1, + "day": "Monday", + "start": "09:00", + "end": "17:00", + "department_id": 1234, + "breaks": "12:00-13:00" + } + ], + "sleepovers": [ + { + "week": 1, + "day": "Monday", + "start": "22:00", + "end": "06:00", + "department_id": 1234 + } + ], + "on_calls": [ + { + "week": 1, + "day": "Tuesday", + "start": "18:00", + "end": "06:00", + "department_id": 1234 + } + ], + "rostered_days_off": [ + { + "week": 1, + "day": "Wednesday", + "start": "00:00", + "end": "00:00" + } + ] + } + } + +200 +OK +(application/json) +Body + + { + "id": 123456, + "name": "Lenny Leonard", + "date_of_birth": "1955-05-12", + "employment_start_date": "1989-12-17", + "employment_end_date": "2019-06-01", + "employee_id": "LL1", + "passcode": "0456", + "department_ids": [], + "preferred_hours": 20, + "award_template_id": 990, + "award_tag_ids": [ + 3816 + ], + "report_department_id": 111, + "managed_department_ids": [ + 222 + ], + "active": true, + "payroll_cessation_code": null, + "enable_login": true, + "can_see_costs": true, + "email": "lenny.leonard@springfieldpowerplant.com", + "photo": "http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png", + "phone": "0404 123 123", + "normalised_phone": "+61404123123", + "salary": 1234.12, + "hourly_rate": 32.47, + "time_zone": "Australia/Brisbane", + "utc_offset": 36000, + "created_at": 1430715548 + "contracted_weekly_hours": 20.0, + "part_time_fixed_hours": 20, + "expected_hours_in_pay_period": 38, + "days_overtime_averaged_over": 7, + "overtime_calculated_over_period_start": "2016-09-12", + "super_fund_membership": null, + "super_fund": null, + "bank_details": null, + "temporary_employee_type": null, + "tax_declaration": null, + "qualifications": [], + "regular_hours": { + "start_date": "2021-01-01", + "schedules": [ + { + "week": 1, + "day": "Monday", + "start": "09:00", + "end": "17:00", + "department_id": 1234, + "breaks": "12:00-13:00" + } + ], + "sleepovers": [ + { + "week": 1, + "day": "Monday", + "start": "22:00", + "end": "06:00", + "department_id": 1234 + } + ], + "on_calls": [ + { + "week": 1, + "day": "Tuesday", + "start": "18:00", + "end": "06:00", + "department_id": 1234 + } + ], + "rostered_days_off": [ + { + "week": 1, + "day": "Wednesday", + "start": "00:00", + "end": "00:00" + } + ] + }, + "updated_at": 1562272900 + } + +DELETEDelete User +This endpoint will just mark the user as inactive and return the user, this effectively does the same as the deactivate user request above. + +This method requires the user scope (see Scopes for more information). +Parameters +id: 654321 (number) - The id of the user +200 +OK +(application/json) +Attributes: +id: 123456 (required) (number) - The id of the user +name: Lenny Leonard (required, string) - The user’s preferred name +legal_first_name: Lenny (string) - The user’s first name +legal_middle_names: James (string) - The user’s middle name/s +legal_last_name: Leonard (string) - The user’s last name +date_of_birth: 1955-05-12 (string) - The user’s date of birth +employment_start_date: 1989-12-17 (string) - The user’s employment start date +employee_id: LL1 (string) - The user’s employee id, used for external systems +passcode: 0456 (required) (string) - The user’s system passcode (used for clocking in and out) +platform_role_ids: 18, 19, 22 (required, array[number]) - IDs of the user’s roles (see Access & Roles) +department_ids: 111 (required) (array[number]) - The department (team) ids that the user is a member of +preferred_hours: 20 (required) (number) - The preferred number of hours to be rostered each week +award (object) - Information about the user’s award, null if user isn’t on award +name: Fast Food Industry (required, string) - The name of the award +identifier: MA000003 (string) - The award’s identifier (will be the award code for all modern awards) +note: This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. (string) - This field is deprecated, and may be removed in the future. Please rely on the award_template_organisation_id field instead. +award_template_id: 990 (number) - The ID of the award template the user is on +award_template_organisation_id: 5624432 (number) - The internal ID of the award template the user is on +award_tag_ids: 3816 (required) (array[number]) - The award tag ids that apply to the user +report_department_id: 111 (number) - The user’s default team +managed_department_ids: 222 (array[number]) - The department ids that this user manages +active: true (required) (boolean) - Whether the user is active in the system +email: lenny.leonard@springfieldpowerplant.com (string) - The user’s email +payslip_email: lenny.personal@example.com (string) - The user’s payslip email address. Null if not explicitly set. +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - An avatar for the user +phone: 0404 123 123 (string) - Phone number for the user +normalised_phone: +61404123123 (string) - Phone number for user with international dialing code +salary: 1234.12 (number) - The weekly salary of the user, if show_wages=true parameter is set +hourly_rate: 32.47 (number) - The hourly rate of the user, if show_wages=true parameter is set, will not show if the salary exists +time_zone: Australia/Brisbane (string) - The user’s time zone +utc_offset: 36000 (required) (number) - The user’s time zone’s UTC offset +contracted_weekly_hours: The number of hours per week a user must work. Working under or over this number of hours may incur overtime or additional costs +part_time_fixed_hours: 20 (number) - Deprecated. This value will always be the same as contracted_weekly_hours. +expected_hours_in_pay_period: 38 (number) - readonly: for salaried staff, defines expected number of hours worked per pay period +days_overtime_averaged_over: 7 (number) - date range (weekly, fortnightly, or 4-weekly) over which overtime calculations should run +overtime_calculated_over_period_start: 2016-09-12 (string) - date from which overtime averaging periods should commence +super_fund (object) - information about the employees super fund +id: 1234 (number) - the unique ID of the employees fund +fund_name: FUND NAME (string) - the name of the fund +product_name: FUND PRODUCT (string) - the name of the fund product +smsf: true, false (boolean) - is the fund a self managed fund +abn: 123456789 (number) - the number of the fund +super_fund_membership (object) - information about the employees super fund membership +super_contribution_type: arpa (string) - the type of the employees super contribution +member_number: 123ACB (string) - the membership number of the employees super fund +occupational_rating: A1 (string) - the occupational rating of the employees super fund +super_fund_id: 1234 (required, number) - the record ID of the employees super fund +employer_preferred: 0 (required, number) - indication of an employers preferred super fund +employer_default: 1 (required, number) - indication of an employers default super fund +employer_generic: 2 (required, number) - indication of an employers generic super fund +bank_details (BankDetails) - the bank details of the employee +address (Address) - the residential address of the employee +tax_declaration (object) - the tax declaration information of the employee +previous_family_name: Smith (string) - the previous family name of the employee +australian_tax_resident: true (boolean) - is the user an Australia resident for tax purposes +australian_tax_residency_status: resident (string) - the normalised Australian tax residency status +tax_free_threshold: true (boolean) - is the employee under the tax free threshold? +senior_tax_offset: false (boolean) - can the employee claim a senior tax offset? +zone_overseas_carer: false (boolean) - can the employee claim a zone or overseas tax offset? +student_loan: true (boolean) - does the employee have a student loan? +financial_supplement_debt: true (boolean) - does the employee have a financial supplement debt? +tax_code: 1150L (string) - the employee’s tax code for tax services +uk_tax_year_status: first_job (string) - For UK residents, the stage of the employee’s career +uk_national_insurance_code: A (string) - For UK residents, the National Insurance category letter (e.g., A, B, C, D, E, F, H, I, J, K, L, M, N, S, V, X, Z) +national_insurance_number: AB123456C (string) - For UK organisations, the employee’s National Insurance number. Returns when the financial scope is in use, to the employee themselves or to a user who can read their sensitive data (such as an admin or payroll officer). This replaces tax_file_number for UK orgs, which is now legacy. +employment_basis: full_time (string) - the employment type the employee has specified on their TFN declaration +tax_scale_type: regular (string) - the tax scale type used for calculations +income_type: salary_and_wages (string) - the type of income for tax purposes +employment_type: employee (string) - the type of employment +senior_marital_status: single (string) - marital status for senior tax offset purposes +home_country: France (string) - the employee’s home country (required for working holiday makers) +nz_tax_code: M (string) - For NZ residents, the tax code +nz_esct_rate: 17.5 (string) - For NZ residents, the ESCT rate +nz_source_of_income: primary_income (string) - For NZ residents, the source of income +nz_varied_tax_rate: 25.0 (number) - For NZ residents, the varied tax rate (required for WT, STC, STC_SL tax codes) +onboarding_status: not_applicable, pending, invited, in_progress, completed, invitation_failed (string) - the status of the employee onboarding progress +kiwi_saver (object) - NZ only. The employee’s KiwiSaver settings. Only present for NZ employees who have KiwiSaver configured +status: active_member, automatically_enrolled, opted_in, not_eligible, opted_out, employed_less_than_28_days, not_required_to_be_automatically_enrolled (string) - the employee’s KiwiSaver enrolment status +ird_status_code: AK (string) - the IRD status code derived from the status (e.g. AK, AE, OK, NK); null when no code applies +employee_deduction_rate: 3.0 (number) - the employee’s contribution rate as a percentage of gross pay +employer_deduction_rate: 3.5 (number) - the employer’s contribution rate as a percentage of gross pay; may be null +savings_suspension: false (boolean) - whether the employee is currently on a savings suspension +opted_out_date: 2024-01-15 (string) - the date the employee opted out, if applicable; null otherwise +qualifications (array of UserQualification) - Information about qualifications the user holds, if Qualifications are enabled +regular_hours (RegularHours) - information about the employees regular hours of work +created_at: 1430715548 (required) (number) - When the user’s Tanda profile was created +record_id: 532432 (number) - the record ID, for use with Platform endpoints +next_pay_group_id: 53242 (number) - the ID for the pay group of the user’s next timesheet +last_synced_mobile_app: 1430715548 (number) - Read only: when the user last opened the mobile app. +automatic_break_rule_set_id: 1430715548 (number) - Read only: the ID for the user’s automatic break rule set. +temporary_employee_type: (TemporaryEmployeeType, optional) - Type of a temporary employee user. This field only exists if Enable Temporary Staff setting is turned on. +position_title: Bartender (string) - Read only +position_template: Bartender (Hourly) (string) +ssn: 123456789 (string) - Read only +emergency_contacts (array of EmergencyContact) - Read only +active: false (boolean) +passcode: 01234568975461 +Invite Users [/api/v2/users/{id}/invite] +POSTInvite User +This endpoint takes a user ID and sends that user an email invite to use Workforce. + +This method requires the user scope (see Scopes for more information). +It also requires the following prerequisites to work - User must exist - You must be able to manage the User - The User must have an email and it must be valid - The User must not have already signed up - The User must not already have a valid invitation + +Failure to meet these pre reqs will result in a returned 400, 403 or 404. Otherwise a 201 CREATED will be returned + +Parameters +id: 654321 (number) - The id of the user +201 +Created +(application/json) +Invite New Users for Onboarding [/api/v2/users/onboarding] +POSTInvite User for Onboarding +If you are using Workforce’s employee onboarding, you can use this endpoint to automatically invite new staff to be onboarded. It takes a name and email, and optionally a phone number and custom message, and sends the employee a link to a form where they can add their details. + +Request Create User for Onboarding (application/json) +Body + + { + "name": "Eddard Sheppard", + "email": "carl.carlson@springfieldpowerplant.com", + "phone": "+61408123654", + "custom_message": "Welcome to the company!" + } + +201 +Created +(application/json) +Invite Existing User for Onboarding [/api/v2/users/{id}/onboard] +POSTInvite Existing User for Onboarding +If you have created a new user with the Create User endpoint and are using Workforce’s employee onboarding, you can use this endpoint to trigger the onboarding process for the new user. + +This endpoint is intended only for brand new users created from the Create User endpoint. If the user has already been invited to Workforce you cannot use this endpoint. +Parameters +id: 123456 (number) - The id of the user +201 +Created +(application/json) +Clocked In Users [/api/v2/users/clocked_in] +GETGet Clocked In Users +This endpoint is deprecated. You should use /shifts/active instead. +Gets a list of staff who are currently at work. Essentially this looks for valid shifts that have a start time but no end time. The shift’s ID is also returned. + +This method requires the user and timesheet scopes (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified users +200 +OK +(application/json) +Body [{“user_id”: 123456, “shift_id”: 1337}] +User Versions [/api/v2/users/{id}/versions] +GETGet User Versions +This method requires the user scope (see Scopes for more information). +Parameters +id: 123456 (number) - The id of the user +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified user versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “User” (string) +changes (array) +(object) +field: name +previous: Lenny L Leonard +updated: Lenny Leonard +(object) +field: preferred_hours +previous: 28 (number) +updated: 20 (number) +Alternate Rates [/api/v2/users/{id}/alternate_rates] +Alternate Rates are user level assigned hourly rates based on team. + +Note: employment_contract_id is an optional param for all Alternate Rate endpoints. + +If no employment_contract_id is included, the endpoint will refrence the latest employment contract. + +These endpoint actions require Allow Additional Rates to be enabled under your organisation’s feature management settings. +GETGet Alternate Rates +This method requires the user scope (see Scopes for more information). +Parameters +id: 162456 (required, number) - The ID of an user +employment_contract_id: 162456 (optional, number) - The ID of an user’s employment contract +200 +OK +(application/json) +Attributes: +alternate_rates (array) +(object) +department_id: 123456 (string) +hourly_rate: 20.01 (string) +PUTUpdate Alternate Rates +To Create and update a user’s alternate rate(s) data, include the user’s id and alternate_rates array. + +Note: Alternate rates can only be assigned to teams that are scope to the user. Also only one alternate rate can be assigned per team and the rate must be different from the user’s base hourly rate. + +This method requires the user scope (see Scopes for more information). +Successfully using this endpoint will overwrite all existing alternate rates for the user. + +Parameters +id: 123456 (required, number) - The ID of an user +employment_contract_id: 162456 (optional, number) - The ID of an user’s employment contract +alternate_rates: (required, array) +object: {“department_id”: 987654, “hourly_rate”: 20.01} (required, object) department_id: (required, number) - The ID of a department/team the user works in hourly_rate: (required, number) - The hourly rate this user will be paid for working in this team +Request Alternate Rates (application/json) +Body + + { + "id": 123456, + "employment_contract_id": 987654, + "alternate_rates": [ + {"department_id": 987654, "hourly_rate": 20.01}, + {"department_id": 987653, "hourly_rate": 7.99}, + {"department_id": 987652, "hourly_rate": 8}, + ] + } + +201 +Created +(application/json) +Attributes(AlternateRatesData) +DELETEDelete Alternate Rates +This method requires the user scope (see Scopes for more information). +This method will delete ALL alternate rates for the user. + +Parameters +id: 162456 (required, number) - The ID of an user +employment_contract_id: 162456 (optional, number) - The ID of an user’s employment contract +200 +OK +(application/json) +Body + + { + "message": "Successfully deleted all alternate rates" + } + +Employee Pay Fields +There are no special system permissions required. To access most of these endpoints, you will need the cost scope for your api token. The user the token is generated for must also have custom permissions edit_wages on for the staff members they wish to edit pay fields for. +Employee Payfields [/api/v2/user_pay_fields] +GETGet Payfields For All Users +Retrieves all pay field objects belonging to the users of the organisation. This endpoint can be paginated. + +This method requires the cost scope (see Scopes for more information). +Parameters +page: 1 (optional, number) - The page number for your pay fields list +page_size: 50 (optional, number) - The number of pay field objects retrieved per page. The maximum is 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Get Payfields By Known ID [/api/v2/user_pay_fields/{id}] +GETGet Payfields By Payfield ID +Retrieves one pay field object by it’s unique id. + +This method requires the cost scope (see Scopes for more information). +Parameters +id: 54321 (number) - The id of the payfield +200 +OK +(application/json) +Attributes: +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Employee Payfields By User [/api/v2/user_pay_fields/user/{user_id}] +GETGet Payfields By User +This endpoint takes a user ID and returns an array of all the users pay field objects. + +This method requires the cost scope (see Scopes for more information). +It also requires the following prerequisites to work - User must exist + +Failure to meet these pre reqs will result in a returned 400, 403 or 404. Otherwise an array of UserPayFieldData will be returned + +Parameters +user_id: 654321 (number) - The id of the user +201 +Created +(application/json) +Attributes (array): +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Verify Employee Payfields By User [/api/v2/user_pay_fields/user/{user_id}/verify] +GETVerify Payfields By User +This endpoint conducts a check on a specific user’s pay field records. The check will ensure correct sequence of dates, and return any errors that might be evident. + +This method requires the cost scope (see Scopes for more information). +Parameters +user_id: 654321 (number) - The id of the user +200 +OK +(application/json) +Attributes (array): +id: 12345 (number) - The id of the payfield +from (object) +date: 2022-01-01 (string) +valid: true (boolean) +to (object) +date: 2023-01-01 (string) +valid: true (boolean) +errors: Something is wrong, Something else is wrong (array[string]) +Updating Pay Fields [/api/v2/user_pay_fields/{id}] +PUTUpdate Payfield +Update a specific param for a single payfield entry. + +This method requires a id to be given. +This method requires the cost scope (see Scopes for more information). + +You do not have to provide every pay field in the request body. +The ones you do provide will form the new value of the updated pay field object. + +The only limitation to updating behaviour is - you cannot update a user’s first pay field object’s from_date. + +You can identify which pay field object is the first by it’s from date. This should be represented as start. + +Parameters +id: 54321 (number) - The ID of payfield entry. +Request Update Payfield (application/json) +Body + + { + "id": 54321, + "user_id": 654321, + "from_date": "1970-1-1", + "to_date": "2020-1-1", + "hourly_rate": 0.0, + "award_tags": "Casual,Level 1", + "contracted_weekly_hours": 38, + "yearly_salary": 60000, + "award_template_organisation_id": 12345 + } + +201 +Created +(application/json) +Attributes: +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Creating Pay Fields [/api/v2/user_pay_fields] +POSTCreate Payfield +user_id and from_date are all that are required in the request body to create a new payfield entry. + +If no to_date is provided as a parameter, this payfield record will apply forever. +Request Create Payfield (application/json) +Body + + { + "user_id": 654321, + "from_date": "1970-1-1", + "to_date": "2020-1-1", + "hourly_rate": 0.0, + "award_tags": "Casual,Level 1", + "yearly_salary": 60000, + "contracted_weekly_hours": 38, + "award_template_organisation_id": 12345 + } + +200 +OK +(application/json) +Body + + { + "id": 123, + "from_date": "1970-1-1", + "to_date": "2020-1-1", + "hourly_rate": 0.0, + "award_tags": "Casual,Level 1", + "yearly_salary": 60000, + "contracted_weekly_hours": 38, + "user_id": 654321, + "award_template_organisation_id": 12345 + } + +Schema + +Attributes: +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Managing Multiple Pay Fields [/api/v2/user_pay_fields/{user_id}] +POSTCreate & Update Multiple Pay Fields For A User +This endpoint can be used through a POST request to manage a specific user’s pay field objects in bulk. + +This method requires a user_id to be given. +This method requires the cost scope (see Scopes for more information). + +You do not have to provide every pay field in the request body. The ones you do provide will form the new value of the updated pay field object. +The only limitation to updating behaviour is - you cannot update a user’s first pay field object’s from_date. You can identify which pay field object is the first by it’s from date. This should be represented as start. + +If you provide an id to a pay field object in the array, the request will look for the existing record and update the corresponding fields. You must provide a valid id in doing so. The whole request will fail there is a non-existent pay field id. + +If you do not provide an id to a pay field object in the array, the request will create a new pay field record for the user. It will create the record with the provided fields. + +Parameters +user_id: 654321 (number) - The ID of the user +Request Create / Update Pay Fields (application/json) +Body + + { + "pay_fields": [ + { + "id": 654321, + "regular_hours": "Pay Increase Jan 2022", + "from_date": "2020-1-1", + "to_date": "2021-1-1", + "hourly_rate": 0.0, + "award_tags": "Casual,Level 1", + "yearly_salary": 60000, + "contracted_weekly_hours": 38, + "award_template_organisation_id": 12345 + } + ] + } + +201 +Created +(application/json) +Attributes (array): +id: 12345 (number) - The id of the payfield +reason_for_change: Pay Fields Update Jan 2022 (string) - The name of a pay field change, usually specifying the reason. +from_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies after +to_date: 1970-1-1 (string) - The effective date that the grouping of payfields applies until +hourly_rate: 0.0 (number) - Hourly rate of the employee for the effective date +yearly_salary: 60000 (number) - Yearly Salary of the employee for the effective date +user_id: 12345 (number) - The id of the user these payfields apply to +award_template_organisation_id: 12345 (number) - The id of the award template organisation that is attached to this user +Deleting Pay Fields [/api/v2/user_pay_fields/{id}] +DELETEDelete Payfield By Id +Delete a specific payfield entry. + +This method requires a id to be given. +You cannot remove the earliest payfield record; There should ALWAYS be a single payfield record. If this isn’t the case, costing and award interpretation will not work as intended. + +This method requires the cost scope (see Scopes for more information). + +Parameters +id: 54321 (number) - The ID of payfield entry. +Request Delete Payfield (application/json) +Body + + { + "id": 54321 + } + +201 +Created +(application/json) +Locations +Admins and general managers can CRUD locations. Team managers can read locations which contain teams they manage. +Location List [/api/v2/locations] +GETGet Locations +This method requires the department scope (see Scopes for more information). +For business hours data, weekday ‘0’ is Sunday, ‘1’ is Monday, with the rest following that order till ‘6’ which ends on Saturday. +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified locations +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +show_business_hours: false (optional, boolean) - If true, business hours will be returned (defaults to false). +200 +OK +(application/json) +Attributes (array): +id: 111 (required) (number) - The id of the location +name: Springfield (required) (string) - The name of the location +short_name: S (string) - The abbreviated form of the location’s name +latitude: -27.459687 (number) - The latitude of the location +longitude: 153.032108 (number) - The longitude of the location +address: Springfield Powerplant Reactors (string) - Address of the location +time_zone: Australia/Brisbane (required) (string) - The location’s time zone +utc_offset: 36000 (required) (number) - The location’s time zone’s UTC offset +public_holiday_regions: au, au_qld (array[string]) - Public holiday regions (see Locations section for list of options) +specific_holiday_dates (array of SpecificHolidayDateData) - The dates and times of specific holidays for the location +business_day_cutoff: 5 (number) - The hour of the day before which data should be treated as being part of the previous day. eg 5 indicates that data from before 5am is part of the previous business day. +business_hours (array of BusinessHoursData) - The business hours of the location +record_id: 532432 (number) - the record ID, for use with Platform endpoints +POSTCreate Location +Use this endpoint to create a Location object. Location objects should have addresses, but the field is not mandatory - you can create a Location with just a name (though it will not be as useful). + +If an address is provided, latitude and longitude are also required. + +This method requires the department scope (see Scopes for more information). +Request Create Location (application/json) +Body + + { + "name": "Springfield", + "short_name": "S", + "latitude": -27.467004, + "longitude": 153.025453, + "address": "Springfield Powerplant Logistics Centre", + "public_holiday_regions": [ + "au" + ], + "specific_holiday_dates": [ + { + "date": "2016-03-14" + }, + { + "date": "2016-08-08", + "from": 12, + "to": 17 + } + ] + } + +200 +OK +(application/json) +Body + + { + "id": 801, + "name": "Springfield", + "short_name": "S", + "latitude": -27.467004, + "longitude": 153.025453, + "address": "Springfield Powerplant Logistics Centre", + "time_zone": null, + "organisation_id": 12, + "public_holiday_regions": [ + "au" + ], + "specific_holiday_dates": [ + { + "date": "2016-03-14" + }, + { + "date": "2016-08-08", + "from": 12, + "to": 17 + } + ], + "business_day_cutoff": 0 + } + +Request Create basic Location (application/json) +Body + + { + "name": "Springfield" + } + +200 +OK +(application/json) +Body + + { + "id": 801, + "name": "Springfield", + "short_name": null, + "latitude": null, + "longitude": null, + "address": null, + "time_zone": null, + "organisation_id": 12, + "public_holiday_regions": [], + "specific_holiday_dates": [], + "business_day_cutoff": 0 + } + +Schema + +Attributes: +id: 111 (required) (number) - The id of the location +name: Springfield (required) (string) - The name of the location +short_name: S (string) - The abbreviated form of the location’s name +latitude: -27.459687 (number) - The latitude of the location +longitude: 153.032108 (number) - The longitude of the location +address: Springfield Powerplant Reactors (string) - Address of the location +time_zone: Australia/Brisbane (required) (string) - The location’s time zone +utc_offset: 36000 (required) (number) - The location’s time zone’s UTC offset +public_holiday_regions: au, au_qld (array[string]) - Public holiday regions (see Locations section for list of options) +specific_holiday_dates (array of SpecificHolidayDateData) - The dates and times of specific holidays for the location +business_day_cutoff: 5 (number) - The hour of the day before which data should be treated as being part of the previous day. eg 5 indicates that data from before 5am is part of the previous business day. +business_hours (array of BusinessHoursData) - The business hours of the location +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: (number) +Location [/api/v2/locations/{id}] +GETGet Location +This method requires the department scope (see Scopes for more information). +Parameters +id: 111 (number) - The id of the location +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +200 +OK +(application/json) +Attributes: +id: 111 (required) (number) - The id of the location +name: Springfield (required) (string) - The name of the location +short_name: S (string) - The abbreviated form of the location’s name +latitude: -27.459687 (number) - The latitude of the location +longitude: 153.032108 (number) - The longitude of the location +address: Springfield Powerplant Reactors (string) - Address of the location +time_zone: Australia/Brisbane (required) (string) - The location’s time zone +utc_offset: 36000 (required) (number) - The location’s time zone’s UTC offset +public_holiday_regions: au, au_qld (array[string]) - Public holiday regions (see Locations section for list of options) +specific_holiday_dates (array of SpecificHolidayDateData) - The dates and times of specific holidays for the location +business_day_cutoff: 5 (number) - The hour of the day before which data should be treated as being part of the previous day. eg 5 indicates that data from before 5am is part of the previous business day. +business_hours (array of BusinessHoursData) - The business hours of the location +record_id: 532432 (number) - the record ID, for use with Platform endpoints +PUTUpdate Location +This method requires the department scope (see Scopes for more information). +Updating a location’s business hours +For business hours data, weekday ‘0’ is Sunday, ‘1’ is Monday, with the rest following that order till ‘6’ which ends on Saturday. Setting a config for a certain weekday through the API will override any existing business hours for that weekday, the rest of the week will stay the same. You can update multiple days at the same time with one request. +Parameters +id: 111 (number) - The id of the location +Request Update Location (application/json) +Body + + { + "name": "Shelbyville", + "short_name": "S", + "latitude": -27.467004, + "longitude": 153.025453 + } + +200 +OK +(application/json) +Attributes: +id: 111 (required) (number) - The id of the location +name: Springfield (required) (string) - The name of the location +short_name: S (string) - The abbreviated form of the location’s name +latitude: -27.459687 (number) - The latitude of the location +longitude: 153.032108 (number) - The longitude of the location +address: Springfield Powerplant Reactors (string) - Address of the location +time_zone: Australia/Brisbane (required) (string) - The location’s time zone +utc_offset: 36000 (required) (number) - The location’s time zone’s UTC offset +public_holiday_regions: au, au_qld (array[string]) - Public holiday regions (see Locations section for list of options) +specific_holiday_dates (array of SpecificHolidayDateData) - The dates and times of specific holidays for the location +business_day_cutoff: 5 (number) - The hour of the day before which data should be treated as being part of the previous day. eg 5 indicates that data from before 5am is part of the previous business day. +business_hours (array of BusinessHoursData) - The business hours of the location +record_id: 532432 (number) - the record ID, for use with Platform endpoints +name: Shelbyville +Request Update Business Hours (application/json) +Body + + { + "business_hours": [ + { "weekday": 0, "start": "08:30", "finish": "17:00" }, + { "weekday": 1, "start": "6am", "finish": "2pm" }, + { "weekday": 1, "start": "3pm", "finish": "8pm" }, + { "weekday": 2, "start": "11:30", "finish": "11:45pm" }, + ] + } + +200 +OK +(application/json) +Attributes: +id: 111 (required) (number) - The id of the location +name: Springfield (required) (string) - The name of the location +short_name: S (string) - The abbreviated form of the location’s name +latitude: -27.459687 (number) - The latitude of the location +longitude: 153.032108 (number) - The longitude of the location +address: Springfield Powerplant Reactors (string) - Address of the location +time_zone: Australia/Brisbane (required) (string) - The location’s time zone +utc_offset: 36000 (required) (number) - The location’s time zone’s UTC offset +public_holiday_regions: au, au_qld (array[string]) - Public holiday regions (see Locations section for list of options) +specific_holiday_dates (array of SpecificHolidayDateData) - The dates and times of specific holidays for the location +business_day_cutoff: 5 (number) - The hour of the day before which data should be treated as being part of the previous day. eg 5 indicates that data from before 5am is part of the previous business day. +business_hours (array of BusinessHoursData) - The business hours of the location +record_id: 532432 (number) - the record ID, for use with Platform endpoints +business_hours (array) +(object) +weekday: 0 (number) +start: 08:30 (string) +finish: 17:00 (string) +(object) +weekday: 1 (number) +start: 06:00 (string) +finish: 14:00 (string) +(object) +weekday: 1 (number) +start: 15:00 (string) +finish: 20:00 (string) +(object) +weekday: 2 (number) +start: 11:30 (string) +finish: 23:45 (string) +DELETEDelete Location +This method requires the department scope (see Scopes for more information). +Locations cannot be deleted if they have any teams. +Parameters +id: 111 (number) - The id of the location +200 +OK +(application/json) +Location Versions [/api/v2/locations/{id}/versions] +GETGet Location Versions +This method requires the department scope (see Scopes for more information). +Parameters +id: 111 (number) - The id of the location +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified location versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Location” (string) +changes (array) +(object) +field: name +previous: Spring field +updated: Springfield +(object) +field: longitude +previous: 153.032108 (number) +updated: 100.500169 (number) +Teams (Departments) +Departments are often referred to as Teams inside Workforce. Teams belong to a Location. +Department managers can read and update information about the departments they manage. Admins can CRUD departments. Employees can read the departments that they belong to, with a subset of fields (see below) + +The department group / team group Feature needs to be enabled before you can access this field. The assisting teams Feature needs to be enabled before you can access this field. + +Team List [/api/v2/departments] +GETGet Teams +This method requires the department scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified teams +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +tracking_categories: false (optional, boolean) - If true, tracking categories for each team will be returned (defaults to false). +page: 1 (optional, number) - The page number for your teams list +page_size: 50 (optional, number) - The number of teams retrieved per page. The maximum is 100 teams per page. +Request As a manager + +200 +OK +(application/json) +Attributes (array): +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +Request As an employee + +200 +OK +(application/json) +Body + +[ + { + "id": 608, + "name": "Waiters", + "location_id": 111, + "colour": "#FBB830" + } +] + +POSTCreate Team +This method requires the department scope (see Scopes for more information). +Request Create Team (application/json) +Body + + { + "name": "Waiters", + "location_id": 111, + "export_name": "WGB-32", + "colour": "#FBB830", + "team_group": "Front of House", + "qualification_ids" [123456], + "manager_ids": [4444, 5555], + "user_ids": [1111, 2222, 3333] + } + +200 +OK +(application/json) +Attributes(DepartmentData) +id: 809 (number) +location_id: 111 +name: Waiters +export_name: WGB-32 +colour: #FBB830 +staff: [1111, 2222, 3333] +managers: [4444, 5555] +team_group: Front of House +assisting_team_ids: (array) +Request Create Basic Team (application/json) +Body + + { + "name": "Waiters", + "location_id": "111", + } + +200 +OK +(application/json) +Body + + { + "id": 809, + "location_id": 111, + "name": "Waiters", + "export_name": null, + "colour": null, + "staff": [], + "managers": [] + } + +Schema + +Attributes: +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +id: (number) +Team [/api/v2/departments/{id}] +GETGet Team +This method requires the department scope (see Scopes for more information). +Parameters +id: 111 (number) - The id of the team +platform: false (optional, boolean) - If true, Platform data will be returned (defaults to false). +tracking_categories: false (optional, boolean) - If true, tracking categories for each team will be returned (defaults to false). +200 +OK +(application/json) +Attributes: +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +PUTUpdate Team +This method requires the department scope (see Scopes for more information). +Updating users & managers +Updating a department’s users or managers through a PUT request will completely replace existing users and managers with the ids provided. It is recommended to only update users and managers on setup of an account. Users provided must also be active or your request will fail. +Updating required team qualifications +When providing qualification ids you wish to assign as ‘required’ to join this team, users in the team must all have the qualification or else your request will fail. +Parameters +id: 111 (number) - The id of the team +Request Update Team Name (application/json) +Body + + { + "name": "Kitchen Staff" + } + +200 +OK +(application/json) +Attributes: +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +name: Kitchen Staff +Request Update Users & Managers (application/json) +Body + + { + "user_ids": [1111, 2222, 3333], + "manager_ids": [4444, 5555] + } + +200 +OK +(application/json) +Attributes: +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +staff: 1111, 2222, 3333 +managers: 4444, 5555 +Request Update Required Qualifications (application/json) +Body + + { + "qualification_ids": [1001, 1002] + } + +200 +OK +(application/json) +Attributes: +id: 808 (required) (number) - The id of the team +location_id: 111 (required) (number) - The id of the team’s location +name: Waiters (required) (string) - The name of the team +export_name: WGB-30 (string) - The payroll export name of the team (if different to the name) +colour: #FBB829 (string) - The team’s colour +staff: 1, 2, 123456 (array[number]) - The user ids of the staff in the team +managers: 4, 5 (array[number]) - The user ids of the managers of the team +associated_tags: Level 2, Level 3 HD (array[string]) - Tags automatically assigned to shifts worked in this team +qualification_ids: 1234, 4321 (array[number]) - The required qualifications to work in this team +assisting_team_ids: 1234, 4321 (array[number]) - The teams that will count toward this team’s requirements on the roster +team_group: Front of House (string) - A group of teams for rostering or reporting +record_id: 532432 (number) - the record ID, for use with Platform endpoints +qualifications: 1001, 1002 +DELETEDelete Team +This method requires the department scope (see Scopes for more information). +Parameters +id: 111 (number) - The id of the team +200 +OK +(application/json) +Team Versions [/api/v2/departments/{id}/versions] +GETGet Team Versions +This method requires the department scope (see Scopes for more information). +Parameters +id: 111 (number) - The id of the team +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified team versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Department” (string) +changes (array) +(object) +field: name +previous: Wait staff +updated: Waiters +(object) +field: colour +previous: #3FAFD7 +updated: #FBB829 +Shift Details +Shift details belong to a Team, and can be assigned on Schedules within that team. +Teams managers can read and update information about the shift details from the teams they manage. Admins can CRUD shift details. + +Shift Detail List [/api/v2/shift_details] +GETGet Shift Details +This method requires the department scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 36 (required) (number) - The id of the shift detail +department_id: 808 (required) (number) - The id of the shift detail’s department +name: Higher Duties (required, string) - The name of the shift detail +POSTCreate Shift Detail +This method requires the department scope (see Scopes for more information). +Request Create Shift Detail (application/json) +Body + + { + "name": "Manager", + "department_id": 808 + } + +200 +OK +(application/json) +Attributes(ShiftDetailData) +id: 532 (number) +department_id: 808 +name: Manager +Shift Detail [/api/v2/shift_details/{id}] +GETGet Shift Detail +This method requires the department scope (see Scopes for more information). +Parameters +id: 36 (number) - The id of the shift detail +200 +OK +(application/json) +Attributes: +id: 36 (required) (number) - The id of the shift detail +department_id: 808 (required) (number) - The id of the shift detail’s department +name: Higher Duties (required, string) - The name of the shift detail +PUTUpdate Shift Detail +This method requires the department scope (see Scopes for more information). +Parameters +id: 36 (number) - The id of the shift detail +Request Update Shift Detail (application/json) +Body + + { + "name": "Shift Runner" + } + +200 +OK +(application/json) +Attributes: +id: 36 (required) (number) - The id of the shift detail +department_id: 808 (required) (number) - The id of the shift detail’s department +name: Higher Duties (required, string) - The name of the shift detail ++ name: Shift Runner + +DELETEDelete Shift Detail +This method requires the department scope (see Scopes for more information). +Parameters +id: 36 (number) - The id of the shift detail +200 +OK +(application/json) +Access & Roles +By default only admin level users can access roles. But you can also define new roles that are able to access roles! +Platform Role List [/api/v2/platform/roles] +GETGet Platform Roles +This method requires the platform scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 3816 (required) (number) - The id of the role +name: Admin (required) (string) - The name of the role +description: access to all functions in Tanda (required, string) - Description of the role +type: organisation_admin (required) (string) - organisation_admin manager employee +enabled: true (required) (boolean) - Can the role be applied to staff? +native: true (required) (boolean) - If false, the role was user-created. If true, system-created. +sort_order: 1 (required) (number) - Order the role should display in +Award Tags +No special permissions are required to read award tags. +Award Tag List [/api/v2/award_tags] +GETGet Award Tags +This method requires the user scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified award tags +200 +OK +(application/json) +Attributes (array): +id: 3816 (required) (number) - The id of the award tag +name: Casual (required) (string) - The name of the award tag +Award Tag [/api/v2/award_tags/{id}] +GETGet Award Tag +This method requires the user scope (see Scopes for more information). +Parameters +id: 3816 (number) - The id of the Award Tag +200 +OK +(application/json) +Attributes: +id: 3816 (required) (number) - The id of the award tag +name: Casual (required) (string) - The name of the award tag +Leave Requests +Leave is time formally not worked by an employee, due to things like public holidays, sickness, or by accruing annual leave. + +For specifying times when an employee requests not to be rostered, please see Unavailability. + +Admins, payroll officers, and roster managers can create, read and update leave requests for all users, and department managers can create, read and update leave requests for users that they manage. Other users are able to create, read and update leave requests for themself. Note that the current user may not update status for their own leave requests unless they are an Admin, payroll officer or roster manager. Deletion of leave requests is not supported. +Leave List [/api/v2/leave] +GETGet Leave List +Lookup multiple leave requests in a single request. + +Your URL parameters must include: + +from and to, or +ids, or +all 3 of the above +You can also specify user_ids to further filter your request. + +This method requires the leave scope (see Scopes for more information). +You will only be able to query up to 366 days when specifying from and to. +By default inactive users are excluded. +Parameters +ids: 1,2,666 (optional, string) - Comma separated list of leave ids to lookup +from: 2016-03-15 (optional, string) - From date to lookup leave in +to: 2016-04-05 (optional, string) - To date to lookup leave in +user_ids: 1,2,123456 (optional, string) - Comma separated list of user ids to lookup leave for +page: 1 (optional, number) - The page number for your leave request list +page_size: 50 (optional, number) - The number of leave requests retrieved per page. Maximum 100 per page. +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified leave requests +include_inactive: true (optional, boolean) - Include inactive staff in your response. +200 +OK +(application/json) +Attributes (array): +id: 666 (required) (number) - The id of the leave request +department_id: 808 (number) - The id of the team with which the leave request’s shifts will be tagged +user_id: 123456 (required) (number) - The id of the user who owns the leave request +reason: Terribly Sick (string) - The reason for the leave request - optional +leave_type: Personal/Carer’s Leave (required, string) - The type of leave request +hours: 7.6 (required) (number) - The number of workable hours the leave request spans +start: 2016-04-01 (required) (string) - The start date for the leave request +finish: 2016-04-01 (required) (string) - The end date for the leave request +start_time: 2000-01-01T09:30:00+10:00 (optional, string) - The start time for a leave request event +finish_time: 2000-01-01T05:30:00+10:00 (optional, string) - The end time for a leave request event +status: pending (required) (string) - The status for the leave request. One of [‘pending’, ‘approved’, ‘rejected’] +multitagging: false (string) - Determines whether shifts on the leave request can be tagged differently. +all_day: true (optional, boolean) - Useful for all day leave requests. +include_inactive: false (optional, boolean) - Useful for all leave requests that include inactive staff. +daily_breakdown (object) +2016-04-01: (object) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-04-01T09:30:00+1000 (required, string) +finish_time: 2016-04-01T17:30:00+1000 (required, string) +POSTCreate Leave Request +Note that a status for a new Leave Request is required. Managers can create approved leave requests for their staff, and themselves if permitted by the organisation settings. Admins can create approved leave requests for all staff. Employees may only create pending leave requests. The server will respond with a 400 when a user creates a leave request violating the above requirements. + +Leave request status must be one of: “pending”, “approved”, “rejected”. + +When a leave request is created with status “approved”, a Shift will be created for each day the leave request spans, and will be attached to the leave request. If a department_id is passed, these shifts will be categorised with the team corresponding to the department_id. Passing a department_id will have no consequence if the leave request status is “pending” or “rejected”. Note that the department_id will not be returned by the API, since technically it is a property of the shift, not of the leave request. + +If you are building a user interface for the creation of leave requests, you might like to use the default hours endpoint to get recommendations for the hours field as the user chooses their dates. + +You can attach a file when creating a leave request using the file_id parameter. See the temporary files endpoint for more information. The file should be a JPEG, PNG, GIF, or PDF. + +This method requires the leave scope (see Scopes for more information). +New: time fields, and all_day boolean added to leave. Time fields allow leave to apply to a portion of a day, instead of a whole day. The date part of the time fields is not to be used and when returned from the API will always use the ruby default date of 2000-01-01. +Request Create Leave (application/json) +Body + + { + "reason": "Sick Day", + "department_id": 2, + "user_id": 123456, + "hours": 114.0, + "start": "2016-03-07", + "start_time": "2000-01-01 09:30:00", + "finish": "2016-03-07", + "finish_time": "2000-01-01 17:30:00", + "leave_type": "Sick Leave", + "fallback_leave_type": "Unpaid Leave", + "status": "pending", + "all_day": false, + "file_id": "73fe3430-4f5d-3a0a-84a7-cffbeb5efeb2", + "daily_breakdown": [ + { + "date": "2016-03-07" + "hours": 114.0, + "all_day": true, + "start_time": "2016-03-07T09:30:00+1000", + "finish_time": "2016-03-07T17:30:00+1000" + } + ] + } + +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave request +department_id: 808 (number) - The id of the team with which the leave request’s shifts will be tagged +user_id: 123456 (required) (number) - The id of the user who owns the leave request +reason: Terribly Sick (string) - The reason for the leave request - optional +leave_type: Personal/Carer’s Leave (required, string) - The type of leave request +hours: 7.6 (required) (number) - The number of workable hours the leave request spans +start: 2016-04-01 (required) (string) - The start date for the leave request +finish: 2016-04-01 (required) (string) - The end date for the leave request +start_time: 2000-01-01T09:30:00+10:00 (optional, string) - The start time for a leave request event +finish_time: 2000-01-01T05:30:00+10:00 (optional, string) - The end time for a leave request event +status: pending (required) (string) - The status for the leave request. One of [‘pending’, ‘approved’, ‘rejected’] +multitagging: false (string) - Determines whether shifts on the leave request can be tagged differently. +all_day: true (optional, boolean) - Useful for all day leave requests. +include_inactive: false (optional, boolean) - Useful for all leave requests that include inactive staff. +daily_breakdown (object) +2016-04-01: (object) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-04-01T09:30:00+1000 (required, string) +finish_time: 2016-04-01T17:30:00+1000 (required, string) +id: 7250 (number) +user_id: 123456 (number) +reason: Holiday in France +leave_type: Annual Leave +fallback_leave_type: Unpaid Leave +hours: 114.0 (number) +start: 2016-03-07 +start_time: 2000-01-01T09:30:00 +finish: 2016-03-08 +finish_time: 2000-01-01T17:30:00 +status: pending +all_day: false +verification: http://springfieldfiles.com/albums/notes/0244.JPG +daily_breakdown: (array) +(object) +date: 2016-03-07 (required, string) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-03-07T09:30:00+1000 (required, string) +finish_time: 2016-03-07T17:30:00+1000 (required, string) +(object) +date: 2016-03-08 (required, string) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-03-08T09:30:00+1000 (required, string) +finish_time: 2016-03-08T17:30:00+1000 (required, string) +Leave Request [/api/v2/leave/{id}] +GETGet Leave Request +This method requires the leave scope (see Scopes for more information). +Parameters +id: 666 (number) - The id of the leave to lookup +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave request +department_id: 808 (number) - The id of the team with which the leave request’s shifts will be tagged +user_id: 123456 (required) (number) - The id of the user who owns the leave request +reason: Terribly Sick (string) - The reason for the leave request - optional +leave_type: Personal/Carer’s Leave (required, string) - The type of leave request +hours: 7.6 (required) (number) - The number of workable hours the leave request spans +start: 2016-04-01 (required) (string) - The start date for the leave request +finish: 2016-04-01 (required) (string) - The end date for the leave request +start_time: 2000-01-01T09:30:00+10:00 (optional, string) - The start time for a leave request event +finish_time: 2000-01-01T05:30:00+10:00 (optional, string) - The end time for a leave request event +status: pending (required) (string) - The status for the leave request. One of [‘pending’, ‘approved’, ‘rejected’] +multitagging: false (string) - Determines whether shifts on the leave request can be tagged differently. +all_day: true (optional, boolean) - Useful for all day leave requests. +include_inactive: false (optional, boolean) - Useful for all leave requests that include inactive staff. +daily_breakdown (object) +2016-04-01: (object) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-04-01T09:30:00+1000 (required, string) +finish_time: 2016-04-01T17:30:00+1000 (required, string) +PUTUpdate Leave Request +This method requires the leave scope (see Scopes for more information). +Employees cannot update a leave request, with the exception of rejecting their own pending leave request. +Parameters +id: 666 (number) - The id of the leave to update +Request Update Leave (application/json) +Body + + { + "reason": "Really Really Sick", + } + +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave request +department_id: 808 (number) - The id of the team with which the leave request’s shifts will be tagged +user_id: 123456 (required) (number) - The id of the user who owns the leave request +reason: Terribly Sick (string) - The reason for the leave request - optional +leave_type: Personal/Carer’s Leave (required, string) - The type of leave request +hours: 7.6 (required) (number) - The number of workable hours the leave request spans +start: 2016-04-01 (required) (string) - The start date for the leave request +finish: 2016-04-01 (required) (string) - The end date for the leave request +start_time: 2000-01-01T09:30:00+10:00 (optional, string) - The start time for a leave request event +finish_time: 2000-01-01T05:30:00+10:00 (optional, string) - The end time for a leave request event +status: pending (required) (string) - The status for the leave request. One of [‘pending’, ‘approved’, ‘rejected’] +multitagging: false (string) - Determines whether shifts on the leave request can be tagged differently. +all_day: true (optional, boolean) - Useful for all day leave requests. +include_inactive: false (optional, boolean) - Useful for all leave requests that include inactive staff. +daily_breakdown (object) +2016-04-01: (object) +hours: 7.6 (optional, number) +all_day: true (required, boolean) +start_time: 2016-04-01T09:30:00+1000 (required, string) +finish_time: 2016-04-01T17:30:00+1000 (required, string) +reason: Really Really Sick +Leave Types [/api/v2/leave/types_for/{user_id}] +GETGet Leave Types for User +This method requires the leave scope (see Scopes for more information). +Parameters +user_id: 123456 (number) - The id of the user to lookup leave types for +200 +OK +(application/json) +Attributes: +Annual Leave (string) - Leave type +Personal/Carer’s Leave (string) - Leave type +Leave Types With Details [/api/v2/leave/types_with_details_for/{user_id}] +GETGet Leave Types with details for User +This method requires the leave scope (see Scopes for more information). +Parameters +user_id: 123456 (number) - The id of the user to lookup leave types for +200 +OK +(application/json) +Body + + [ + { + name: "Annual Leave", + prevent_negative: true, + default_fallback_leave_type_name: "Unpaid Leave" + }, + { + name: "Unpaid Leave", + prevent_negative: false, + default_fallback_leave_type_name: nil + } ] + +Leave Request Versions [/api/v2/leave/{id}/versions] +GETGet Leave Request Versions +This method requires the leave scope (see Scopes for more information). +Parameters +id: 666 (number) - The id of the leave request +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified leave request versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Leave” (string) +changes (array) +(object) +field: status +previous: pending +updated: approved +(object) +field: reason +previous: Holiday +updated: Terribly Sick +Default Leave Hours [/api/v2/leave/hours_between] +GETGet Default Hours for a Leave Request +Use this endpoint to get a recommendation for the number of hours of leave someone should take based on the given dates. This will take into account public holidays and weekends, and the default leave hours setting in an account. For example, if given a date range of 5 days, of which 1 is a weekend and 1 is a holiday, and with a default leave hours setting of 7.6, the endpoint will return 22.8 hours. + +This method requires the leave scope (see Scopes for more information). +Parameters +user_id: 123456 (number) - The id of the user +start: 2016-03-15 (string) - Start date of leave request +finish: 2016-03-17 (string) - Finish date of leave request +leave_type: Annual Leave (optional, string) - The leave type +200 +OK +(application/json) +Body + + { + "hours": "22.8", + } + +Leave Balances +Leave balances denote how much leave (of each type) someone has accrued. + +Leave balance interactions require the same permissions as leave requests. +Leave Balance List [/api/v2/leave_balances] +GETGet Leave Balances +Lookup leave balances for one or more user_ids. + +This method requires the leave scope (see Scopes for more information). +Parameters +user_ids: 1,2,666 (required, string) - Comma separated list of user ids to lookup leave balances for +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified leave balances +200 +OK +(application/json) +Attributes (array): +id: 666 (required) (number) - The id of the leave balance +user_id: 123456 (required) (number) - The id of the user who owns the leave balance +leave_type: Annual Leave (required, string) - The type of leave +hours: 4 (required) (number) - The number of hours of leave accrued +should_accrue: false (boolean) - If true, this leave balance will accrue in Tanda automatically. If false, you should keep it up to date. Default is false. +POSTCreate Leave Balance +Use the Leave Types For User call to find accessible leave types for your user. + +This method requires the leave scope (see Scopes for more information). +Request Create Leave Balance (application/json) +Body + + { + "user_id": 123456, + "leave_type": "Annual Leave", + "hours": 18.0 + } + +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave balance +user_id: 123456 (required) (number) - The id of the user who owns the leave balance +leave_type: Annual Leave (required, string) - The type of leave +hours: 4 (required) (number) - The number of hours of leave accrued +should_accrue: false (boolean) - If true, this leave balance will accrue in Tanda automatically. If false, you should keep it up to date. Default is false. ++ user_id: 123456 (number) ++ hours: 18.0 (number) ++ leave_type: Annual Leave + +Request Create multiple Leave Balances (application/json) +Body + + { + "balances": [ + { + "user_id": 123456, + "leave_type": "Annual Leave", + "hours": 18.0 + }, + { + "user_id": 654321, + "leave_type": "Sick Leave", + "hours": 22.0 + } + ] + } + +Response 200 (application/json) Attributes (array): + ++ (object) + + user_id: 123456 (number) + + hours: 18.0 (number) + + leave_type: Annual Leave ++ (object) + + user_id: 654321 (number) + + hours: 22.0 (number) + + leave_type: Sick Leave + +Leave Balance [/api/v2/leave_balances/{id}] +GETGet Leave Balance +This method requires the leave scope (see Scopes for more information). +Parameters +id: 666 (number) - The ID of the leave balance to lookup +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave balance +user_id: 123456 (required) (number) - The id of the user who owns the leave balance +leave_type: Annual Leave (required, string) - The type of leave +hours: 4 (required) (number) - The number of hours of leave accrued +should_accrue: false (boolean) - If true, this leave balance will accrue in Tanda automatically. If false, you should keep it up to date. Default is false. +PUTUpdate Leave Balance with ID +This method requires the leave scope (see Scopes for more information). +Parameters +id: 666 (number) - The ID of the leave balance to update +Request Update Leave (application/json) +Body + + { + "hours": 22.5 + } + +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave balance +user_id: 123456 (required) (number) - The id of the user who owns the leave balance +leave_type: Annual Leave (required, string) - The type of leave +hours: 4 (required) (number) - The number of hours of leave accrued +should_accrue: false (boolean) - If true, this leave balance will accrue in Tanda automatically. If false, you should keep it up to date. Default is false. ++ hours: 22.5 (number) + +Update Leave Balance with User ID [/api/v2/leave_balances/user/{user_id}] +PUTUpdate Leave Balance with User ID +This method requires the leave scope (see Scopes for more information). +Parameters +user_id: 123456 (number) - The ID of the user whose leave balance you want to update +Request Update Leave Balance with User ID (application/json) +Body + + { + "leave_type": "Annual Leave", + "hours": 18.0, + "should_accrue": false + } + +200 +OK +(application/json) +Attributes: +id: 666 (required) (number) - The id of the leave balance +user_id: 123456 (required) (number) - The id of the user who owns the leave balance +leave_type: Annual Leave (required, string) - The type of leave +hours: 4 (required) (number) - The number of hours of leave accrued +should_accrue: false (boolean) - If true, this leave balance will accrue in Tanda automatically. If false, you should keep it up to date. Default is false. ++ hours: 18.0 (number) + +Request Create multiple Leave Balances with User ID (application/json) +Body + + { + "balances": [ + { + "leave_type": "Annual Leave", + "hours": 18.0 + }, + { + "leave_type": "Sick Leave", + "hours": 22.0 + } + ] + } + +Response 200 (application/json) Attributes (array): + ++ (object) + + hours: 18.0 (number) + + leave_type: Annual Leave ++ (object) + + hours: 22.0 (number) + + leave_type: Sick Leave + +Predict Leave Balances [/api/v2/leave_balances/{id}/predict] +POSTPredict Leave Balance +This endpoint takes a leave balance ID and a date, and predicts what the leave balance will be on the given date. The date must be in the future. + +This method requires the leave scope (see Scopes for more information). +You should make it clear when displaying the result that this is a prediction. There is no guarantee the user’s actual leave balance will match this when the date comes. + +Parameters +id: 654321 (number) - The ID of the leave balance +Request Predict Leave Balance (application/json) +Body + + { + "date": "2019-12-24" + } + +200 +OK +(application/json) + { “prediction”: 22.5 } +Availability +Availability supersedes Unavailability for organisations that have migrated to the availability model. + +All availability endpoints require the unavailability scope (see Scopes for more information). +Callers only see users they can already see via Users — user_ids is filtered against the caller’s visible-users scope. Unknown or hidden ids are silently dropped. Inactive (terminated) users are included when requested by explicit id, so reports covering past staff keep working. +Effective Availability [/api/v2/availability/effective] +GETGet Effective Availability +Returns the day-by-day merged projection of a user’s availability. Use this to answer the question “what is the availability of X users in Y date range?”. + +This method requires the unavailability scope. +This endpoint returns a higher volume of data per user than /api/v2/unavailability. This makes year long ranges less practical to deliver. When fetching effective availability for all visible users (i.e. user_ids omitted) the from..to range is capped at 31 days. There is no date-range cap when fetching for a specific set of users. The upside of the larger payload is that the data is flatter and simpler to interpret than the previous unavailability format. +Parameters +from: 2026-04-20 (required, string) - Inclusive start of the projection window (ISO date, YYYY-MM-DD). +to: 2026-04-26 (required, string) - Inclusive end of the projection window (ISO date, YYYY-MM-DD). +user_ids: 42,123456 (optional, string) - Comma-separated list of user ids to project availability for. When omitted, defaults to all active users the caller can see. In that “all visible users” mode the from..to window must be 31 days or fewer. Explicit user_ids mode has no date range cap and includes inactive (terminated) users. +200 +OK +(application/json) +Body + + [ + { + "user_id": 42, + "availability": [ + { + "date": "2026-04-20", + "time_blocks": [ + { "start": "00:00", "finish": "10:00" }, + { "start": "14:00", "finish": "24:00" } + ] + }, + { + "date": "2026-04-21", + "time_blocks": [ + { "start": "09:00", "finish": "17:00" } + ] + }, + { + "date": "2026-04-23", + "time_blocks": [ + { "start": "08:00", "finish": "18:00" } + ] + }, + { + "date": "2026-04-26", + "time_blocks": [] + } + ] + } + ] + +Notes: + +availability contains exactly one entry per date in from..to inclusive, in chronological order. +time_blocks lists the availability windows for that date (when the employee is available). [] means fully unavailable; a full-day window is rendered as {"start": "00:00", "finish": "24:00"}. +An empty time blocks array ("time_blocks": []) means not available at all that day. +A finish that lands on the following midnight is rendered as "24:00" on the same day rather than "00:00" on the next. +Ongoing Availability [/api/v2/availability/ongoing] +GETGet Ongoing Availability +Returns each user’s ongoing patterns that are currently active or scheduled to start in the future — the pattern effective today plus any approved patterns with start_date > today. Superseded historical patterns are not returned; use Effective Availability to project day-by-day availability across the merged layers. + +Use this to monitor scheduled patterns: what’s currently running, what’s about to change, what’s coming up, and how recently each pattern was edited. + +This method requires the unavailability scope. +Parameters +user_ids: 4704593 (required, string) - Comma-separated list of user ids to fetch ongoing availability for. Inactive (terminated) users are included when requested by id. +200 +OK +(application/json) +Body + + [ + { + "user_id": 4704593, + "ongoing": [ + { + "id": 49, + "start_date": "2026-06-01", + "frequency": "weekly", + "updated_at": "2026-05-20T03:21:00Z", + "days": [ + { "day_in_pattern": 0, "time_blocks": [{ "start": "12:00", "finish": "22:00" }] }, + { "day_in_pattern": 1, "time_blocks": [{ "start": "16:00", "finish": "22:00" }] }, + { "day_in_pattern": 2, "time_blocks": [] }, + { "day_in_pattern": 3, "time_blocks": [] }, + { "day_in_pattern": 4, "time_blocks": [] }, + { "day_in_pattern": 5, "time_blocks": [] }, + { "day_in_pattern": 6, "time_blocks": [] } + ] + } + ] + } + ] + +Notes: + +frequency is the repeat cadence of the pattern. One of weekly or fortnightly. +start_date is the first day the pattern applies. For the currently-effective pattern this may be in the past; for upcoming patterns it is in the future. +updated_at is an ISO-8601 timestamp of the last edit to the record. +days describes the repeating pattern. day_in_pattern is a zero-based index into the cycle (for weekly, 0 is Monday through 6 Sunday; for fortnightly, 0–13). Every slot in the cycle is always present in the response. +time_blocks lists availability windows directly (when the employee is available). An empty array means fully unavailable for that day-in-pattern; a day the employee is available for in full is rendered as an explicit full-day window ({"start": "00:00", "finish": "24:00"}), matching /api/v2/availability/effective. +Availability Requests [/api/v2/availability/requests] +GETGet Availability Requests +Returns each user’s individual availability requests. This includes ongoing patterns and temporary changes. This endpoint returns all statuses by default (pending, approved, and rejected), with one entry per underlying record. Use it to sync availability requests into external systems, mirroring what the deprecated /api/v2/unavailability endpoint provided. + +Inactive (terminated) users are included when their ids are requested, so reports covering past staff keep working. + +This method requires the unavailability scope. +Parameters +user_ids: 4704593 (optional, string) - Comma-separated list of user ids to fetch records for. When omitted, defaults to all active users the caller can see. +ids: 51,52 (optional, string) - Comma-separated list of record ids to fetch. Filters to these specific records. +statuses: pending,rejected (optional, string) - Comma-separated statuses to filter by. One or more of pending, approved, rejected. Defaults to all statuses. +from: 2026-06-01 (optional, string) - With to, keeps only records overlapping the window. Open-ended ongoing patterns overlap every window from their start_date onward. +to: 2026-06-30 (optional, string) - See from. Both or neither must be given. +updated_after: 1780000000 (optional, number) - Unix timestamp; keeps only records updated strictly after it. Use for incremental syncs. +200 +OK +(application/json) +Body + + [ + { + "id": 51, + "user_id": 4704593, + "type": "temporary", + "status": "rejected", + "reason": "UNPAID CASUAL LEAVE", + "start_date": "2026-06-14", + "finish_date": "2026-06-15", + "all_day": true, + "created_at": "2026-06-01T10:12:00+10:00", + "updated_at": "2026-06-02T09:30:00+10:00" + }, + { + "id": 52, + "user_id": 4704593, + "type": "ongoing", + "status": "approved", + "reason": "Study", + "start_date": "2026-06-01", + "finish_date": null, + "all_day": false, + "frequency": "weekly", + "days": [ + { "day_in_pattern": 0, "time_blocks": [{ "start": "09:00", "finish": "17:00" }] }, + { "day_in_pattern": 1, "time_blocks": [] }, + { "day_in_pattern": 2, "time_blocks": [] }, + { "day_in_pattern": 3, "time_blocks": [] }, + { "day_in_pattern": 4, "time_blocks": [] }, + { "day_in_pattern": 5, "time_blocks": [] }, + { "day_in_pattern": 6, "time_blocks": [] } + ], + "created_at": "2026-05-20T08:00:00+10:00", + "updated_at": "2026-05-20T08:00:00+10:00" + }, + { + "id": 53, + "user_id": 4704593, + "type": "temporary", + "status": "approved", + "reason": "Partial day off", + "start_date": "2026-06-20", + "finish_date": "2026-06-20", + "all_day": false, + "days": [ + { "date": "2026-06-20", "time_blocks": [{ "start": "00:00", "finish": "10:00" }, { "start": "14:00", "finish": "24:00" }] } + ], + "created_at": "2026-06-05T08:00:00+10:00", + "updated_at": "2026-06-05T08:00:00+10:00" + } + ] + +Notes: + +type is ongoing for a recurring pattern or temporary for a one-off change. +status is the approval workflow state: pending, approved, or rejected. +reason is the requester-supplied title for the record. +finish_date is null for open-ended ongoing patterns. temporary records are one-offs bounded by start_date/finish_date. +all_day is true when the record has no specific-time windows — it applies to whole days. +frequency (weekly or fortnightly) is present only for ongoing records. +days gives the time-of-day breakdown. It is always present for ongoing records; for temporary records it is present only when they have specific-time windows (all_day: false) and omitted for whole-day ones (all_day: true). In every case time_blocks are the periods the user is available, [] means fully unavailable, and a fully-available day is an explicit 00:00–24:00 block. The keying differs by type: +ongoing records follow Ongoing Availability: one {day_in_pattern, time_blocks} entry per slot in the cycle. +temporary records are keyed by concrete date: one {date, time_blocks} entry per date in start_date..finish_date, matching the shape of Effective Availability. A specific-time one-off carved out as unavailable is reported as its inverse — the windows the user remains available. +Records are ordered by start_date ascending. +Unavailability +Unavailability is an employee’s way to notify the person building the roster that they are not going to be available during a certain time period. Unavailability is most used by casual staff or employees with flexible hours. + +For specifying times when an employee formally took time off, or to request time off (most often for part time and full time employees), please see Leave. + +This endpoint group is deprecated and will be removed in a future release. Organisations that have migrated to the new availability model should read from /api/v2/availability/effective instead. For migrated organisations this endpoint reflects the legacy unavailability data only and will not include records created in the new availability model. +If the organisation has unavailability enabled, then all employees can CRUD unavailability about themselves. Roster managers, and admins can CRUD unavailability for everyone and department managers can CRUD unavailability for users they can manage. +Unavailability List [/api/v2/unavailability] +GETGet Unavailabilities +Lookup multiple leave requests in a single request. + +You must specify either both from and to or ids (you can specify both if you want too). user_ids can be specified in any case. + +This method requires the unavailability scope (see Scopes for more information). +You will only be able to query up to 366 days when specifying from and to. +Parameters +ids: 1,2,42 (optional, string) - Comma separated list of unavailability ids to lookup +from: 2016-03-15 (optional, string) - From date to lookup unavailability in +to: 2016-04-05 (optional, string) - To date to lookup unavailability in +user_ids: 1,2,123456 (optional, string) - Comma separated list of user ids to lookup unavailability for +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified unavailability +200 +OK +(application/json) +Body + + [ + { + "id": 42, + "user_id": 123456, + "title": "Doing errr... something", + "start": 1459814400, + "finish": 1459836000, + "repeating": false, + "repeating_info": null, + "all_day": false + } + ] + +POSTCreate Unavailability +This method requires the unavailability scope (see Scopes for more information). +There’s two ways to create unavailabilities: using dates (for all_day events), or using times (for events that don’t go all day). + +Regardless of which method you use, you should use the unavailability minimum date from the Settings endpoint to validate your dates or times locally. If you pass a time that is earlier than the minimum date (in the given user’s time zone), the request will fail. You can override the failure by setting the override_validations flag. + +To create an event using dates, use these parameters. Note that date_from and date_to are inclusive. If you have an unavailability that only goes for one day, they should have the same value. If you have an unavailability that goes for two full days, they should be one day apart, etc. + +all_day: true +user_id - integer +title - string +repeating - boolean +date_from - string, eg “2016-03-15” +date_to - string, eg “2016-03-16” - In this example, you will be unavailable on the 15th AND the 16th of March +To create an event using times, use these parameters: + +all_day: false +user_id - integer +title - string +repeating - boolean +start - integer, eg 1459375200 +finish - integer, eg 1459382400 - In this example, you will be unavailable from 2016-03-30 22:00:00 UTC to 2016-03-31 00:00:00 UTC +To bypass checking minimum date and published schedule checks, use override_validations: + +all_day: true +override_validations: true +user_id - integer +title - string +repeating - boolean +start - integer, eg 1459375200 +finish - integer, eg 1459382400 - In this example, you will be unavailable from 2016-03-30 22:00:00 UTC to 2016-03-31 00:00:00 UTC +You can also use the repeating param to tell an event to repeat. For example: + +all_day: false +user_id - integer +title - string +repeating - true +repeating_info: {"interval": "week","occurrences": 4} +start - integer, eg 1494838800 +finish - integer, eg 1494867600 - In this example, you will be unavailable from 2017-05-15 09:00:00 UTC to 2017-05-15 17:00:00 UTC, and at the same times for the next 3 weeks after (4 weeks total). +There’s more examples in the sidebar. The examples are based on a user in the AEST+10 time zone. + +Request Create Unavailability (application/json) +Body + + { + "user_id": 123456, + "title": "Buying a unicycle", + "start": 1459375200, + "finish": 1459382400, + "repeating": false + } + +201 +Created +(application/json) +Body + + { + "id": 106092, + "user_id": 123456, + "title": "Buying a unicycle", + "start": 1459375200, + "finish": 1459382400, + "repeating": false, + "repeating_info": null, + "all_day": false + } + +Request Create all day Unavailability (application/json) +Body + + { + "user_id": 123456, + "title": "Buying a unicycle", + "date_from": "2017-05-15", + "date_to": "2017-05-15", + "repeating": false, + "all_day": true + } + +201 +Created +(application/json) +Body + + { + "id": 106092, + "user_id": 123456, + "title": "Buying a unicycle", + "start": 1494770400, + "finish": 1494856800, + "repeating": false, + "repeating_info": null, + "all_day": true + } + +Request Create repeating Unavailability (application/json) +Body + + { + "user_id": 123456, + "title": "Learning to ride my unicycle", + "start": 1459382400, + "finish": 1459389600, + "repeating": true, + "repeating_info": { + "interval": "day", + "occurrences": 3 + } + } + +201 +Created +(application/json) +Body + + { + "id": 106093, + "user_id": 123456, + "title": "Learning to ride my unicycle", + "start": 1459382400, + "finish": 1459389600, + "repeating": true, + "repeating_info": { + "interval": "day", + "occurrences": 3, + "start": 1459382400, + "ids": [ + 106093, + 106094, + 106095 + ] + }, + "all_day": false + } + +Repeating Unavailability List [/api/v2/unavailability/repeating_for/{id}] +GETGet Repeating Unavailabilities +This method requires the unavailability scope (see Scopes for more information). +Parameters +id: 43 (number) - The id of the unavailability to get repeating information for +200 +OK +(application/json) +Attributes: +(FirstRepeatingUnavailabilityData) +(SecondRepeatingUnavailabilityData) +Unavailability [/api/v2/unavailability/{id}] +GETGet Unavailability +This method requires the unavailability scope (see Scopes for more information). +Parameters +id: 42 (number) - The id of unavailability to lookup +Request Non-repeating unavailability + +200 +OK +(application/json) +Body + + { + "id": 42, + "user_id": 123456, + "title": "Doing errr... something", + "start": 1459814400, + "finish": 1459836000, + "repeating": false, + "repeating_info": null, + "all_day": false + } + +Schema + +Attributes: +id: 43 (required) (number) - The id of the unavailability +user_id: 123456 (required) (number) - The user that the unavailability applies for +title: Doing errr… something else (string) - The reason for the unavailability +start: 1459994400 (required) (number) - The start time for the unavailability +finish: 1460001600 (required) (number) - The end time for the unavailability +repeating: true (required) (boolean) - Whether this is part of a set of repeating unavailabilities +repeating_info (RepeatingUnavailabilityInfoData) - Info about the unavailabilities that are part of this repeating set of unavailabilities (if the unavailability is repeating) +id: (number) +Request Repeating unavailability + +200 +OK +(application/json) +Attributes: +id: 43 (required) (number) - The id of the unavailability +user_id: 123456 (required) (number) - The user that the unavailability applies for +title: Doing errr… something else (string) - The reason for the unavailability +start: 1459994400 (required) (number) - The start time for the unavailability +finish: 1460001600 (required) (number) - The end time for the unavailability +repeating: true (required) (boolean) - Whether this is part of a set of repeating unavailabilities +repeating_info (RepeatingUnavailabilityInfoData) - Info about the unavailabilities that are part of this repeating set of unavailabilities (if the unavailability is repeating) +PUTUpdate Unavailability +Update the unavailability’s title (in the case of repeating unavailability, all of the event’s titles will be updated). + +This method requires the unavailability scope (see Scopes for more information). +Parameters +id: 42 (number) - The id of unavailability to lookup +Request Update Unavailability (application/json) +Body + + { + "title": "Helping Grandma" + } + +200 +OK +(application/json) +Body + + { + "id": 42, + "user_id": 123456, + "title": "Helping Grandma", + "start": 1459814400, + "finish": 1459836000, + "repeating": false, + "repeating_info": null, + "all_day": false + } + +Schema + +Attributes: +id: 43 (required) (number) - The id of the unavailability +user_id: 123456 (required) (number) - The user that the unavailability applies for +title: Doing errr… something else (string) - The reason for the unavailability +start: 1459994400 (required) (number) - The start time for the unavailability +finish: 1460001600 (required) (number) - The end time for the unavailability +repeating: true (required) (boolean) - Whether this is part of a set of repeating unavailabilities +repeating_info (RepeatingUnavailabilityInfoData) - Info about the unavailabilities that are part of this repeating set of unavailabilities (if the unavailability is repeating) +id: (number) +DELETEDelete Unavailability +This method requires the unavailability scope (see Scopes for more information). +Parameters +id: 42 (number) - The id of the leave to delete +200 +OK +(application/json) +Data Streams +Our user guide has more information on working with data streams through the API. +A data stream is a container for many Store Stats. The data stream specifies the origin of the data, an identifier for the origin (if any), and the interval of the data. All store stats added to a data stream should be totaled data for a time period equal to the data_interval. + +Data streams themselves are nice for holding data, but they are made useful by joining them to other objects. See Data Stream Joins for more on how this works. The Data Streams endpoint provides some functionality for quickly creating joins, but you should use the Data Stream Joins endpoint if you need more control over their structure or additional attributes. + +Team managers can read data streams that are joined to their team. General managers and payroll officers can read all data streams, and admins can create read and update all data streams. +Data Stream List [/api/v2/datastreams] +GETGet Data Streams +This method requires the datastream scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified data streams +Response 200 (application/json) Attributes (array): + ++ (GenericDatastreamData) ++ (UserEnteredDatastreamData) ++ (RetailExpressDatastreamData) + +POSTCreate Data Stream +You cannot create more than one data stream via the API with the same name. If you try to create a data stream with a name that already exists, that data stream will instead be updated and returned (effectively this will function as a PUT request). + +The data interval must be either 86400, 3600, 1800, or 900. This corresponds to 1 day, 1 hour, 30 minutes, or 15 minutes. + +It’s highly recommended you give a data stream a ‘default_stat_type’. Having this means that when the data stream is joined to locations or teams, the correct data is joined. Without this field you may see some unexpected results. For example, if your data stream houses ‘sales’ and ‘transactions’, and you mainly want to use the ‘sales’ data, you should set the default_stat_type to ‘sales’ + +This method requires the datastream scope (see Scopes for more information). +Request Create Data Stream (application/json) +Body + + { + "name": "My Special Data", + "data_interval": 900, + "default_stat_type": "sales" + } + +200 +OK +(application/json) +Attributes: +id: 1069 (required) (number) - The id of the data stream +name: My Special Data (required, string) - The name of the data stream +source: api (string) - The data stream’s source +data_interval: 900 (required) (number) - The time between the data stream’s store stats in seconds +print_mode: hidden (optional, string) - Should the data stream be printed when printing a roster? Options: hidden, values, cumulative_sum +roster_display_mode: values (optional, string) - Should the data stream be displayed on rosters? Options: hidden, values, cumulative_sum +Data Stream [/api/v2/datastreams/{id}] +GETGet Data Stream +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 26 (number) - The id of the data stream to lookup +200 +OK +(application/json) +Attributes: +id: 26 (required) (number) - The id of the data stream +name: Reactor Revenue (Retail Express) (required) (string) - The name of the data stream +source: retail_express (required) (string) - The data stream’s source +section_identifier: 11223344 (string) - A way to map to the external data provider +data_interval: 900 (required) (number) - The time between the data stream’s store stats in seconds +print_mode: hidden (optional, string) - Should the data stream be printed when printing a roster? Options: hidden, values, cumulative_sum +roster_display_mode: values (optional, string) - Should the data stream be displayed on rosters? Options: hidden, values, cumulative_sum +PUTUpdate Data Stream +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 26 (number) - The id of the data stream to update +Request Update Data Stream (application/json) +Body + + { + "name": "Foot Traffic!", + } + +200 +OK +(application/json) +Attributes: +id: 1069 (required) (number) - The id of the data stream +name: My Special Data (required, string) - The name of the data stream +source: api (string) - The data stream’s source +data_interval: 900 (required) (number) - The time between the data stream’s store stats in seconds +print_mode: hidden (optional, string) - Should the data stream be printed when printing a roster? Options: hidden, values, cumulative_sum +roster_display_mode: values (optional, string) - Should the data stream be displayed on rosters? Options: hidden, values, cumulative_sum ++ name: Foot Traffic! + +DELETEDelete Data Stream +Only data streams created via the API can be deleted via the API. This also deletes all associated Data Stream Joins and Store Stats, and cannot be reversed. + +The data stream will be scheduled for deletion, but may not be removed immediately. You can GET the data stream to check if it’s been deleted. +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 26 (number) - The id of the data stream +202 +Accepted +(application/json) +Data Stream Versions [/api/v2/datastreams/{id}/versions] +GETGet Data Stream Versions +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 26 (number) - The id of the data stream +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified data stream versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Datastream” (string) +changes (array) +(object) +field: data_interval +previous: 900 (number) +updated: 1800 (number) +(object) +field: name +previous: Employee Superfund +updated: Reactor Revenue +Data Stream Joins +Our user guide has more information on working with data streams through the API. +Data Stream Joins connect Data Streams to the object which owns the stream’s data. For example, data from a stream may be relate to a Team, Location, or to the Organisation itself. + +If a data stream is joined to the organisation, it will show its store stats in the context of the whole organisation. Similarly, if it is joined to one or many teams, it will show its store stats when looking at stats for each of those teams. Data streams that are joined to a team, but not the organisation, will show their store stats in the context of the teams, but will not show when looking at the organisation as a whole (this is often useful for team specific stats). + +Data Stream Joins also connect a single ‘stat_type’ from a data stream. You should specify which stat_type you want to join from the data stream to the Organisation, Location, or Department. For example, your data stream might have both ‘sales’ data and ‘transaction’ data in it. If you want to attach sales data to a team, then you must specify the stat_type_id for sales in the payload. + +If you do not specify a stat_type, one will be inferred by what kind of data your datastream has. Eg if your data stream only has sales data in it, the join will automatically be given the sales stat type. + +Only admins can manage data stream joins. +As well as managing this relationship, a Data Stream Join can also be given a rostering ratio, which is used to calculate how many staff should be rostered into the join’s object (the “streamable”) based on stream data. For example, a data stream might have a total value of 30 (values themselves are provided using Store Stats) for a half hour period, and the data stream’s join to a team might have a rostering ratio of 8. In this case Workforce would recommend that 4 staff be rostered for that half hour period. + +Data Stream Join List [/api/v2/datastreamjoins] +GETGet Data Stream Joins +This method requires the datastream scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified data stream joins +200 +OK +(application/json) +Attributes (array): +id: 162 (required) (number) - The id of the data stream join +data_stream_id: 24 (required) (number) - The id of the data stream join’s data stream +data_streamable_id: 111 (required) (number) - The id of the data stream join’s data streamable (location or team) +data_streamable_type: Department (required) (string) - The type of the data stream join’s data streamable (“Location”, “Department”, or “Organisation”) +rostering_ratio: 1.175 (number) - The ratio of units to staff required when rostering to this data stream +POSTCreate Data Stream Join +This method requires the datastream scope (see Scopes for more information). +Request Create Joined to Team (application/json) +Body + + { + "data_stream_id": 24, + "data_streamable_id": 111, + "data_streamable_type": "Department", + "rostering_ratio": 2.5, + "stat_type_id": 123 + } + +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +data_stream_id: 24 (required) (number) - The id of the data stream join’s data stream +data_streamable_id: 111 (required) (number) - The id of the data stream join’s data streamable (location or team) +data_streamable_type: Department (required) (string) - The type of the data stream join’s data streamable (“Location”, “Department”, or “Organisation”) +rostering_ratio: 1.175 (number) - The ratio of units to staff required when rostering to this data stream ++ data_streamable_id: 111 (number) ++ data_streamable_type: Department (string) ++ rostering_ratio: 2.5 (number) ++ stat_type_id: 123 (number) + +Request Create Joined to Organisation (application/json) +Body + + { + "data_stream_id": 24, + "data_streamable_type": "Organisation", + "stat_type_id": 123 + } + +200 +OK +(application/json) +Body + + { + "id": 162, + "data_stream_id": 24, + "data_streamable_id": null, + "data_streamable_type": "Organisation", + "rostering_ratio": 1.175, + "stat_type_id": 123 + } + +Data Stream Join [/api/v2/datastreamjoins/{id}] +GETGet Data Stream Join +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the data stream join to lookup +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +data_stream_id: 24 (required) (number) - The id of the data stream join’s data stream +data_streamable_id: 111 (required) (number) - The id of the data stream join’s data streamable (location or team) +data_streamable_type: Department (required) (string) - The type of the data stream join’s data streamable (“Location”, “Department”, or “Organisation”) +rostering_ratio: 1.175 (number) - The ratio of units to staff required when rostering to this data stream +PUTUpdate Data Stream Join +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the data stream join to update +Request Update Data Stream Join (application/json) +Body + + { + "rostering_ratio": 1.5 + } + +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +data_stream_id: 24 (required) (number) - The id of the data stream join’s data stream +data_streamable_id: 111 (required) (number) - The id of the data stream join’s data streamable (location or team) +data_streamable_type: Department (required) (string) - The type of the data stream join’s data streamable (“Location”, “Department”, or “Organisation”) +rostering_ratio: 1.175 (number) - The ratio of units to staff required when rostering to this data stream ++ rostering_ratio: 1.5 (number) + +DELETEDelete Data Stream Join +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the data stream join to delete +200 +OK +(application/json) +Store Stats +Our user guide has more information on working with data streams through the API. +Store stats are the individual statistics that belong to a Data Stream. All store stats for a data stream should be totaled data for a time period equal to the data stream’s data_interval, starting at the store stat’s time. + +Only store stats where the type parameter is equal to “sales” will be displayed on the weekly planner on the dashboard. You’re able to store a wide variety of stats in Workforce, and differentiate them using the type parameter, but be aware of this special case. + +Store stats can be read by a user, if that user is able to read the data stream that the store stat belongs to. +Only admins can create store stats. They are able to do this for any data stream. + +Store Stats for Datastream [/api/v2/storestats/for_datastream/{datastream_id}/] +GETStore Stats for Datastream +This method requires the datastream scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +datastream_id: 26 (number) - The id of the data stream to lookup store stats for +from: 2016-03-01 (string) - The start date of the range to lookup store stats in +to: 2016-03-20 (string) - The id of the data stream to lookup store stats for +type: sales (optional, string) - The type of store stats to lookup (if not specified, all stats are returned) +200 +OK +(application/json) +Attributes (array): +id: 3518 (required) (number) - The id of the stat +time: 1459295100 (required) (number) - The time of the stat +stat: 3.50 (required) (number) - The value of the stat +type: sales (required) (string) - The type of the stat +datastream_id: 24 (required) (number) - The id of the data stream join’s data stream +Create Store Stats for Datastream [/api/v2/storestats/for_datastream/{datastream_id}] +POSTCreate Store Stats for Datastream +Store stats can either be posted one at a time, or in bulk. When posting store stats in bulk, all the store stats must be for the same data stream. + +To post store stats in bulk, simply represent them all in an array, under the stats key. Note that each combination of time and type must be unique - you must not send duplicate data. + +POSSIBLE DATA DELETION: All existing store stats on the data stream within the newly posted store stats’ time parameter range will be replaced with the newly posted store stats. +Please see the general Store Stat information above for an example. + +Pass the optional top-level update_tips parameter as true to recompute any tip jars attached to this data stream across the affected date range once the store stats are saved. It defaults to false, so tip jar amounts are only refreshed when you explicitly opt in. +{ "update_tips": true, "stats": [ { "time": 1459296900, "stat": 3.5, "type": "sales" }, { "time": 1459297800, "stat": 1, "type": "transactions" } ] } + +This method requires the datastream scope (see Scopes for more information). +Parameters +datastream_id: 27 (number) - The id of the data stream to create store stats on +Request Create single Store Stat (application/json) +Body + + { + "time": 1459296900, + "stat": 3.5, + "type": "sales" + } + +201 +Created +(application/json) +Attributes: +id: 3518 (required) (number) - The id of the stat +time: 1459295100 (required) (number) - The time of the stat +stat: 3.50 (required) (number) - The value of the stat +type: sales (required) (string) - The type of the stat +datastream_id: 24 (required) (number) - The id of the data stream join’s data stream ++ id: 12231 (number) ++ time: 1459296900 (number) ++ stat: 3.5 (number) ++ type: sales (string) + +Request Create multiple Store Stats (application/json) +Body + + { + "stats": [ + { + "time": 1459296900, + "stat": 3.5, + "type": "sales" + }, + { + "time": 1459296900, + "stat": 1, + "type": "transactions" + }, + { + "time": 1459297800, + "stat": 20.1, + "type": "sales" + } + ] + } + +Response 201 (application/json) Attributes (array): + ++ (object) + + id: 12231 (number) + + time: 1459296900 (number) + + stat: 3.5 (number) + + type: sales (string) ++ (object) + + id: 12232 (number) + + time: 1459296900 (number) + + stat: 1 (number) + + type: transactions (string) ++ (object) + + id: 12233 (number) + + time: 1459297800 (number) + + stat: 20.1 (number) + + type: sales (string) + +Deleting Store Stats [/api/v2/storestats/for_datastream/{datastream_id}] +POSTDeleting Store Stats +To remove store stats generated through the API for a time period, you will need to make a POST, and provide store stats with a 0 as the stat for the start and end of that period. + +For example, if you wanted to remove sales and transactions store stats for the period of 1459296900 to 1459383300 then posting the following data to the Create Store Stats endpoint would “delete” that data. This is becuase each post actually overrides existing store stat data for the period. { "stats": [ { "time": 1459296900, "stat": 0, "type": "sales" }, { "time": 1459296900, "stat": 0, "type": "transactions" }, { "time": 1459383300, "stat": 0, "type": "sales" }, { "time": 1459383300, "stat": 0, "type": "transactions" } ] } + +Create Store Stats for multiple Datastreams [/api/v2/storestats/for_datastream] +POSTCreate Store Stats for Multiple Datastreams +Store stats can either be posted one at a time, or in bulk. When posting store stats in bulk, all the store stats must be for the same data stream. + +To post store stats in bulk, simply represent them all in an array, with the datastream_id included for each stat. Note that each combination of datastream_id, time and type must be unique - you must not send duplicate data. + +POSSIBLE DATA DELETION: All existing store stats on the data stream within the newly posted store stats’ time parameter range will be replaced with the newly posted store stats. +Please see the general Store Stat information above for an example. + +Pass the optional top-level update_tips parameter as true to recompute the tip jars attached to each posted data stream across that stream’s affected date range once the store stats are saved. It defaults to false, so tip jar amounts are only refreshed when you explicitly opt in. +{ "update_tips": true, "stats": [ { "datastream_id": 123456, "time": 1459296900, "stat": 7.5, "type": "sales" }, { "datastream_id": 123456, "time": 1459297800, "stat": 3, "type": "transactions" } { "datastream_id": 123457, "time": 1459296900, "stat": 3.5, "type": "sales" }, { "datastream_id": 123457, "time": 1459297800, "stat": 1, "type": "transactions" } ] } + +Request Create single Store Stat (application/json) +Body + + { + "datastream_id": 123457, + "time": 1459296900, + "stat": 3.5, + "type": "sales" + } + +201 +Created +(application/json) +Attributes: +id: 3518 (required) (number) - The id of the stat +time: 1459295100 (required) (number) - The time of the stat +stat: 3.50 (required) (number) - The value of the stat +type: sales (required) (string) - The type of the stat +datastream_id: 24 (required) (number) - The id of the data stream join’s data stream ++ id: 12231 (number) ++ datastream_id: 123457 (number), ++ time: 1459296900 (number) ++ stat: 3.5 (number) ++ type: sales (string) + +Request Create multiple Store Stats (application/json) +Body + + { + "stats": [ + { + "datastream_id": 123456, + "time": 1459296900, + "stat": 7.5, + "type": "sales" + }, + { + "datastream_id": 123456, + "time": 1459296900, + "stat": 3, + "type": "transactions" + }, + { + "datastream_id": 123457, + "time": 1459297800, + "stat": 3.5, + "type": "sales" + }, + { + "datastream_id": 123457, + "time": 1459297800, + "stat": 1, + "type": "transactions" + } + } + ] + } + +Response 201 (application/json) Attributes (array): + ++ (object) + + id: 12231 (number) + + datastream_id: 123456 (number), + + time: 1459296900 (number) + + stat: 7.5 (number) + + type: sales (string) ++ (object) + + id: 12232 (number) + + datastream_id: 123456 (number), + + time: 1459296900 (number) + + stat: 3 (number) + + type: transactions (string) ++ (object) + + id: 12233 (number) + + datastream_id: 123457 (number), + + time: 1459297800 (number) + + stat: 3.5 (number) + + type: sales (string) ++ (object) + + id: 12234 (number) + + datastream_id: 123457 (number), + + time: 1459297800 (number) + + stat: 1 (number) + + type: transactions (string) + +Devices +Devices are physical time clocks that are used by employees for clocking in and out, a device has many Clock Ins. + +Admins and department managers can read devices, and admins can and update all devices. +Device List [/api/v2/devices] +GETGet Devices +This method requires the device scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified devices +200 +OK +(application/json) +Attributes (array): +id: 1234 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: false (required) (boolean) - Whether the device has been returned +last_heard_from: 1459296900 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2016-01-02 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +POSTCreate Device +This method requires the device scope (see Scopes for more information). +Provide one or both of location_id and nickname when registering a time clock. If a location_id is provided, but a nickname isn’t, the location’s name is used as a nickname. + +Request Create new timeclock (application/json) +Body + + { + "nickname": "Old Reactor Timeclock", + "location_id": 111 + } + +200 +OK +(application/json) +Attributes: +id: 1234 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: false (required) (boolean) - Whether the device has been returned +last_heard_from: 1459296900 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2016-01-02 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +nickname: Old Reactor Timeclock +Returned Device List [/api/v2/devices/returned] +GETGet Returned Devices +This method requires the device scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified returned devices +200 +OK +(application/json) +Attributes (array): +id: 8403 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Meltdown Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: true (required) (boolean) - Whether the device has been returned +return_date: 2016-03-02 (required) (string) - The date the device was returned +last_heard_from: 1456877652 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2015-10-12 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +Device [/api/v2/devices/{id}] +GETGet Device +This method requires the device scope (see Scopes for more information). +Parameters +id: 1234 (number) - The id of the device to lookup +Request Active device + +200 +OK +(application/json) +Attributes: +id: 1234 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: false (required) (boolean) - Whether the device has been returned +last_heard_from: 1459296900 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2016-01-02 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +Request Returned device + +200 +OK +(application/json) +Attributes: +id: 8403 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Meltdown Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: true (required) (boolean) - Whether the device has been returned +return_date: 2016-03-02 (required) (string) - The date the device was returned +last_heard_from: 1456877652 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2015-10-12 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +PUTUpdate Device +This method requires the device scope (see Scopes for more information). +Parameters +id: 1234 (number) - The id of the device to edit +Request Update device information (application/json) +Body + + { + "nickname": "Old Reactor Timeclock", + "location_id": 111 + } + +200 +OK +(application/json) +Attributes: +id: 1234 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: false (required) (boolean) - Whether the device has been returned +last_heard_from: 1459296900 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2016-01-02 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +nickname: Old Reactor Timeclock +Return Device [/api/v2/devices/{id}/return] +POSTReturn Device +This method requires the device scope (see Scopes for more information). +Parameters +id: 1234 (number) - The id of the device to return +200 +OK +(application/json) +Attributes: +id: 1234 (required) (number) - The id of the device +location_id: 111 (optional, number) - The ID of the time clock’s location. +nickname: Reactor Timeclock (required, string) - The nickname of the device. If not provided, the location’s name is used. +returned: false (required) (boolean) - Whether the device has been returned +last_heard_from: 1459296900 (number) - The time that the device last communicated with the Tanda server +dispatch_date: 2016-01-02 (string) - The date that the device was sent out +location_specific: false (required) (boolean) - Whether the device is specific to the assigned location or not +returned: true +return_date: 2016-04-01 (string) - The date the device was returned +Device Versions [/api/v2/devices/{id}/versions] +GETGet Device Versions +This method requires the device scope (see Scopes for more information). +Parameters +id: 1234 (number) - The id of the device +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified device versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Device” (string) +changes (array) +(object) +field: nickname +previous: Breakroom +updated: Reactor Timeclock +(object) +field: location_id +previous: 111 (number) +updated: 112 (number) +Device Access Code [/api/v2/devices/reset_access_code] +POSTReset Device Access Code +This method requires the device scope (see Scopes for more information). +The access code is used by admins when setting up a device as an alternative to entering their username and password. + +200 +OK +(application/json) +Attributes: +access_code: 12345678 (required) (string) - Randomly generated 8 digit string +Clock Ins +Clock ins represent actual times that the employee arrived and left from a location (most often a work site or office). + +You must be able to manage the user to make a clock in for them. To read clock ins for a user or device, the user must be able to manage the the user, or read the device respectively. +Clock In [/api/v2/clockins] +POSTPerform Clock In for User +Use this endpoint to send clock ins to Workforce in “real time” as employees clock in or out of work. The clock in will be stored as a distinct object in Workforce, but will also be copied to a Shift, which will then be visible on a Timesheets. When posting clockins to Workforce, here’s a few things to be aware of: + +Time rounding +It is possible in Workforce to configure rounding settings, to do things like round clock in times to the nearest 5 minutes. At the very least, the clock in time will be rounded to the floor of the minute when processed. Therefore, you should not expect that you will find a Shift in Workforce with exactly the same start or end time as the time that you provide here. Employees in Workforce are paid based on the output of Timesheets & Shifts, not of Clock Ins, so if you want to adjust a clocked time later you should adjust the relevant Shift instead. + +De-duplication +If two very similar clock ins are posted one after the other (eg. two clock ins with the same type, and times within a few minutes of each other) then it is possible that Workforce will discard one. The intent of this logic is to prevent issues like people “misclicking” on physical time clocks. If you want complete control over how a Shift will be displayed in Workforce, use the Shifts endpoint. When querying for Clock Ins directly, any Clock In with true in its removed field has been removed from the Shift as another Clock In has replaced it. + +Non-immediate processing +This endpoint will return a 201 HTTP status with the newly created Clock In if the request was valid. At this point the shift_id will be null. There will be a short delay between the Clock In being created, and it being processed. This processing entails adding the Clock In to a Timesheet, and recalculating the Timesheet’s costs. Once the Clock In has been processed, the shift_id field will be updated to be the ID of the Shift that the Clock In was applied to. During processing, if the Clock In is considered to be a duplicate, or needs to be discarded for any other reason, it may never be applied to a Shift. If this is the case, the removed field will be true. + +Clock Ins are copied to timesheets +Use the Timesheets and Shifts endpoints to see the net result of your Clock Ins on staff timesheets. You can lookup clock ins using the Get Clock Ins endpoint, and once the clock in has been processed, you can use its shift_id to lookup the shift it applied to. Additionally, you can use either the Clock Ins for User, or the Clock Ins for Device methods, to view raw Clock Ins created through this endpoint. + +Clock In image format +Images on Clock Ins are optional. However, if you wish to include an image with a Clock In, the image must be a base64 PNG or JPEG using the Data URI scheme. An example encoded image can be seen below: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg== If you do choose to provide an image, and the image isn’t in the correct format, you will receive a response with a 400 HTTP status. An example of the response can be seen on the right. + +Don’t predict the future +Clock Ins with times more than 10 minutes into the future (as computed at the server’s time) will be rejected. Ensure your client’s time is correct. + +The following fields are supported when clocking in or out. Examples of all these fields can be seen in the sidebar to the right. + +user_id - required, integer - the ID of the User clocking in or out. +type - required, string - must be either start, finish, break_start, or break_finish. +time - required, integer - a unix timestamp representing when the clock in or out happened. This time cannot be in the future. +device_id - required, integer - the ID of the Time Clock used to clock in or out. Used to verify the location the clock in took place at. +photo - optional, string - a photo of the user clocking in. If provided, must be a base64 encoded PNG or JPEG image. The size of the PNG image (decoded) must not exceed 5MB in size. +department_id - optional, integer - the Team being clocked in to. If provided, the User must already be in this Team (otherwise it will be ignored). This will show on the user’s timesheet. +answers - optional, array of objects - one or more answers to timeclock questions. For each object, a timeclock_question_id, trigger, and answer are required. There is no validation performed on this - if your answer is invalid, it will be silently ignored. If you need validation, send a POST to the timeclock question answers endpoint. +This method requires the device scope (see Scopes for more information). +You will only be able to clock in for users you can manage, on devices you can see. +Request Clock In (application/json) +Body + + { + "user_id": 123456, + "type": "start", + "time": 1459296192 + } + +201 +Created +(application/json) +Body + + { + "id": 6108, + "user_id": 123456, + "device_id": 1234, + "time": 1459296192, + "type": "start", + "latitude": null, + "longitude": null, + "photo": null, + "department_id": null, + "shift_id": null, + "removed": false + } + +Request Clock Out (application/json) +Body + + { + "user_id": 123456, + "type": "finish", + "time": 1459296192 + } + +201 +Created +(application/json) +Body + + { + "id": 6108, + "user_id": 123456, + "device_id": 1234, + "time": 1459296192, + "type": "finish", + "latitude": null, + "longitude": null, + "photo": null, + "department_id": null, + "shift_id": null, + "removed": false + } + +Request Specific Photo & Time Clock (application/json) +Body + + { + "user_id": 123456, + "type": "start", + "time": 1459296192, + "device_id": 1234, + "photo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAaCAI..." + } + +201 +Created +(application/json) +Body + + { + "id": 6108, + "user_id": 123456, + "device_id": 1234, + "time": 1459296192, + "type": "start", + "latitude": null, + "longitude": null, + "photo": "http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png", + "department_id": null, + "shift_id": null, + "removed": false + } + +Request Specific Team (application/json) +Body + + { + "user_id": 123456, + "type": "start", + "time": 1459296192, + "department_id": 808 + } + +201 +Created +(application/json) +Body + + { + "id": 6108, + "user_id": 123456, + "device_id": 1234, + "time": 1459296192, + "type": "start", + "latitude": null, + "longitude": null, + "photo": null, + "department_id": 808, + "shift_id": null, + "removed": false + } + +Request Bad Image (application/json) +Body + + { + "user_id": 123456, + "type": "start", + "time": 1459296192, + "department_id": 808, + "photo": "non-base64 png string" + } + +400 +Bad Request +(application/json) +Body + + { + "error": "Photo parameter is not a valid base64 string!" + } + +Request Image Too Large (application/json) +Body + + { + "user_id": 123456, + "type": "start", + "time": 1459296192, + "department_id": 808, + "photo": "very large base64 string" + } + +400 +Bad Request +(application/json) +Body + + { + "error": "Photo is too large. File size limit is 5 MB." + } + +Request Answer to TC Question (application/json) +Body + + { + "user_id": 123456, + "type": "finish", + "time": 1459296192, + "answers": [{ "timeclock_question_id": 42, "answer": 63.4, "trigger": "finish" }] + } + +201 +Created +(application/json) +Body + + { + "id": 6108, + "user_id": 123456, + "device_id": 1234, + "time": 1459296192, + "type": "start", + "latitude": null, + "longitude": null, + "photo": null, + "department_id": null, + "shift_id": null, + "removed": false + } + +Clock In List [/api/v2/clockins] +GETClock Ins for User +Both from and to are required, and at least one of user_id and device_id are required. + +To get a list of users who are clocked in right now, use the Get Clocked In Users endpoint. + +This method requires the device scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +user_id: 123456 (number) - The id of the user to lookup clock ins for +from: 2016-03-15 (required, string) - From date to lookup clock ins within +to: 2016-04-05 (required, string) - To date to lookup clock ins within +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified clock ins +include_inactive: true (optional, boolean) - User filter to return data for inactive staff +200 +OK +(application/json) +Attributes (array): +id: 6108 (required) (number) - The id of the clock in +user_id: 123456 (required) (number) - The id of the user clocking in +device_id: 1234 (optional, number) - The id of the time clock the user clocks in using. If not provided a generic time clock record will be used. +time: 1459209907 (required) (number) - The time the clock in was made +type: start (required) (string) - start, finish, break_start, or break_finish +latitude: -27.459687 (number) - The latitude of the clock in +longitude: 153.032108 (number) - The longitude of the clock in +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - The photo the device took when the user clocked in +department_id: 808 (number) - The id of the department the clock in (if clock in was for a specific department) +shift_id: 1337 (number) - The id of the shift the clock in got applied to (will be nil until the clock in has been processed) +removed: false (required) (boolean) - True if the clock in was removed from the shift due to de-duplication or another reason, see the Clock In endpoint for more information +GETClock Ins for Device +Both from and to are required, and at least one of user_id and device_id are required. + +This method requires the device scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +device_id: 1234 (number) - The id of the device to lookup clock ins for +from: 2016-03-15 (required, string) - From date to lookup clock ins within +to: 2016-04-05 (required, string) - To date to lookup clock ins within +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified clock ins +200 +OK +(application/json) +Attributes (array): +id: 6108 (required) (number) - The id of the clock in +user_id: 123456 (required) (number) - The id of the user clocking in +device_id: 1234 (optional, number) - The id of the time clock the user clocks in using. If not provided a generic time clock record will be used. +time: 1459209907 (required) (number) - The time the clock in was made +type: start (required) (string) - start, finish, break_start, or break_finish +latitude: -27.459687 (number) - The latitude of the clock in +longitude: 153.032108 (number) - The longitude of the clock in +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - The photo the device took when the user clocked in +department_id: 808 (number) - The id of the department the clock in (if clock in was for a specific department) +shift_id: 1337 (number) - The id of the shift the clock in got applied to (will be nil until the clock in has been processed) +removed: false (required) (boolean) - True if the clock in was removed from the shift due to de-duplication or another reason, see the Clock In endpoint for more information +GETClock Ins for User on Device +Both from and to are required, and at least one of user_id and device_id are required. + +This method requires the device scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +user_id: 123456 (number) - The id of the user to lookup clock ins for +device_id: 1234 (number) - The id of the device to lookup clock ins for +from: 2016-03-15 (required, string) - From date to lookup clock ins within +to: 2016-04-05 (required, string) - To date to lookup clock ins within +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified clock ins +200 +OK +(application/json) +Attributes (array): +id: 6108 (required) (number) - The id of the clock in +user_id: 123456 (required) (number) - The id of the user clocking in +device_id: 1234 (optional, number) - The id of the time clock the user clocks in using. If not provided a generic time clock record will be used. +time: 1459209907 (required) (number) - The time the clock in was made +type: start (required) (string) - start, finish, break_start, or break_finish +latitude: -27.459687 (number) - The latitude of the clock in +longitude: 153.032108 (number) - The longitude of the clock in +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - The photo the device took when the user clocked in +department_id: 808 (number) - The id of the department the clock in (if clock in was for a specific department) +shift_id: 1337 (number) - The id of the shift the clock in got applied to (will be nil until the clock in has been processed) +removed: false (required) (boolean) - True if the clock in was removed from the shift due to de-duplication or another reason, see the Clock In endpoint for more information +Clock In [/api/v2/clockins/{id}] +GETGet Clock In +For information about how clock ins work, please see the Perform Clock In for User endpoint. + +This method requires the device scope (see Scopes for more information). +You will only be able to see clock ins for users you can manage, or for devices you can see. +Parameters +id: 6108 (number) - The id of the clockin to lookup +200 +OK +(application/json) +Attributes: +id: 6108 (required) (number) - The id of the clock in +user_id: 123456 (required) (number) - The id of the user clocking in +device_id: 1234 (optional, number) - The id of the time clock the user clocks in using. If not provided a generic time clock record will be used. +time: 1459209907 (required) (number) - The time the clock in was made +type: start (required) (string) - start, finish, break_start, or break_finish +latitude: -27.459687 (number) - The latitude of the clock in +longitude: 153.032108 (number) - The longitude of the clock in +photo: http://vignette1.wikia.nocookie.net/family-guyamerican-dadthe-simpsons-and-futurama/images/f/ff/250px-Lenny_Leonard.png (string) - The photo the device took when the user clocked in +department_id: 808 (number) - The id of the department the clock in (if clock in was for a specific department) +shift_id: 1337 (number) - The id of the shift the clock in got applied to (will be nil until the clock in has been processed) +removed: false (required) (boolean) - True if the clock in was removed from the shift due to de-duplication or another reason, see the Clock In endpoint for more information +Qualifications +The Qualifications Feature needs to be enabled before you can access this endpoint. +Qualifications List [/api/v2/qualifications] +GETGet Qualifications +This method requires the qualifications scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 123706 (number, required) - The qualification ID +name: Blue Card (string, required) - The name of the qualification +maximum_hours: 21.34 (number) - Maximum number of hours that can be worked per week if this qualification is enabled +POSTCreate Qualification +Once created, a qualification will be visible in your qualifications list. Use its ID to add it to staff profiles, and to teams. + +This method requires the qualifications scope (see Scopes for more information). +Request (application/json) +Body + + { + "name": "Responsible Service of Alcohol", + "maximum_hours": 20.5 + } + +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The qualification ID +name: Blue Card (string, required) - The name of the qualification +maximum_hours: 21.34 (number) - Maximum number of hours that can be worked per week if this qualification is enabled ++ name: Responsible Service of Alcohol ++ maximum_hours: 20.5 + +Qualification [/api/v2/qualifications/{id}] +GETGet Qualification +This method requires the qualifications scope (see Scopes for more information). +Parameters +id: 123706 (number) - The id of the qualification +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The qualification ID +name: Blue Card (string, required) - The name of the qualification +maximum_hours: 21.34 (number) - Maximum number of hours that can be worked per week if this qualification is enabled +PUTUpdate Qualification +This method requires the qualifications scope (see Scopes for more information). +Parameters +id: 123706 (number) - The id of the qualification to edit +Request (application/json) +Body + + { + "name": "RSA v2" + } + +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The qualification ID +name: Blue Card (string, required) - The name of the qualification +maximum_hours: 21.34 (number) - Maximum number of hours that can be worked per week if this qualification is enabled ++ name: RSA v2 + +DELETEDelete Qualification +Deleting a qualification will also remove it from associated staff and teams. + +This method requires the qualifications scope (see Scopes for more information). +Parameters +id: 123706 (number) - The id of the qualification +200 +OK +(application/json) +System Settings +System settings are accessible through the API if they are visible on the Workforce settings page. Please refer to the settings page inside Workforce for up to date documentation on each setting and the options available. + +Only admins are able to access settings. +Settings [/api/v2/settings] +GETGet Settings +This method requires the settings scope (see Scopes for more information). +The full range of properties that are returned may vary on a per-user basis. To the right is a sample, to illustrate the structure of responses. Therefore, your code should gracefully handle the condition where a particular key is not present. + +200 +OK +(application/json) +Attributes: +enable_availability: false (boolean) - Employees can use an app to enter unavailability +unavailability_minimum_date: 2016-02-29 (string) - The earliest date for which Unavailability can be entered. +enable_employee_leave: true (boolean) - Allow employees to enter leave requests through Tanda +leave_request_default_length: 7.6 (number) - Default leave request length (hours) +Settings Versions [/api/v2/settings/versions] +GETGet Settings Versions +This method requires the settings scope (see Scopes for more information). +The full range of properties that are returned may vary on a per-user basis. To the right is a sample, to illustrate the structure of responses. Therefore, your code should gracefully handle the condition where a particular key is not present. + +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified settings versions +200 +OK +(application/json) +Attributes (array): +version_id: 123456 (number) +time: 1459209907 (number) +event: update +author (object) +id: 123456 (number) +name: API v2 +item_id: 1235435 (number) +item_type: “Setting” (string) +changes (array) +(object) +field: enable_availability +previous: true (boolean) +updated: false (boolean) +(object) +field: unavailability_minimum_days +previous: 3 (number) +updated: 4 (number) +Award Templates +Use the award templates endpoint to read and update which managed awards an organisation has enabled. + +Only admins are able to access this endpoint. +Award Templates List [/api/v2/award_templates] +GETGet Enabled Award Templates +This method returns a list of award templates enabled for the API user’s organisation. + +This method requires the settings scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified award templates +include_custom_award_templates: false (optional, boolean) - If true, award template organisation will be included in the response. Defaults to false. +200 +OK +(application/json) +Attributes (array): +award_template_id: 990 (number, required) - The award template ID +name: Clerks Private Sector (string) - The name of the award or EBA +identifier: MA000002 (string) - An identifier for the award or EBA +award_template_organisation_id: 5624432 (number) - The internal ID linking this template to the organisation (if linked) +POSTEnable Award Template +Use this method to enable an award template for a particular organisation. A single field, the award_template_id, is required. This field is unique across Workforce. You can get a list of valid IDs by making a call to the available award templates method. + +This method enables the award template, then triggers a background job to populate the Workforce organisation with the relevant payroll rules and allowances. It may take up to several hours for those to be created and costing information to be updated. + +If you include the extract_leave_types parameter with a value of true, any leave types included in the template will be made user-visible and user-editable. The default is false which means that leave types will be managed by the template after it’s created. + +If you include the replace_leave_types parameter with a value of true, any existing leave types in the organisation prior to the award template’s being added will be removed. The default is false which will not alter existing leave types. + +This method requires the settings scope (see Scopes for more information). +Request Enable Award Template (application/json) +Body + + { + "award_template_id": 990, + } + +200 +OK +(application/json) +Attributes(AwardTemplateData) +award_template_id: 990 (number) +Request Enable Award Template With Leave Types Extracted (application/json) +Body + + { + "award_template_id": 990, + "extract_leave_types": true, + "replace_leave_types": true + } + +200 +OK +(application/json) +Attributes(AwardTemplateData) +award_template_id: 990 (number) +Award Templates List [/api/v2/award_templates/available] +GETGet Available Award Templates +This method returns a full list of award templates available in Workforce. + +This method requires the settings scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +award_template_id: 990 (number, required) - The award template ID +name: Clerks Private Sector (string) - The name of the award or EBA +identifier: MA000002 (string) - An identifier for the award or EBA +award_template_organisation_id: 5624432 (number) - The internal ID linking this template to the organisation (if linked) +Organisations +Use the organisations endpoint to get a list of clients you manage, and register new clients. If you have access to this endpoint, you can also get access tokens for clients you manage. + +To get access to this endpoint, please contact Workforce. +Organisation List [/api/v2/organisations] +GETGet Organisations +This method requires the organisation scope (see Scopes for more information). +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified organisations +200 +OK +(application/json) +Attributes (array): +id: 747 (required) (number) - Organisation’s unique ID +name: Tom’s Restaurant (required, string) - The organisation’s name +payroll_system: myob (string) - The payroll software used by the organisation +country: Australia (required) (string) - The country in which the organisation is legally based +locale: en (string) - The i18n locale for the organisation +time_zone: Brisbane (string) - The organisation’s time zone name +industry: The organisation’s industry +customer_ids: 748, 749 (array[number]) - Organisation IDs for customers of this organisation +POSTCreate Organisation +This method requires the organisation scope (see Scopes for more information). +The following fields can be used when creating a new organisation: + +name - required, string - the organisation’s name. +country - required, string - the country the organisation is legally established in. +timesheet_interval - required, integer - 1 for weekly timesheets, 2 for fortnightly timesheets. +week_start_day - required, integer - the day of the week that timesheets should start on. 0 is Sunday, 1 is Monday, and so on. +locale - optional, string - the language tag for system translation. Defaults to en. Click here for more information. +time_zone - optional, string - the time zone for the new organisation. If not provided, defaults to authenticated user’s time zone. Uses zone names from here, eg “Brisbane” or “Perth”. +award_template_ids - optional, array[integer] - IDs of award templates you’d like to make available to this organisation. Note that you’ll still need to enable the award template once the organisation is created. +industry - optional, string - the industry the organisation belongs to. e.g. ‘Aged Care’. +customer_ids - optional, array[integer] - IDs of other organisations which are customers of this organisation. This organisation will be able to access its customers and impersonate users within them through the Workforce UI. + +Request Create Organisation (application/json) +Body + + { + "name": "Vandelay Industries", + "country": "New Zealand", + "locale": "en-NZ", + "timesheet_interval": 1, + "week_start_day": 0, + "time_zone": "Auckland", + "award_template_ids": [1,2,3], + "customer_ids": [] + } + +200 +OK +(application/json) +Attributes(OrganisationData) +id: 748 (number) +name: Vandelay Industries +country: New Zealand +locale: en-NZ +time_zone: Auckland +customer_ids: (array[number]) +Organisation [/api/v2/organisations/{id}] +GETGet Organisation +Get an organisation by id. + +This method requires the organisation scope (see Scopes for more information). +200 +OK +(application/json) +Attributes: +id: 747 (required) (number) - Organisation’s unique ID +name: Tom’s Restaurant (required, string) - The organisation’s name +payroll_system: myob (string) - The payroll software used by the organisation +country: Australia (required) (string) - The country in which the organisation is legally based +locale: en (string) - The i18n locale for the organisation +time_zone: Brisbane (string) - The organisation’s time zone name +industry: The organisation’s industry +customer_ids: 748, 749 (array[number]) - Organisation IDs for customers of this organisation +PUTUpdate Organisation +This method requires the organisation scope (see Scopes for more information). +The following fields can be updated using this endpoint: + +name - required, string - the organisation’s name. +country - required, string - the country the organisation is legally established in. +locale - optional, string - the language tag for system translation. Defaults to en. Click here for more information. +award_template_ids - optional, array[integer] - IDs of award templates you’d like to make available to this organisation. Note that you’ll still need to enable the award template once the organisation is created. +industry - optional, string - the industry the organisation belongs to. e.g. ‘Aged Care’. +customer_ids - optional, array[integer] - IDs of other organisations which are customers of this organisation. This organisation will be able to access its customers and impersonate users within them through the Workforce UI. + +Parameters +id: 747 (number) - The ID of the organisation +Request Add Customer (application/json) +Body + + { + "customer_ids": [ + 737, + 727 + ] + } + +200 +OK +(application/json) +Attributes(OrganisationData) +customer_ids: 727, 737 (array[number]) +Request Clear Customers (application/json) +Body + + { + "customer_ids": [] + } + +200 +OK +(application/json) +Attributes(OrganisationData) +customer_ids: (array[number]) +Organisation Access Tokens [/api/oauth/token] +POSTCreate Access Token +Use this endpoint to get an access token for an organisation that you have read access to. To request a token, make a POST to /api/oauth/token - the same path you would use to request a regular Password access token. Supply these fields: + +access_token - required, string - your current valid access token (the one you use to get a list of organisations) +organisation_id - required, integer - the organisation you’d like to get an access token for - get this from the list of organisations +scope - required, string - one or more Scopes separated by spaces +grant_type - required, string - must be partner_token +The right sidebar contains an example access token reques & response. The access token in the response will give you access to the organisation ID provided in the request. + +Request Create Access Token +curl https://my.workforce.com/api/oauth/token -X POST -H "Cache-Control: no-cache" \ -F "access_token=YOUR_CURRENT_ACCESS_TOKEN" \ -F "organisation_id=747" \ -F "scope=user me" \ -F "grant_type=partner_token" + +Response 200 + + { + "access_token": "6833b9ecaa84ce420da3cafaa43124d241cb28b5287b72d131f6b38bcb64cd91", + "token_type": "bearer", + "scope": "user me", + "created_at": 1457304578 + } + +Personal Details +View & update personal details. + +All endpoints here require the personal scope. Some endpoints require the financial scope to view financial data, or the user scope to view data about an employee. +Your Personal Details [/api/v2/personal_details] +These details will apply to all accounts linked under your profile +GETGet Personal Details +Gets the personal details of the currently authenticated user + +This method requires the personal scope. +To retrieve financial information, such as a Tax File Number, you must also have the financial scope. + +200 +OK +(application/json) +Body + + { + "email": "bartsimpson@tanda.co", + "gender": "male", + "emergency_contacts": [{ + "name": "Marge Simpson", + "relationship": "Mom", + "phone": "0400 000 000" + }] + } + +PUTUpdate Personal Details +Update the currently authenticated user’s personal details. + +Any emergency_contacts provided will be appended to existing emergency contacts. +Employee’s Personal Details [/api/v2/personal_details/{user_id}] +GETGet Employee’s Personal Details +This endpoint requires the user and personal scopes. The authenticated user must also be a manager of the employee identified by user_id. The financial scope is required to see financial data. +Parameters +user_id: 123456 (number) - The id of the user +200 +OK +(application/json) +Body + + { + "email": "bartsimpson@tanda.co", + "gender": "male", + "emergency_contacts": [{ + "id": 1234, + "name": "Marge Simpson", + "relationship": "Mom", + "phone": "0400 000 000" + }] + } + +PUTUpdate Employee’s Personal Details +Update a user’s personal details. + +This endpoint requires the user and personal scopes. The authenticated user must also be a manager of the employee identified by user_id. The financial scope is required to see financial data. +Any emergency_contacts provided will be appended to existing emergency contacts. +Parameters +user_id: 123456 (number) - The ID of the user +Webhooks +Get notified when things happen inside Workforce. + +Check out our user guide to walk you through setting up a Webhook - there’s no programming knowledge required! +There are three concepts to understand when working with webhooks: + +Topics: a topic is a type of event that can happen inside Workforce, for example “user.created” or “clockin.updated” - a full list of topics can be found below. +Subscriptions: a subscription is comprised of a URL (where we send the notification payload) and the specific events in Workforce (topics) you want sent to the URL, think of these as the events you have subscribed to be notified about. +Notifications: a notification is a HTTP POST request we make to the URL you configure in the subscription. All notifications received will follow the format outlined in the notification payload section below. +Notification Payload +Headers Content-Length: 475 Content-Type: application/json X-Webhook-Signature: g7LZCBPlwBg1vid0dOHgt7HToR0= + +Payload { "hook_key": "", "hook_time": "", "payload": { "organisation_id": "", "body": {}, "topic": "" } } + +hook_key: The hook_key is a UUID associated with the webhook event, if you’re worried about duplications interfering with your integration, use this to dedup. +hook_time: The hook_time is the time the event was emitted in epoch format. +body: The contents of the payload.body depend upon what topic the subscription is for, for each topic you can generally find the payload body under the corresponding API endpoint documentation e.g. a subscription to the leaverequest.created event will have the same fields as the response to the endpoint to create a leave request. +topic: This is the topic name as detailed in the ‘Topic’ column below. +Available Topics +Topic Description +clockin.updated Sent when a clockin is updated, for example to get a shift ID attached post-processing. Not sent for when a clockin with a photo is processed, use clockin.photo for this. +clockin.photo Sent when a clockin with a photo is processed. Not all clockins have photos, so this will be a subset of clockin.updated. +department.created Sent when a team is created. +department.updated Sent when a team is updated. +department.destroyed Sent when a team is destroyed. +leaverequest.approved Sent when a leave request is approved. +leaverequest.created Sent when a leave request is created. +leaverequest.rejected Sent when a leave request is rejected. +leaverequest.updated Sent when a leave request is updated (this includes approval and rejection). +schedule.created Sent when a schedule is created (doesn't send for roster templates). +schedule.updated Sent when a schedule is changed (doesn't send for roster templates). +schedule.destroyed Sent when a schedule is deleted (doesn't send for roster templates). +schedule.published Sent when a schedule is sent to an employee by email or SMS, or when it's printed +shift.approved Sent when a shift is approved. +shift.unapproved Sent when a shift is unapproved. +shift.updated Sent when a shift is changed (includes approval). +super_fund_membership.updated Sent when a user's super fund membership is changed. +timesheet.approved Sent when a timesheet is approved. +timesheet.exported Sent when a timesheet is exported. +timesheet.costed Sent when a timesheet is recalculated due to a change elsewhere in the system. Note this is only sent if the recalculation results in a change in the total_cost field. +unavailability.created Sent when an unavailability is created. +qualification.created Sent when an qualification is created. +qualification.updated Sent when an qualification is changed. +qualification.destroyed Sent when an qualification is deleted. +user.created Sent when a new user is created, either manually, by API, or by bulk import. +user.deactivated Sent when a new user is deactivated, either manually or via the API. +user.reactivated Sent when a new user is reactivated, either manually or via the API. +user.onboarding_complete Sent when a user has complete their onboarding. +user.updated Sent when a user is changed. +leavebalance.created Sent when a leave balance is created. +leavebalance.updated Sent when a leave balance is updated. +platform_record.created Sent when a platform record is created. +platform_record.updated Sent when a platform record is updated. +platform_record.destroyed Sent when a platform record is destroyed. +ats_jobapplication.stage_changed Sent when a job application stage is updated. +Want to see other topics added to this list? Let us know! + +Creating Webhooks +To manage Webhooks you have two choices: + +the API if this is part of a more complicated integration with Workforce. +the UI if the use case is simple enough. +Programmatically +Webhooks can be managed via API calls, please note you’ll need to be authenticated as an Admin user when making calls to the /api/v2/webhooks endpoints. + +The available endpoints for working with subscriptions are documented below - you can also work with an individual subscription in various ways. + +Through the UI +Workforce presents the Webhooks portal to manage existing and create new subscriptions through the UI. + +Simply enter a URL, Security Token (covered in more detail here) and select the topics you wish to be notified about at the specified URL. + +Testing your Webhooks +When working with webhooks you may find it useful to emit events to somewhere you can inspect them without having to setup a server, try and Google “testing Webhooks” or “tools for testing webhooks”. + +Securing Webhooks +To validate that the webhook came from Workforce and not an unknown 3rd party, we attach a security token to each request in the X-Webhook-Signature header. The security token is constructed from a secret you provide when you first create the webhook subscription. + +The token is computed using the HMAC SHA1 hashing algorithm. The secret you provide during subscription creation is used as the key and the request body (all of it) is the message passed to the hashing function. + +X-Hook-Signature = HMACSHA1(Secret Token, Request Body) + +By checking that the X-Webhook-Signature matches when you receive it you can be sure that the webhook came from Workforce & was not tampered with. + +Some samples for validating the signature are below: + +NodeJS +Python +Ruby +Warning: make sure you are checking the value sent in the X-Webhook-Signature header. Other headers may also be included (eg. X-Hook-Signature) but these are deprecated and should not be used. + +Delivery Timeframes +While all efforts are made to ensure our webhook system is working without delay. Our webhook events are sent as best effort and there is no guarantee an event will be sent from our systems within a specific timeframe. It is important to keep in mind that the delivery timeframes may vary. + +Event Ordering +We cannot guaratee that our systems will send events in order they occur in our systems, you should design your receiving systems to handle this. + +Failures & Retries +When we send a notification, we expect to get back a 200 response code within 30 seconds. If we don’t get a 200 after 30 seconds, we’ll consider the webhook failed & begin retrying it. + +We will retry 12 times, roughly every 5 minutes. +We will retry another 24 times, roughly every 1 hour. +We will stop retrying if we recieve a 200 status. +If after 36 attempts, we still haven’t recieved a 200 status, we will stop sending new webhook events to the URL in question. Webhook events that are currently processing will continue to process (up to 36 tries). To send new events, you will need to manually re-enable the webhook via the UI: https://my.workforce.com/api/webhooks + +Best Practices +Before going live with your application, test that your integration is working properly. + +If your webhook receiver performs complex computation or might be blocked by IO/network you may find it times out when trying to receive the webhook from Workforce. For this reason we recommend you acknowledge receipt of a webhook immediately by returning a 200. + +Webhook endpoints might occasionally receive the same payload more than once. If your integrations logic is dependent upon exactly once delivery we advise you to safeguard against this by making your receiver idempotent, one way of doing this is by using the hook_key field and storing it temporarily, if you receive the same hook_key more than once you can discard it. + +Due to the inherent complexities of sending data across the internet your webhook endpoints may receive payload events out of any inherent order. If your integrations logic is dependent upon ordered event payloads we advise you to safeguard against this by building your receiver system to handle this. + +You should use security tokens, and verify them. See our instructions on Securing Webhooks. + +Subscriptions List [/api/v2/webhooks] +Only admin users are able to access these endpoints. Webhook configuration is common across an organisation. +GETGet Subscriptions +200 +OK +(application/json) +Attributes (array): +id: 123456 (number) - The Tanda defined ID for this subscription +url: https://requestb.in/TaNdA (required) (string) - The URL to send notifications to +token: IaMaSeCrEt (string) - A secret used to verify notification authenticity +topics: user.created, clockin.updated (required, array[string]) - A list of topics to get notifications for +POSTCreate Subscription +We will send a notification with the topic created when a webhook is first created. + +Request (application/json) +Body + + { + "url": "https://requestb.in/TaNdA", + "token": "IaMaOptionalSeCrEt", + "topics": ["user.created", "clockin.updated"] + } + +200 +OK +(application/json) +Attributes: +id: 123456 (number) - The Tanda defined ID for this subscription +url: https://requestb.in/TaNdA (required) (string) - The URL to send notifications to +token: IaMaSeCrEt (string) - A secret used to verify notification authenticity +topics: user.created, clockin.updated (required, array[string]) - A list of topics to get notifications for +Subscription [/api/v2/webhooks/{id}] +GETGet Subscription +Parameters +id: 123456 (number) - The id of the subscription +200 +OK +(application/json) +Attributes: +id: 123456 (number) - The Tanda defined ID for this subscription +url: https://requestb.in/TaNdA (required) (string) - The URL to send notifications to +token: IaMaSeCrEt (string) - A secret used to verify notification authenticity +topics: user.created, clockin.updated (required, array[string]) - A list of topics to get notifications for +DELETEDelete Subscription +Parameters +id: 123456 (number) - The id of the subscription +200 +OK +(application/json) +Public Holidays +Here is a list of public holiday regions supported: + +Region Code Region Name +au Australia (national holidays only) +au_act Australian Capital Territory +au_nsw New South Wales +au_nt Northern Territory +au_qld Queensland +au_sa South Australia +au_tas Tasmania +au_vic Victoria +au_wa Western Australia +cy Cyprus (national holidays) +do Dominican Republic (national holidays) +el Greece (national holidays) +fi Finland (Finnish) (national holidays) +gb United Kingdom (national holidays only) +gb_eng England +gb_nir Northern Island +gb_sct Scotland +gb_wls Wales +jp Japan (national holidays) +lr Liberia (national holidays) +nz New Zealand (national holidays) +mm Myanmar (national holidays) +mt_en Malta (English) (national holidays) +mu Mauritius (national holidays) +us United States of America (national holidays) +za South Africa (national holidays) +ph The Philippines (national holidays) +vi Vietnam (national holidays) +ca Canada (national holidays) +be_fr Belgium (national holidays in French) +be_nl Belgium (national holidays in Dutch) +th Thailand (national holidays only) +uy Uruguay (national holidays only) +You can see holiday dates in your System Settings. + +Public Holidays [/api/v2/public_holidays] +GETGet Public Holidays +Given a list of regions, and years, returns a list of public holidays in those regions in those years. + +Parameters +regions: au,au_qld (required, string) - one or more comma separated public holiday regions +years: 2018,2019 (required, string) - one or more comma separated public holiday years +201 +Created +(application/json) +Body + + { + "au": [{"date": "2018-01-01", "name": "New Year's Day"}], + "au_qld": [{"date": "2018-01-01", "name": "New Year's Day"}] + } + +Temporary Files +Temporary Files [/api/v2/temporary_files] +POSTCreate Temporary File +Use this endpoint to upload files that will then be called by other endpoints. This allows you to process files “in the background” while a user is doing other things in your app. For example, if you are building a form for a user to request time off, you might start uploading to the temporary files endpoint as soon as the user chooses a verification photo. That way, when they submit the form, you have already uploaded the file and they don’t need to wait for it to upload. + +Files must not be larger than 5mb. + +Unlike other endpoints, this endpoint does not accept JSON. Instead it expects binary file data. + +If you’re writing JavaScript, use the FormData API. +If you’re using CURL, use the -F (--form) flag. +If you’re using Postman, use the Form-data option, with a param of type “File” and key file. +If you like, you can pass a comma separated list of content types to the content_types param. If provided, the API will validate these against the uploaded file, and you will get a 400 status back if the file is invalid. + +If the upload is successful, you’ll get back a file_id. Use this ID with endpoints that support file attachments. There’s a few things you should know about file IDs: + +A file ID can only be used once. It will be deleted once the uploaded file has successfully been linked via another endpoint. +If a file ID isn’t used for a week it will be deleted. + +Parameters +file: (required, file data) +content_types: image/gif,image/png (optional, string) - a comma separated list of content types to validate against +201 +Created +(application/json) +Body + + { + "file_id": "73fe3430-4f5d-3a0a-84a7-cffbeb5efeb2" + } + +Shift Reminders +Shift Reminders are sent as push notifications to the Workforce App before a schedule is set to start. Use this endpoint to configure reminders. + +The minutes_before_shift_start param, used to control when reminders send, must be a multiple of 5, and must be between 5 and 1435 (ie. it can’t be more than 1 day). You can set up as many reminders as you like. Reminders are tied to the current authenticated user, which means they work across all organisations the user works at. + +Shift Reminder List [/api/v2/shift_reminders] +GETGet Shift Reminders +This method requires the me scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 123706 (number, required) - The shift reminder ID +minutes_before_shift_start: 30 (number, required) - How long before a shift (in minutes) the reminder should send +POSTCreate Shift Reminder +This method requires the me scope (see Scopes for more information). +Request (application/json) +Body + + { + "minutes_before_shift_start": 120 + } + +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The shift reminder ID +minutes_before_shift_start: 30 (number, required) - How long before a shift (in minutes) the reminder should send ++ minutes_before_shift_start: 120 + +Shift Reminder [/api/v2/shift_reminders/{id}] +GETGet Shift Reminder +This method requires the me scope (see Scopes for more information). +Parameters +id: 123706 (number) - The ID of the shift reminder +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The shift reminder ID +minutes_before_shift_start: 30 (number, required) - How long before a shift (in minutes) the reminder should send +PUTUpdate Shift Reminder +This method requires the me scope (see Scopes for more information). +Parameters +id: 123706 (number) - The ID of the shift reminder to edit +Request (application/json) +Body + + { + "minutes_before_shift_start": 45 + } + +200 +OK +(application/json) +Attributes: +id: 123706 (number, required) - The shift reminder ID +minutes_before_shift_start: 30 (number, required) - How long before a shift (in minutes) the reminder should send ++ minutes_before_shift_start: 45 + +DELETEDelete Shift Reminder +This method requires the me scope (see Scopes for more information). +Parameters +id: 123706 (number) - The ID of the shift reminder +200 +OK +(application/json) +User Files +User files are documents that can be attached to a user’s profile. These files are visible to the user and their managers. + +Only users with appropriate permissions can upload files. The user making the request must have permission to update the target user. +Unlike most endpoints, this endpoint does not accept JSON. Instead it expects multipart form data containing both the user_id and the file. + +If you’re writing JavaScript, use the FormData API. +If you’re using CURL, use the -F (--form) flag. +If you’re using Postman, use the Form-data option. +Files must not be larger than 5mb. + +User Files [/api/v2/user_files] +POSTUpload User File +Upload a file to be attached to a user’s profile. + +This method requires the user scope (see Scopes for more information). +Parameters +user_id: 123456 (required, number) - The ID of the user to attach the file to +file: (required, file) - The file to upload +document_type: contract (optional, enum[string]) - How the file is classified. Defaults to other. +Members +warning +contract +document +performance_review +training +form +file_note +other +viewable_by_attached_user: true (optional, boolean) - Whether the attached user can see the file. Defaults to true. +viewable_by_attached_users_managers: true (optional, boolean) - Whether the attached user’s managers can see the file. Defaults to true. +Request Upload File (multipart/form-data) +Body + + { + "user_id": 123456, + "file": (binary), + "document_type": "contract", + "viewable_by_attached_user": true, + "viewable_by_attached_users_managers": true + } + +200 +OK +(application/json) +Body + + { + "id": 42 + } + +400 +Bad Request +(application/json) +Body + + { + "error": "file is missing" + } + +400 +Bad Request +(application/json) +Body + + { + "error": "document_type does not have a valid value" + } + +403 +Forbidden +(application/json) +Body + + { + "error": "You do not have access to this endpoint." + } + +404 +Not Found +(application/json) +Body + + { + "error": "No known user with that id!" + } + +Custom Events +Custom Events List [/api/v2/custom_events] +GETGet Custom Events List +Get a full list of custom events in a single request. + +This method requires the roster and department scopes (see Scopes for more information). +Parameters +from: 2018-09-01 (required, string) - From date to lookup custom events +to: 2018-09-01 (required, string) - To date to lookup custom events +200 +OK +(application/json) +Attributes (array): +id: 123677 (number, required) - The custom event ID +organisation_id: 23100 (number, required) - The organisation id where this custom event belongs to +start: 2018-09-01 (required) (string) - The date of the first day of the custom event +finish: 2018-09-01 (required) (string) - The last date of the custom event +name: Tanda Panda Day (required, string) - The name of the custom event +discourage_time_off: true (required) (boolean) - This states whether a custom events blocks time off or not +locations: 2002,2003,2004 (required) (array[number]) - The location ids to where the custom event applies +POSTCreate Custom Events List +Creates a custom event under the current organisation and placed under the specified location + +This method requires the roster and department scopes (see Scopes for more information). +Request Create Custom Event (application/json) +Body + + { + "name": "Workforce Panda Day", + "start": "2018-09-01", + "finish": "2018-09-01", + "discourage_time_off": true, + "custom_event_joins_attributes": [ + {"location_id": 2002}, + {"location_id": 2003}, + {"location_id": 2004} + ] + } + +200 +OK +(application/json) +Attributes: +id: 123677 (number, required) - The custom event ID +organisation_id: 23100 (number, required) - The organisation id where this custom event belongs to +start: 2018-09-01 (required) (string) - The date of the first day of the custom event +finish: 2018-09-01 (required) (string) - The last date of the custom event +name: Tanda Panda Day (required, string) - The name of the custom event +discourage_time_off: true (required) (boolean) - This states whether a custom events blocks time off or not +locations: 2002,2003,2004 (required) (array[number]) - The location ids to where the custom event applies +PUTUpdate Custom Event Request +This method requires the roster and department scopes (see Scopes for more information). +Parameters +id: 123677 (required, number) - The id of the custom event to update +Request Update Custom Event (application/json) +Body + + { + "name": "Workforce Panda Month", + "start": "2018-09-01", + "finish": "2018-09-30", + "discourage_time_off":true, + } + +200 +OK +(application/json) +Attributes: +id: 123677 (number, required) - The custom event ID +organisation_id: 23100 (number, required) - The organisation id where this custom event belongs to +start: 2018-09-01 (required) (string) - The date of the first day of the custom event +finish: 2018-09-01 (required) (string) - The last date of the custom event +name: Tanda Panda Day (required, string) - The name of the custom event +discourage_time_off: true (required) (boolean) - This states whether a custom events blocks time off or not +locations: 2002,2003,2004 (required) (array[number]) - The location ids to where the custom event applies ++ name: Workforce Panda Month ++ start: `2018-09-01` ++ finish: `2018-09-30` ++ discourage_time_off: true + +DELETEDelete Custom Event +This method requires the roster and department scopes (see Scopes for more information). +Parameters +id: 123677 (required, number) - The id of the custom event to delete +200 +OK +(application/json) +Custom Event Locations [/api/v2/custom_events/{id}/locations] +PUTUpdate Custom Event Locations +This method requires the roster and department scopes (see Scopes for more information). +Parameters +id: 123677 (required, number) - The id of the custom event to update +location_ids: 2007 (required, array[number]) +Request Update Custom Event locations (application/json) +Body + + { + "location_ids": [2007] + } + +200 +OK +(application/json) +Attributes: +id: 123677 (number, required) - The custom event ID +organisation_id: 23100 (number, required) - The organisation id where this custom event belongs to +start: 2018-09-01 (required) (string) - The date of the first day of the custom event +finish: 2018-09-01 (required) (string) - The last date of the custom event +name: Tanda Panda Day (required, string) - The name of the custom event +discourage_time_off: true (required) (boolean) - This states whether a custom events blocks time off or not +locations: 2002,2003,2004 (required) (array[number]) - The location ids to where the custom event applies ++ name: Workforce Panda Month ++ start: `2018-09-01` ++ finish: `2018-09-30` ++ discourage_time_off: true ++ locations: 2002, 2003, 2004, 2005, 2007 + +DELETEDelete Custom Event Location +This method requires the roster and department scopes (see Scopes for more information). +Parameters +id: 123677 (required, number) - The id of the custom event to update +location_ids: 2007 (required, array[number]) +Request Update Custom Event locations (application/json) +Body { “location_ids”: [2005, 2007] } +200 +OK +(application/json) +Attributes: +id: 123677 (number, required) - The custom event ID +organisation_id: 23100 (number, required) - The organisation id where this custom event belongs to +start: 2018-09-01 (required) (string) - The date of the first day of the custom event +finish: 2018-09-01 (required) (string) - The last date of the custom event +name: Tanda Panda Day (required, string) - The name of the custom event +discourage_time_off: true (required) (boolean) - This states whether a custom events blocks time off or not +locations: 2002,2003,2004 (required) (array[number]) - The location ids to where the custom event applies ++ name: Workforce Panda Month ++ start: `2018-09-01` ++ finish: `2018-09-30` ++ discourage_time_off: true ++ locations: 2002, 2003, 2004 + +Schedule Swap Plans +About +Models the life cycle of swapping a shift between two people. The two people involved in a swap are referred to as the sender (the original staff member) and the recipient (the new staff member). + +Strategies +Schedule swap plans can be handled with different strategies. The strategy states include undecided, broadcasted and curated. Schedule swap plans in an undecided state, represent a plan that is new and has not had a strategy selected. + +When a schedule swap plan is using the broadcasted strategy, managers will need to approve a person offering to cover the schedule. + +When a schedule swap plan is using the curated strategy, the swap will be assigned to the first person accepting to cover the schedule. + +Schedule swap plans created less than 3 days in advance of a schedule’s start are automatically broadcated. +States +A schedule swap plan can be in 3 states. The states are pending, approved and rejected. + +Schedule Swap Plan List [/api/v2/schedule_swap_plans] +GETSchedule Swap Plan +Returns a list of schedule swap plans. Gets all plans by default, but you may specify which plans you would like to return using the statuses parameter. Schedule Swap Plans that are visible include: + +Schedule swap plans where you are the sender +Schedule swap plans where you are a manager of the sender +This method requires the roster scope. (see Scopes for more information) +Request (appplication/json) +Body + + { + "statuses": "pending,approved,rejected" + } + +200 +OK +(application/json) +Attributes (array): +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +POSTSchedule Swap Plan +Creates a new schedule swap plan. If the schedule’s start is more than 3 days away, the schedule swap plan will have a strategy of undecided, else it will automatically be broadcasted - unless the request is sent with the prepare_auto_broadcast: false parameter. + +This method requires the roster scope. (see Scopes for more information) +Schedule swap plans can only be created for yourself or someone you manage +Schedule swap plans can only be created for schedules that meet the following criteria: - Is in the future - Has a start - Has a team +Request (application/json) +Body + + { + "schedule_id": 10203834, + // "prepare_auto_broadcast": true + } + +200 +OK +(application/json) +Attributes: +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +Broadcast [/api/v2/schedule_swap_plans/{id}/broadcast] +Transistions a swap from undecided to broadcasted making all concealed schedule swap proposals transition to pending. A schedule swap proposal will still require to be approved for this schedule swap plan to transition to approved + +PUTBroadcast A Swap +This method requires the roster scope. (see Scopes for more information) +Parameters +id: 26 (number) - The schedule swap plan ID +200 +OK +(application/json) +Attributes: +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +Curate [/api/v2/schedule_swap_plans/{id}/curate] +Transistions a schedule swap plan from undecided to curated making all specified concealed schedule swap proposals transition to pending. This schedule swap plan will transition directly to approved when a user accepts to cover the schedule + +PUTCurate A Swap +This method requires the roster scope. (see Scopes for more information) +Parameters +id: 26 (number) - The schedule swap plan ID +Request (application/json) +Body + + { + "proposal_ids": [12312983, 12890371] + } + +200 +OK +(application/json) +Attributes: +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +Reject [/api/v2/schedule_swap_plans/{id}/reject] +PUTReject A Swap +Transitions a schedule swap plan to rejected making all its schedule swap proposals also transition to rejected + +This method requires the roster scope. (see Scopes for more information) +Schedule swap plans can only be rejected yourself or for someone you manage +Parameters +id: 26 (number) - The schedule swap plan ID +200 +OK +(application/json) +Attributes: +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +Notify [/api/v2/schedule_swap_plans/{id}/notify] +PUTNotify Everyone Who Can Cover +Will send a notification to all users available to cover this shift through the Workforce App + +Notifications will only be sent to users who have a schedule swap proposals in a pending or seen state. +This method requires the roster scope. (see Scopes for more information) +Only a manager of a swap can notify the users available +Parameters +id: 26 (number) - The schedule swap plan ID +200 +OK +(application/json) +Attributes: +id: 1231342 (number, required) - The schedule swap plan ID +recipient_id: 3249234 (number) - The person who received the schedule +sender_id: 4593929 (number, required) - The person who offered to swap the schedule +schedule_id: 2340280 (number) - The schedule being swapped +status: approved (string) - The status of the swap. Will be one of ‘pending’, ‘approved’ or ‘rejected’ +strategy: broadcasted (required) - the method of swapping the schedule. Will be one of ‘undecided’, ‘broadcasted’ or ‘curated’ +reason: cannot work this shift - the reason the sender cannot work the schedule +Schedule Swap Proposals +About +Models the life cycle of vetting one potential recipient for a schedule swap plan. + +States +Used to specify where a proposal is in its life cycle: + +Schedule swap proposals which are concealed are only visible to managers. and are visible to enable a manager to curate a schedule swap plan. + +Schedule swap proposals which are pending are new and have not been actioned by a user. + +Schedule swap proposals which are seen have been marked as viewed by the users (e.g. a swap can be marked as seen through the API, or by the Workforce App) + +Schedule swap proposals which are accepted have had a user offer to cover the shift but are awaiting approval by a manager. + +Schedule swap proposals which are approved have been accepted, and the user of this proposal is now scheduled to work the shift. + +Schedule swap proposals which are rejected have been declined by either the user or a manager of the user + +Schedule Swap Proposal List [/api/v2/schedule_swap_proposals] +GETSchedule Swap Proposals +Returns a list of schedule swap proposals. Gets all proposals by default, but you may specify which proposals to return using the statuses parameter. Schedule swap proposals that are visible include: + +schedule swap proposals of users you manage +schedule swap proposals for yourself (that are not concealed) +If a user has another commitment at the same time as the proposed schedule. There will be no proposal for this user +This method requires the roster scope. (see Scopes for more information) +Request (appplication/json) +Body + + { + "statuses": "concealed,seen,pending,approved,rejected,accepted" + } + +200 +OK +(application/json) +Attributes (array): +id: 23409 (number, required) - The schedule swap proposal ID +schedule_swap_plan_id: 23 (number, required) - The schedule swap plan ID that this proposal belongs to. +user_id: 1263 (number, required) - The user ID that this proposal is for +status: pending (string, required) - The state of the proposal. Will be one of ‘concealed’, ‘pending’, ‘seen’, ‘accepted’, ‘approved’, ‘rejected’ +strategy: curated (string, required) - The strategy of the schedule swap plan this proposal belongs to. Will be one of ‘undecided’, ‘broadcasted’, ‘curated’ +response: Thankyou for covering this shift 🙏 (string, optional) - A response for the user of the proposal from a manager +complete: true (boolean, required) - Whether the schedule swap proposal has been processed yet - most data will be empty until this is true +created_at: 1643637600 (number, required) - The date the schedule swap proposal was created, in unix time +updated_at: 1646056800 (number, required) - The date the schedule swap proposal was last updated, in unix time +schedule_details (object) - the details of the proposed schedule +automatic_break_length: 30 (number, required) - the length of automatic breaks in minutes +cost: 196.48 (number, required) - the cost the shift would have +department_id: 23423 (number, required) - the team ID the schedule will be in +finish: 230948239048 (number, optional) - the time the schedule would finish at +start: 1239182309902 (number, required) - the time the schedule would start at +shift_detail_id: 9210 (number, optional) - the shift detail ID for the detail the shift would have +breaks: (array[ScheduleBreakData]) - the breaks the schedule would have +roster_details (object) - the details of the proposed roster +hours_in_week: 32 (number, optional) - number of hours the employee will work +errors: (array[RosterError]) the validation errors that the roster would contain +Schedule Swap Proposal [/api/v2/schedule_swap_proposals/{id}/status] +PUTSchedule Swap Proposal +Useful to transition a proposal between states. + +Proposals are only eligible for transitions under specific circumstances. The conditions are: +* Transition to seen - proposal must currently be pending or accepted - proposal must be your own + +* Transition to acccepted - proposal must currently be pending or seen - proposal must be your own + +* Transition to approved - proposal must be accepted - plan must be pending + +* Transition to rejected - proposal must be pending, seen or accepted - proposal must be your own, or someones who you manage + +Parameters +id: 23409 (number, required) - The ID of the schedule swap proposal to update +status: ‘seen’ (string, required) - Must be one of ‘seen’, ‘accepted’, ‘approved’, ‘rejected’ +200 +OK +(application/json) +Attributes: +id: 23409 (number, required) - The schedule swap proposal ID +schedule_swap_plan_id: 23 (number, required) - The schedule swap plan ID that this proposal belongs to. +user_id: 1263 (number, required) - The user ID that this proposal is for +status: pending (string, required) - The state of the proposal. Will be one of ‘concealed’, ‘pending’, ‘seen’, ‘accepted’, ‘approved’, ‘rejected’ +strategy: curated (string, required) - The strategy of the schedule swap plan this proposal belongs to. Will be one of ‘undecided’, ‘broadcasted’, ‘curated’ +response: Thankyou for covering this shift 🙏 (string, optional) - A response for the user of the proposal from a manager +complete: true (boolean, required) - Whether the schedule swap proposal has been processed yet - most data will be empty until this is true +created_at: 1643637600 (number, required) - The date the schedule swap proposal was created, in unix time +updated_at: 1646056800 (number, required) - The date the schedule swap proposal was last updated, in unix time +schedule_details (object) - the details of the proposed schedule +automatic_break_length: 30 (number, required) - the length of automatic breaks in minutes +cost: 196.48 (number, required) - the cost the shift would have +department_id: 23423 (number, required) - the team ID the schedule will be in +finish: 230948239048 (number, optional) - the time the schedule would finish at +start: 1239182309902 (number, required) - the time the schedule would start at +shift_detail_id: 9210 (number, optional) - the shift detail ID for the detail the shift would have +breaks: (array[ScheduleBreakData]) - the breaks the schedule would have +roster_details (object) - the details of the proposed roster +hours_in_week: 32 (number, optional) - number of hours the employee will work +errors: (array[RosterError]) the validation errors that the roster would contain +Timeclock Questions +Use Timeclock Questions to create workflows around clocking in. There are currently three types of questions. The types are allowance, shift_tag and shift_note. Answering the questions will perform the following: + +An allowance question will apply an allowance to the shift +A shift_tag question will set the tag on the shift +A shift_note question will add a note to the shift +These triggers are available: * start – question is asked when a staff member enters a clockin. * finish – question is asked when a staff member enters a clockout. +These conditions are available: * early – question is only asked when a trigger is early for its corresponding schedule. * late – question is only asked when a trigger is late for its corresponding schedule. * unscheduled – question is only asked when a trigger has no corresponding schedule. * schedule_needs_acceptance - question is asked if the schedule still needs acceptance or if the schedule is accepted, but the clock in is early or late. * shift_length_over - question is only asked if the shift length exceeds the condition threshold in hours +All methods require the device scope (see Scopes for more information). +Timeclock Questions List [/api/v2/timeclock_questions] +GETGet Timeclock Questions +Parameters +ask_on_devices: timeclock,app (optional, string) - Filter to a particular type of question. Comma separated list of devices. +200 +OK +(application/json) +Attributes (array): +id: 42 (number, required) - the question ID +question: What was your cash tips balance? (required) (string) - The question to ask +required: false (optional, boolean) - Should the shift data be considered incomplete if the question is not answered? (displays warning message to ensure employee is aware the question is required) +trigger_on: finish (required) (string) - when should the question be asked? +ask_on_devices: timeclock (optional, array[string]) - what devices should this question show on? +condition: early (optional, string) - conditions under which this question should show +type: allowance (required) (string) - type of answer expected +lenience: 15 (number, required) - for conditional question, how much lenience when asked +process: ask_question (required) (string) - what action should take place if the given criteria is met. ask_question block_clockin +POSTCreate Timeclock Questions +When creating a timeclock questions you can also create answer options. This will allow Workforce to suggest options when asking the question. + +Request (application/json) +Body + + { + "condition": "late" // (optional), + "name": "What is the meaning of life?", + "trigger_on": "finish", + "type": "shift_note", + "required": true, + "answer_options_attributes": [ // (optional) + { answer: "42" }, + ] + } + +201 +Created +(application/json) +Attributes: +id: 42 (number, required) - the question ID +question: What was your cash tips balance? (required) (string) - The question to ask +required: false (optional, boolean) - Should the shift data be considered incomplete if the question is not answered? (displays warning message to ensure employee is aware the question is required) +trigger_on: finish (required) (string) - when should the question be asked? +ask_on_devices: timeclock (optional, array[string]) - what devices should this question show on? +condition: early (optional, string) - conditions under which this question should show +type: allowance (required) (string) - type of answer expected +lenience: 15 (number, required) - for conditional question, how much lenience when asked +process: ask_question (required) (string) - what action should take place if the given criteria is met. ask_question block_clockin +Timeclock Question [/api/v2/timeclock_questions/{id}] +DELETEDelete Timeclock Question +Deleting a timeclock question will not delete the corresponding allowance. + +Parameters +id: 42 (number) - The id of the question +200 +OK +(application/json) +Answer Questions [/api/v2/timeclock_questions/{id}/answer] +POSTAnswer Questions +This answer submits an answer to a timeclock question. The answer will be stored based on how the question is configured. For example it might be set as an allowance value, or a tag on a shift. + +You can post answers through this endpoint, or as part of a clock in. If you use this endpoint, the answer will be validated, and you’ll get an error if it’s the wrong format. If you use the clock ins endpoint, no validation is performed. + +The answers type depends on the question type. Expected types for answers are: - shift_note question expects string - allowance question expects number +Parameters +id: 42 (number, required) - The ID of the question +user_id: 234 (number, required) - the ID of the user answering +shift_id: 23432 (number, required) - the ID of the shift this question relates to +answer: 63.4 (string number, required) - the actual answer to the question. the value type depends on the question. +Request Submit a cash tips balance (application/json) +Body + + { + "user_id": 234, + "shift_id": 23432, + "answer": 63.4 + } + +200 +OK +(application/json) +Platform +All methods require the platform scope (see Scopes for more information). +Object List [/api/v2/platform/objects] +GETGet Objects +Gets all of your current platform objects + +200 +OK +(application/json) +Attributes (array[PlatformObjectData]) +Object [/api/v2/platform/objects/{id}] +GETGet Object +Gets a specific platform object by id + +Parameters + +id: 42 (number) - The id of the object +Response 200 (application/json) + +Attributes (PlatformObjectData) +PUTPut Object +Updates a specific platform object + +Parameters + +id: 42 (number) - The id of the object +Request (application/json) + +Body + + { + "fields": [ + { "name": "Description", "key": "description", "type": "text" }, + { "name": "Amount", "key": "amount", "type": "float" } + ], + "owned_associations": [ + { "name": "Client", "key": "client_id", "type": "has_one" }, + { "name": "Sales Rep", "key": "employee_id", "type": "has_many" } + ] + } + +Response 201 (application/json) + +Attributes (PlatformObjectData) +DELETEDelete Object +Deletes a specific platform object + +Parameters + +id: 42 (number) - The id of the object +Response 204 (application/json) + +Record List [/api/v2/platform/{api_name}] +GETGet Records +Gets all platform records for a specific platform object + +You cannot use this endpoint for native objects (shift, user, etc), only for custom ones. +Parameters + +api_name: invoices (string) - The api name of the object +Response 200 (application/json) + +Body + + { + "id": 19283, + "description": 'an invoice', + "amount": 99.99 + } + +POSTPost Record +Creates a platform record for a specific platform object + +Parameters + +api_name: invoices (string) - The api name of the object +Request (application/json) + +Body + + { + "description": 'an invoice', + "amount": 99.99 + } + +Response 201 (application/json) + +Body + + { + "id": 19283, + "description": 'an invoice', + "amount": 99.99 + } + +Record [/api/v2/platform/{api_name}/{id}] +GETGet Record +Gets a specific platform record for a specific platform object + +Parameters + +api_name: invoices (string) - The api name of the object +id: 42 (number) - The id of the object +Response 200 (application/json) + +Body + + { + "id": 19283, + "description": 'an invoice', + "amount": 99.99 + } + +PUTPut Record +Updates a specific platform record for a specific platform object + +Parameters + +api_name: invoices (string) - The api name of the object +id: 42 (number) - The id of the object +Request (application/json) + +Body + + { + "description": 'another invoice' + } + +Response 201 (application/json) + +Body + + { + "id": 19283, + "description": 'an invoice', + "amount": 99.99 + } + +DELETEDelete Record +Deletes a specific platform record for a specific platform object + +Parameters + +api_name: invoices (string) - The api name of the object +id: 42 (number) - The id of the object +Response 204 (application/json) + +Reports +The reports endpoint contains read-only endpoints to answer questions that get asked a lot where the answer is better off automated. + +All reports require the cost scope (see Scopes for more information). +Adoption Metrics [/api/v2/reports/adoption_metrics] +GETGet Adoption Metrics +Provides a summary of adoption of key product features across the organisation. + +Parameters +weeks: 1 (number) - 1 2 4 - number of weeks to aggregate data for +include_user_ids: false (optional, boolean) - if true, include user IDs for every property. Defaults to false. +200 +OK +(application/json) +Body + + { + "last_sign_in": 1586962894, + "active_employees": 1797, + "app": { + "average": 655, + "delta": 0 + }, + "timesheets": { + "average": 0, + "delta": 0 + }, + "approved_timesheets": { + "average": 67, + "delta": 3.728436282693378 + }, + "clockins": { + "average": 307, + "delta": 17.028380634390654 + }, + "rostering": { + "average": 224, + "delta": 10.127991096271565 + }, + "onboarding": { + "completed": 0, + "added_manually": 1792, + "new": 1797 + } + } + +Shift Ratings +Either the user_id or employee_number fields are required to submit a shift rating. The user_id is the systems unique id (id field from /users) and the employee_number is the employee payroll_number (employee_number from /users). + +Posting another shift rating for a user for that day will override the current shift rating. + +Shift Rating requires the timesheet scope (see Scopes for more information). +Shift Ratings [/api/v2/shift_ratings] +POSTPost Shift Ratings +Allows for a shift rating from 1 to 5 to be added to a users shift in Workforce if they worked on that day. + +Request Submit Shift Rating with user_id (application/json) +Body + + { + "user_id": 12345, + "date": "2020-07-22", + "rating": 5 + } + +Request Submit Shift Rating with employee_number (application/json) +Body + + { + "employee_number": "abc123", + "date": "2020-07-22", + "rating": 5 + } + +200 +OK +(application/json) +Body + + { + "id": 9665, + "timesheet_id": 7007, + "user_id": 123456, + "date": "2016-03-12", + "start": 1457741040, + "break_start": null, + "break_finish": null, + "break_length": null, + "finish": 1457762640, + "status": "PENDING", + "allowances": [], + "tag": null, + "tag_id": null, + "department_id": 808, + "department_export_name": "abc123", + "metadata": "My special metadata", + "leave_request_id": null, + "rating": 5, + "last_rated_by": 123345 + } + +Cognitive Creator Configuration +Either the department_id or cognitive_creator_configuration_id fields are required to for creating or updating a cognitive creator configuration. + +Cognitive creator configuration requires the department scope (see Scopes for more information). +Cognitive Creator Configuration list [/api/v2/cognitive_creator_configuration] +GETGet Cognitive Creator Configuration +Gets all your Cognitive Creator Configurations. + +Parameters +updated_after: 1451570400 (optional, number) - Time filter to find recently created or modified users +200 +OK +(application/json) +Attributes (array[CognitiveCreatorConfigurationData]) +Cognitive Creator Configuration [/api/v2/cognitive_creator_configuration/{id}] +GETGet Cognitive Creator Configuration +Gets the specified Cognitive Creator Configuration by ID. + +200 +OK +(application/json) +Attributes (CognitiveCreatorConfigurationData) +PUTPut Cognitive Creator Configuration +Update the specified Cognitive Creator Configuration by ID. + +Request Cognitive Creator Configuration (application/json) +Body + + { + "minimum_staff": 0, + "maximum_staff": 3, + "max_concurrent_start": 2, + "min_length": 3.5, + "max_length": 8, + "time_to_open": 0.5, + "time_to_close": 0.5, + "undercoverage_penalty_ratio": 1.3, + "round_down_head_count": true, + "consolidate_shift_count": true + } + +200 +OK +(application/json) +Attributes (CognitiveCreatorConfigurationData) +DELETEDelete Cognitive Creator Configuration +Delete the specified Cognitive Creator Configuration by ID. This will effectively reset the settings to default. + +200 +OK +(application/json) +Cognitive Creator Configuration by department [/api/v2/cognitive_creator_configuration/for_department/{department_id}] +GETGet Cognitive Creator Configuration +Get Cognitive Creator Configuration for a department. + +200 +OK +(application/json) +Attributes (CognitiveCreatorConfigurationData) +POSTPost Cognitive Creator Configuration +Create cognitive creator configuration for a department. Posting another cognitive creator configuration for a department will override the current configuration. + +Request Cognitive Creator Configuration (application/json) +Body + + { + "minimum_staff": 0, + "maximum_staff": 3, + "max_concurrent_start": 2, + "min_length": 3.5, + "max_length": 8, + "time_to_open": 0.5, + "time_to_close": 0.5, + "undercoverage_penalty_ratio": 1.3, + "round_down_head_count": true, + "consolidate_shift_count": true + } + +200 +OK +(application/json) +Attributes (CognitiveCreatorConfigurationData) +Predicted Store Stats +Our user guide has more information on working with data streams through the API. +Predicted store stats are identical to the Store Stats model, however, instead of being for what actually happened, they are the prediction of what will happen. All predicted store stats for a data stream should be totaled data for a time period equal to the data stream’s data_interval, ending at a predicted store stat’s time. + +Only predicted store stats where the type parameter is equal to “sales” will be displayed on the weekly planner on the dashboard. You’re able to store a wide variety of stats in Workforce, and differentiate them using the type parameter, but be aware of this special case. + +Predicted store stats can be read by a user, if that user is able to read the data stream that the store stat belongs to. +Only admins can create predicted store stats. They are able to do this for any data stream. + +The api currently only supports POST and GET methods of Predicted Store Stats. + +Predicted Store Stats for Datastream [/api/v2/predicted_storestats/for_datastream/{datastream_id}] +GETPredicted Store Stats for Datastream +This method requires the datastream scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +datastream_id: 26 (number) - The id of the data stream to lookup store stats for +from: 2016-03-01 (string) - The start date of the range to lookup store stats in +to: 2016-03-20 (string) - The id of the data stream to lookup store stats for +type: sales (optional, string) - The type of store stats to lookup (if not specified, all stats are returned) +200 +OK +(application/json) +Attributes (array): +id: 3518 (required) (number) - The id of the stat +time: 1459295100 (required) (number) - The time of the stat +stat: 3.50 (required) (number) - The value of the stat +type: sales (required) (string) - The type of the stat +datastream_id: 24 (required) (number) - The id of the data stream join’s data stream +Predicted Store Stats for Location [/api/v2/predicted_storestats/for_location/{location_id}] +GETPredicted Store Stats for Location +This method requires the datastream scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +location_id: 26 (number) - The id of the location to lookup store stats for +from: 2016-03-01 (string) - The start date of the range to lookup store stats in. +to: 2016-03-20 (string) - The id of the data stream to lookup store stats for. We will crop the data to the nearest business day, eg if the location is open 8am -> midnight, then we will set the cuttoff time for the data to be 4am. Data before then will go to the previous business day, data after will come in for this business day. +200 +OK +(application/json) +Attributes +data_stream: (GenericDatastreamData) +data_stream_joins: (array[DataStreamJoinData]), +stat_type: sales (string) +stats: (array[StoreStatData]) +Predicted Store Stats for Multiple Datastreams [/api/v2/predicted_storestats/for_datastream] +POSTPredicted Store Stats for Multiple Datastreams +To post predicted store stats in bulk, simply represent them all in an array, with the datastream_id included for each stat. Note that each combination of datastream_id, time and type must be unique - you must not send duplicate data. + +POSSIBLE DATA DELETION: All existing predicted store stats on the data stream within the newly posted store stats’ time parameter range will be replaced with the newly posted predicted store stats. +Please see the general Store Stat information above for an example. + +{ "stats": [ { "datastream_id": 123456, "time": 1459296900, "stat": 7.5, "type": "sales" }, { "datastream_id": 123456, "time": 1459297800, "stat": 3, "type": "transactions" } { "datastream_id": 123457, "time": 1459296900, "stat": 3.5, "type": "sales" }, { "datastream_id": 123457, "time": 1459297800, "stat": 1, "type": "transactions" } ] } + +Request Create single Predicted Store Stat (application/json) +Body + + { + "datastream_id": 123457, + "time": 1459296900, + "stat": 3.5, + "type": "sales" + } + +201 +Created +(application/json) +Attributes: +id: 3518 (required) (number) - The id of the stat +time: 1459295100 (required) (number) - The time of the stat +stat: 3.50 (required) (number) - The value of the stat +type: sales (required) (string) - The type of the stat +datastream_id: 24 (required) (number) - The id of the data stream join’s data stream ++ id: 12231 (number) ++ datastream_id: 123457 (number), ++ time: 1459296900 (number) ++ stat: 3.5 (number) ++ type: sales (string) + +Request Create multiple Store Stats (application/json) +Body + + { + "stats": [ + { + "datastream_id": 123456, + "time": 1459296900, + "stat": 7.5, + "type": "sales" + }, + { + "datastream_id": 123456, + "time": 1459296900, + "stat": 3, + "type": "transactions" + }, + { + "datastream_id": 123457, + "time": 1459297800, + "stat": 3.5, + "type": "sales" + }, + { + "datastream_id": 123457, + "time": 1459297800, + "stat": 1, + "type": "transactions" + } + } + ] + } + +Response 201 (application/json) Attributes (array): + ++ (object) + + id: 12231 (number) + + datastream_id: 123456 (number), + + time: 1459296900 (number) + + stat: 7.5 (number) + + type: sales (string) ++ (object) + + id: 12232 (number) + + datastream_id: 123456 (number), + + time: 1459296900 (number) + + stat: 3 (number) + + type: transactions (string) ++ (object) + + id: 12233 (number) + + datastream_id: 123457 (number), + + time: 1459297800 (number) + + stat: 3.5 (number) + + type: sales (string) ++ (object) + + id: 12234 (number) + + datastream_id: 123457 (number), + + time: 1459297800 (number) + + stat: 1 (number) + + type: transactions (string) + +Delete Predicted Store Stats [/api/v2/predicted_storestats/for_datastream/{datastream_id}] +DELETEDelete Predicted Store Stats +This method requires the datastream scope (see Scopes for more information). +You will only be able to query up to 31 days using the from and to parameters. +Parameters +datastream_id: 26 (number) - The id of the datastream to lookup store stats for +from: 2016-03-01 (string) - The start date of the range to lookup store stats in. +to: 2016-03-20 (string) - The end of the date range of to lookup store stats in. We will crop the data to the nearest business day, eg if the location is open 8am -> midnight, then we will set the cuttoff time for the data to be 4am. Data before then will go to the previous business day, data after will come in for this business day. +200 +OK +(application/json) +Attributes +stats: (array[PredictedStoreStatData]) +Stat Types +Stat Types connect Data Streams to the object which owns the stream’s data. For example, data from a stream may be relate to a Team, Location, or to the Organisation itself. + +Stat Type List [/api/v2/stat_types] +GETGet Stat Types +This method requires the datastream scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 162 (required) (number) - The id of the data stream join +stat_type: sales (required) (string) - The ket of the stat type, this much exactly match whats uploaded to StoreStats +label: Sales (required) (string) - The human readable name for this stat type +format: currency (string) - How this stat type is formatted +data_type: sum (string) - What kind of data this is (sum or static) +Stat Type [/api/v2/stat_types/{id}] +GETGet Stat Type +This method requires the datastream scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the stat type to lookup +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +stat_type: sales (required) (string) - The ket of the stat type, this much exactly match whats uploaded to StoreStats +label: Sales (required) (string) - The human readable name for this stat type +format: currency (string) - How this stat type is formatted +data_type: sum (string) - What kind of data this is (sum or static) +POSTCreate Stat Type +This method requires the datastream scope (see Scopes for more information). +Request Create Stat Type (application/json) +Body + + { + "stat_type": "sales", + "label": "Sales", + "format": "currency", + "data_type": "sum" + } + +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +stat_type: sales (required) (string) - The ket of the stat type, this much exactly match whats uploaded to StoreStats +label: Sales (required) (string) - The human readable name for this stat type +format: currency (string) - How this stat type is formatted +data_type: sum (string) - What kind of data this is (sum or static) ++ stat_type: sales (string) ++ label: Sales (string) ++ format: currency (string) ++ data_type: sum (string) + +Request Create stat type with custom format (application/json) +Body + + { + "stat_type": "deliveries", + "label": "Deliveries", + "format": "%{whole_number} 🚚", + "data_type": "sum" + } + +200 +OK +(application/json) +Body + + { + "id": 162, + "stat_type": "deliveries", + "label": "Deliveries", + "format": "%{whole_number} 🚚", + "data_type": "sum" + } + +Request Create stat type for head counts (application/json) +Body + + { + "stat_type": "required staff", + "label": "Required Staff", + "format": "people", + "data_type": "static" + } + +200 +OK +(application/json) +Body + + { + "id": 162, + "stat_type": "required staff", + "label": "Required Staff", + "format": "people", + "data_type": "static" + } + +PUTUpdate Stat Type +This method requires the datastream scope (see Scopes for more information). +You cannot modify the stat_type field on a Stat Type. You must create a new stat type instead. + +Parameters +id: 162 (number) - The id of the stat type to update +Request Update Stat Type (application/json) +Body + + { + "format": "whole_number" + } + +200 +OK +(application/json) +Attributes: +id: 162 (required) (number) - The id of the data stream join +stat_type: sales (required) (string) - The ket of the stat type, this much exactly match whats uploaded to StoreStats +label: Sales (required) (string) - The human readable name for this stat type +format: currency (string) - How this stat type is formatted +data_type: sum (string) - What kind of data this is (sum or static) ++ format: whole_number (string) + +DELETEDelete Stat Type +This method requires the datastream scope (see Scopes for more information). +This will delete ALL store stats, and data stream joins that have been created with this stat type. Please be careful. + +Parameters +id: 162 (number) - The id of the stat type to delete +200 +OK +(application/json) +Wage Comparison +Wage Comparisons are used to compare the wage cost of employees under different rates of pay. + +Wage Comparison List [/api/v2/wage_comparisons] +GETGet Wage Comparisons +This method requires the cost scope (see Scopes for more information). +200 +OK +(application/json) +Attributes (array): +id: 34 (number, required) - The Wage Comparison ID +from_date: 2020-12-31 (string, required) - The date the Wage comparison Starts +to_date: 2021-11-29 (string, required) - the date the wage comparison Ends +award_template_organisation_id: 56 (number, optional) - The award template organisation id belonging to the desired award. +hourly_rate: 32.47 (number, optional) - The hourly rate of the user +salary: 1234.12 (number, optional) - The weekly salary of the user +award_tags: “Level 8,Full Time,First Aid Certificate” (string, optional) - a list of award tags that applies to the employee. +Wage Comparison [/api/v2/wage_comparisons/{id}] +GETGet Wage Comparison +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the wage comparison to lookup +200 +OK +(application/json) +Attributes: +id: 34 (number, required) - The Wage Comparison ID +from_date: 2020-12-31 (string, required) - The date the Wage comparison Starts +to_date: 2021-11-29 (string, required) - the date the wage comparison Ends +award_template_organisation_id: 56 (number, optional) - The award template organisation id belonging to the desired award. +hourly_rate: 32.47 (number, optional) - The hourly rate of the user +salary: 1234.12 (number, optional) - The weekly salary of the user +award_tags: “Level 8,Full Time,First Aid Certificate” (string, optional) - a list of award tags that applies to the employee. +POSTCreate Wage Comparison +To post a wage comparison for an employee, include the user_id and from_date along with any required optional parameters. Note that from_date and to_date must not overlap for different wage comparisons for the same user. + +This method requires the cost scope (see Scopes for more information). +Parameters +user_id: 1 (required, number) - The ID of the user being compared. +from_date: 2016-03-02 (required, string) - The date comparison starts from. +to_date: 2016-03-02 (optional, string) - The date the comparison goes to, will go forever and ever, if it’s left blank. +hourly_rate: 22.56 (optional, number) - The hourly rate of the comparison. +salary: 70592 (optional, number) - the salary of the comparison. +award_tags: ‘full time, meal break’ (optional, string) - A comma separated list of award tags that apply to the comparison. +award_template_organisation_id: 4 (optional, number) - the relevant award template organisation id. +Request Create Wage Comparison (application/json) +Body + + { + "user_id": 123456, + "from_date": "2016-03-12", + "to_date": "2017-04-12", + "hourly_rate": 22.36, + "salary": 808, + "award_tags": "full time, meal break", + "award_template_organisation_id": 4 + } + +201 +Created +(application/json) +Attributes(WageComparisonData) +user_id: 123456 +from_date: 2016-03-12 +to_date: 2017-04-12 +hourly_rate: 22.36 +salary: 808 +award_template_organisation_id: 4 +award_tags: full time, meal break +Schema +Attributes: +id: 34 (number, required) - The Wage Comparison ID +from_date: 2020-12-31 (string, required) - The date the Wage comparison Starts +to_date: 2021-11-29 (string, required) - the date the wage comparison Ends +award_template_organisation_id: 56 (number, optional) - The award template organisation id belonging to the desired award. +hourly_rate: 32.47 (number, optional) - The hourly rate of the user +salary: 1234.12 (number, optional) - The weekly salary of the user +award_tags: “Level 8,Full Time,First Aid Certificate” (string, optional) - a list of award tags that applies to the employee. +id: (number) +PUTUpdate Wage Comparison +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (required, number) - The id of the wage comparison to update +user_id: 1 (optional, number) - The ID of the user being compared. +from_date: 2016-03-02 (required, string) - The date comparison starts from. +to_date: 2016-03-02 (optional, string) - The date the comparison goes to, will go forever and ever, if it’s left blank. +hourly_rate: 22.56 (optional, number) - The hourly rate of the comparison. +salary: 70592 (optional, number) - the salary of the comparison. +award_tags: ‘full time, meal break’ (optional, string) - A comma separated list of award tags that apply to the comparison. +award_template_organisation_id: 4 (optional, number) - the relevant award template organisation id. +Request Update Wage Comparison (application/json) +Body + + { + "user_id": 123456, + "from_date": "2016-03-12", + "to_date": "2017-04-12", + "hourly_rate": 22.36, + "salary": 808, + "award_tags": "full time, meal break", + "award_template_organisation_id": 4 + } + +200 +OK +(application/json) +Attributes(WageComparisonData) +user_id: 123456 +from_date: 2016-03-12 +to_date: 2017-04-12 +hourly_rate: 22.36 +salary: 808 +award_template_organisation_id: 4 +award_tags: full time, meal break +Schema +Attributes: +id: 34 (number, required) - The Wage Comparison ID +from_date: 2020-12-31 (string, required) - The date the Wage comparison Starts +to_date: 2021-11-29 (string, required) - the date the wage comparison Ends +award_template_organisation_id: 56 (number, optional) - The award template organisation id belonging to the desired award. +hourly_rate: 32.47 (number, optional) - The hourly rate of the user +salary: 1234.12 (number, optional) - The weekly salary of the user +award_tags: “Level 8,Full Time,First Aid Certificate” (string, optional) - a list of award tags that applies to the employee. +id: (number) +DELETEDelete Wage Comparison +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The id of the wage comparison to delete +200 +OK +(application/json) +Wage Comparison Outer Limits User Setting [/api/v2/wage_comparisons/{id}/outer_limits] +GETGet Wage Comparison Outer Limits User Setting +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The ID of the wage comparison that has the outer limit user setting. +200 +OK +(application/json) +Attributes(OuterLimitsUserSettingData) +Schema(OuterLimitsUserSettingData) +404 +Not Found +(application/json) +Attributes +error: No known Wage Comparison with id 162 +404 +Not Found +(application/json) +Attributes +error: Wage comparison does not have any outer limits set +POSTCreate Wage Comparison Outer Limits User Setting +If a wage comparison does not have outer limits set, this endpoint creates them. If the wage comparison already has outer limits, an error will be returned. + +The outer limit user setting that is created will also be returned in the response. + +The request fields are as follows: + +penalty_hours number: (required) Example: 6. +The outer limit of penalty hours. + +overtime_hours number: (required) Example: 6. +The outer limit of overtime hours. + +averaging_cadence string: (required) Example: pay_cycle. +The averaging cadence to use. Must be one of pay_cycle, overtime_averaging, or other. + +custom_cycle_length string: (optional) Example: weekly. +If averaging_cadence is set to other, this value is required and is the cycle length that will be used. Otherwise, the value is ignored. Must be one of weekly, fortnightly, three_weekly, or four_weekly. + +custom_anchor_date date: (optional) Example: 2022-01-24. +If averaging_cadence is set to other, this value is required and is used as the anchor for each cycle. For example, if the anchor date is set to Monday 24th January and the cycle length is weekly, the outer limit test period will be each Monday to the following Sunday. + +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The ID of the wage comparison that the outer limit setting will be created for. +Request (application/json) +Body + + { + "penalty_hours": 6, + "overtime_hours": 6, + "averaging_cadence": "other", + "custom_cycle_length": "weekly", + "custom_anchor_date": "2022-01-24" + } + +200 +OK +(application/json) +Attributes(OuterLimitsUserSettingData) +Schema(OuterLimitsUserSettingData) +409 +Conflict +(application/json) +Attributes +error: Wage comparison already has outer limits set +404 +Not Found +(application/json) +Attributes +error: No known Wage Comparison with id 162 +PUTUpdate Wage Comparison Outer Limits User Setting +Updates an existing outer limits user setting with new data. This endpoint supports partial updates - you only need to specify the fields that you wish to update. + +The full outer limits user setting will be returned in the response. + +See Create Wage Comparison Outer Limits User Settings for more information about the request fields. + +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The ID of the wage comparison that the outer limit setting will be created for. +Request (application/json) +Body + + { + "penalty_hours": 9 + } + +200 +OK +(application/json) +Attributes(OuterLimitsUserSettingData) +Schema(OuterLimitsUserSettingData) +404 +Not Found +(application/json) +Attributes +error: No known Wage Comparison with id 162 +404 +Not Found +(application/json) +Attributes +error: Wage comparison does not have any outer limits set +DELETEDelete Wage Comparison Outer Limits User Setting +This endpoint deletes the outer limits settings for a wage comparison. If the deletion was successful, the response is empty. + +This method requires the cost scope (see Scopes for more information). +Parameters +id: 162 (number) - The ID of the wage comparison that the outer limit setting will be created for. +Response 200 (application/json) + +404 +Not Found +(application/json) +Attributes +error: No known Wage Comparison with id 162 +404 +Not Found +(application/json) +Attributes +error: Wage comparison does not have any outer limits set +Recommended Hours +Recommended Hours refer to the optimal number of hours a roster should have dependent on Workforce’s congitive tool suite. + +Recommended Hours [/api/v2/recommended_hours] +GETGet Recommended Hours +Get the recommended hours of work for a department, for a given date range. You need to provide all the listed paramters below (department_id, from_date, and to_date). + +The endpoint recommends hours by date and dependent on your cognitive configurations for the department you are requesting. In cases where your business hours end after midnight, you can use the from_time and to_time parameters to specific a time range alongside the dates. The from_time will be used in conjunction with the from_date, to_time with the to_date. +This method requires the roster scope (see Scopes for more information). +Parameters +department_id: 1234 (required, number) - The ID of the department. +from_date: 2016-03-02 (required, string) - The starting date of the range. +to_date: 2016-03-02 (required, string) - The date the range will go to. +from_time: 06:00 (optional, string) - The starting time of the range. +to_time: 17:30 (optional, string) - The time the range will go to. +full_increments: true (optional, boolean) - This will show recommended hours by 15 minute increments. +in_minutes: 2016-03-02 (optional, boolean) - This will show recommended hours as minute values. +200 +OK +(application/json) +Attributes: +department_id: 1234 (number, required) - The ID of the department +department_name: Delivery Team (string, required) - The name of the department +total_recommended_hours_for_date_range: 110.0 (string) - The total recommended hours for the date range +recommended_hours_by_date (object) +2016-04-01: 31.0 (number, required) - The number of hours for that date. +2016-04-02: 10.0 (number, required) - The number of hours for that date. +2016-04-03: 25.0 (number, required) - The number of hours for that date. +Rostered Days Off +Rostered Days Off (RDOs) represent planned days off for employees. They can be paid or unpaid and are used within rosters to streamline scheduling and ensure compliance with overtime rules. + +Rostered Days Off are utilised for: + +Quick building of rosters. +Automatic application of overtime rules where applicable. +To view or manage rostered days off, the user must be a team manager, roster manager, or admin. In the case of the team manager, the shift must be for someone in the user’s managed teams. +Rostered Days Off [/api/v2/rostered_days_off] +GETGet Rostered Days Off +Get rostered days off by roster ID, user ID, or by specific rostered day off IDs. At least one filter parameter is required. Multiple parameters can be used at once. + +This method requires the roster scope (see Scopes for more information). +Parameters +roster_id: 123 (optional, integer) - ID of the roster +user_id: 456 (optional, integer) - ID of the user to filter rostered days off +ids: 1,2,3 (optional, string) - Comma-separated list of rostered day off IDs +page: 1 (optional, number) - The page number for your rostered days off list +page_size: 50 (optional, number) - The number of rostered days off retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 123 (required) (number) - The ID of the rostered day off +user_id: 456 (number) - The ID of the user that the rostered day off belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the rostered day off belongs to +start: 1456902000 (number) - The start of the rostered day off in unix time +finish: 1456916400 (number) - The finish of the rostered day off in unix time +department_id: 111 (number) - The department (team) ID for the rostered day off +updated_at: 1519621685 (number) When the rostered day off record was last updated in unix time +POSTCreate Rostered Day Off +Create a new rostered day off for a user. + +This method requires the roster scope (see Scopes for more information). +Request Create Rostered Day Off (application/json) +Body + + { + "user_id": 456, + "date": "2024-03-15", + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 (optional) + } + +200 +OK +(application/json) +Body + + { + "id": 101, + "user_id": 456, + "daily_schedule_id": 202, + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 + } + +Schema + +Attributes: +id: 123 (required) (number) - The ID of the rostered day off +user_id: 456 (number) - The ID of the user that the rostered day off belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the rostered day off belongs to +start: 1456902000 (number) - The start of the rostered day off in unix time +finish: 1456916400 (number) - The finish of the rostered day off in unix time +department_id: 111 (number) - The department (team) ID for the rostered day off +updated_at: 1519621685 (number) When the rostered day off record was last updated in unix time +id: (number) +Rostered Day Off [/api/v2/rostered_days_off/{id}] +GETGet Rostered Day Off +Get a specific rostered day off by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the rostered day off +200 +OK +(application/json) +Attributes: +id: 123 (required) (number) - The ID of the rostered day off +user_id: 456 (number) - The ID of the user that the rostered day off belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the rostered day off belongs to +start: 1456902000 (number) - The start of the rostered day off in unix time +finish: 1456916400 (number) - The finish of the rostered day off in unix time +department_id: 111 (number) - The department (team) ID for the rostered day off +updated_at: 1519621685 (number) When the rostered day off record was last updated in unix time +DELETEDelete Rostered Day Off +Delete a rostered day off by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the rostered day off +200 +OK +(application/json) +Sleepovers +Sleepovers represent periods where an employee is rostered for a sleepover shift, typically in care services. They are used within rosters alongside regular schedules and rostered days off. + +Sleepovers are utilised for: + +Quick building of rosters. +Automatic application of overtime rules where applicable. +To view or manage sleepovers, the user must be a team manager, roster manager, or admin. In the case of the team manager, the shift must be for someone in the user’s managed teams. +Sleepovers [/api/v2/sleepovers] +GETGet Sleepovers +Get sleepovers by roster ID, user ID, or by specific sleepover IDs. At least one filter parameter is required. Multiple parameters can be used at once. + +This method requires the roster scope (see Scopes for more information). +Parameters +roster_id: 123 (optional, integer) - ID of the roster +user_id: 456 (optional, integer) - ID of the user to filter sleepovers +ids: 1,2,3 (optional, string) - Comma-separated list of sleepover IDs +page: 1 (optional, number) - The page number for your sleepovers list +page_size: 50 (optional, number) - The number of sleepovers retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 123 (required) (number) - The ID of the sleepover +user_id: 456 (number) - The ID of the user that the sleepover belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the sleepover belongs to +start: 1456902000 (number) - The start of the sleepover in unix time +finish: 1456916400 (number) - The finish of the sleepover in unix time +department_id: 111 (number) - The department (team) ID for the sleepover +updated_at: 1519621685 (number) When the sleepover record was last updated in unix time +POSTCreate Sleepover +Create a new sleepover for a user. + +This method requires the roster scope (see Scopes for more information). +Request Create Sleepover (application/json) +Body + + { + "user_id": 456, + "date": "2024-03-15", + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 (optional) + } + +200 +OK +(application/json) +Body + + { + "id": 101, + "user_id": 456, + "daily_schedule_id": 202, + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 + } + +Schema + +Attributes: +id: 123 (required) (number) - The ID of the sleepover +user_id: 456 (number) - The ID of the user that the sleepover belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the sleepover belongs to +start: 1456902000 (number) - The start of the sleepover in unix time +finish: 1456916400 (number) - The finish of the sleepover in unix time +department_id: 111 (number) - The department (team) ID for the sleepover +updated_at: 1519621685 (number) When the sleepover record was last updated in unix time +id: (number) +Sleepover [/api/v2/sleepovers/{id}] +GETGet Sleepover +Get a specific sleepover by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the sleepover +200 +OK +(application/json) +Attributes: +id: 123 (required) (number) - The ID of the sleepover +user_id: 456 (number) - The ID of the user that the sleepover belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the sleepover belongs to +start: 1456902000 (number) - The start of the sleepover in unix time +finish: 1456916400 (number) - The finish of the sleepover in unix time +department_id: 111 (number) - The department (team) ID for the sleepover +updated_at: 1519621685 (number) When the sleepover record was last updated in unix time +DELETEDelete Sleepover +Delete a sleepover by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the sleepover +200 +OK +(application/json) +On Calls +On calls represent periods where an employee is rostered to be on call. They are used within rosters alongside regular schedules and rostered days off. + +On calls are utilised for: + +Quick building of rosters. +Automatic application of overtime rules where applicable. +To view or manage on calls, the user must be a team manager, roster manager, or admin. In the case of the team manager, the shift must be for someone in the user’s managed teams. +On Calls [/api/v2/on_calls] +GETGet On Calls +Get on calls by roster ID, user ID, or by specific on call IDs. At least one filter parameter is required. Multiple parameters can be used at once. + +This method requires the roster scope (see Scopes for more information). +Parameters +roster_id: 123 (optional, integer) - ID of the roster +user_id: 456 (optional, integer) - ID of the user to filter on calls +ids: 1,2,3 (optional, string) - Comma-separated list of on call IDs +page: 1 (optional, number) - The page number for your on calls list +page_size: 50 (optional, number) - The number of on calls retrieved per page. Maximum 100 per page. +200 +OK +(application/json) +Attributes (array): +id: 123 (required) (number) - The ID of the on call +user_id: 456 (number) - The ID of the user that the on call belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the on call belongs to +start: 1456902000 (number) - The start of the on call in unix time +finish: 1456916400 (number) - The finish of the on call in unix time +department_id: 111 (number) - The department (team) ID for the on call +updated_at: 1519621685 (number) When the on call record was last updated in unix time +POSTCreate On Call +Create a new on call for a user. + +This method requires the roster scope (see Scopes for more information). +Request Create On Call (application/json) +Body + + { + "user_id": 456, + "date": "2024-03-15", + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 (optional) + } + +200 +OK +(application/json) +Body + + { + "id": 101, + "user_id": 456, + "daily_schedule_id": 202, + "start": 1710500400, + "finish": 1710522000, + "department_id": 789 + } + +Schema + +Attributes: +id: 123 (required) (number) - The ID of the on call +user_id: 456 (number) - The ID of the user that the on call belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the on call belongs to +start: 1456902000 (number) - The start of the on call in unix time +finish: 1456916400 (number) - The finish of the on call in unix time +department_id: 111 (number) - The department (team) ID for the on call +updated_at: 1519621685 (number) When the on call record was last updated in unix time + +id: (number) +On Call [/api/v2/on_calls/{id}] +GETGet On Call +Get a specific on call by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the on call +200 +OK +(application/json) +Attributes: +id: 123 (required) (number) - The ID of the on call +user_id: 456 (number) - The ID of the user that the on call belongs to +daily_schedule_id: 789 (number) - The ID of the daily schedule that the on call belongs to +start: 1456902000 (number) - The start of the on call in unix time +finish: 1456916400 (number) - The finish of the on call in unix time +department_id: 111 (number) - The department (team) ID for the on call +updated_at: 1519621685 (number) When the on call record was last updated in unix time +DELETEDelete On Call +Delete an on call by ID. + +This method requires the roster scope (see Scopes for more information). +Parameters +id: 101 (required, integer) - ID of the on call +200 +OK +(application/json) +Payroll Payslips +Payslips contain information about an employee’s earnings for a specific pay period. They include details about gross pay, tax, deductions, superannuation, and net pay. + +Payslips [/api/v2/payroll/payslips] +GETGet Payslips +Get all payslips for the current organisation. Results are paginated and sorted by pay run start date (newest first), then by payslip ID for consistent ordering within the same pay run. + +Request +Headers Authorization: Bearer {access_token} +Parameters +page: 1 (number, optional) - The page of results to return +page_size: 100 (number, optional) - The number of results per page (max 100) +pay_run_id: 456 (number, optional) - Filter payslips by pay run ID. When provided, only payslips belonging to the specified pay run will be returned. +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the payslip +pay_run_id: 456 (number, required) - The ID of the pay run this payslip belongs to +user_id: 789 (number, required) - The ID of the user this payslip is for +from_date: 2023-01-01 (string, required) - The start date of the pay period +to_date: 2023-01-15 (string, required) - The end date of the pay period +payment_date: 2023-01-20 (string, required) - The date when payment will be made +gross_pay: 1500.00 (number, required) - The total gross pay amount +tax: 300.00 (number, required) - The total tax amount +deductions: 100.00 (number, required) - The total deductions amount +super: 142.50 (number, required) - The total superannuation amount +net_pay: 1100.00 (number, required) - The net pay amount (after tax and deductions) +email_sent: true (boolean, required) - Whether the payslip has been emailed to the employee +kiwi_saver: 90.00 (number) - NZ only. The total KiwiSaver amount (employee deduction plus employer contribution) +employee_kiwi_saver_deduction: 45.00 (number) - NZ only. The total KiwiSaver amount deducted from the employee +employer_kiwi_saver_contribution: 45.00 (number) - NZ only. The total KiwiSaver amount contributed by the employer +Get Specific Payslip [GET /api/v2/payroll/payslips/{id}] +Get a specific payslip by ID. + +Parameters +id: 123 (number, required) - The ID of the payslip +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the payslip +pay_run_id: 456 (number, required) - The ID of the pay run this payslip belongs to +user_id: 789 (number, required) - The ID of the user this payslip is for +from_date: 2023-01-01 (string, required) - The start date of the pay period +to_date: 2023-01-15 (string, required) - The end date of the pay period +payment_date: 2023-01-20 (string, required) - The date when payment will be made +gross_pay: 1500.00 (number, required) - The total gross pay amount +tax: 300.00 (number, required) - The total tax amount +deductions: 100.00 (number, required) - The total deductions amount +super: 142.50 (number, required) - The total superannuation amount +net_pay: 1100.00 (number, required) - The net pay amount (after tax and deductions) +email_sent: true (boolean, required) - Whether the payslip has been emailed to the employee +kiwi_saver: 90.00 (number) - NZ only. The total KiwiSaver amount (employee deduction plus employer contribution) +employee_kiwi_saver_deduction: 45.00 (number) - NZ only. The total KiwiSaver amount deducted from the employee +employer_kiwi_saver_contribution: 45.00 (number) - NZ only. The total KiwiSaver amount contributed by the employer +404 +Not Found +(application/json) +Body { “error”: “Payslip not found!” } +Get Payslips For User [GET /api/v2/payroll/payslips/for/{user_id}] +Get all payslips for a specific user. + +Parameters +user_id: 456 (number, required) - The ID of the user +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the payslip +pay_run_id: 456 (number, required) - The ID of the pay run this payslip belongs to +user_id: 789 (number, required) - The ID of the user this payslip is for +from_date: 2023-01-01 (string, required) - The start date of the pay period +to_date: 2023-01-15 (string, required) - The end date of the pay period +payment_date: 2023-01-20 (string, required) - The date when payment will be made +gross_pay: 1500.00 (number, required) - The total gross pay amount +tax: 300.00 (number, required) - The total tax amount +deductions: 100.00 (number, required) - The total deductions amount +super: 142.50 (number, required) - The total superannuation amount +net_pay: 1100.00 (number, required) - The net pay amount (after tax and deductions) +email_sent: true (boolean, required) - Whether the payslip has been emailed to the employee +kiwi_saver: 90.00 (number) - NZ only. The total KiwiSaver amount (employee deduction plus employer contribution) +employee_kiwi_saver_deduction: 45.00 (number) - NZ only. The total KiwiSaver amount deducted from the employee +employer_kiwi_saver_contribution: 45.00 (number) - NZ only. The total KiwiSaver amount contributed by the employer +404 +Not Found +(application/json) +Body { “error”: “Employee not found!” } +Payroll Deduction Types +Deduction types define categories of deductions that can be applied to employee pay. They include information about tax reduction effects and payment details. + +Deduction Types [/api/v2/payroll/deduction_types] +GETGet Deduction Types +Get all deduction types for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the deduction type +name: Health Insurance (string, required) - The name of the deduction type +stp_category: H (string) - The STP reporting category code +reduces_tax: true (boolean, required) - Whether this deduction reduces taxable income +reduces_workcover: false (boolean, required) - Whether this deduction reduces workcover calculations +reduces_payroll_tax: false (boolean, required) - Whether this deduction reduces payroll tax calculations +payroll_account_id: 456 (number, required) - The ID of the payroll account this deduction is linked to +bank_detail (object) - Bank account details for the deduction +bsb: 123456 (string) - BSB number +account_name: Health Insurance Ltd (string) - Account name +account_number: 12345678 (string) - Account number +Get Specific Deduction Type [GET /api/v2/payroll/deduction_types/{id}] +Get a specific deduction type by ID. + +Parameters +id: 123 (number, required) - The ID of the deduction type +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction type +name: Health Insurance (string, required) - The name of the deduction type +stp_category: H (string) - The STP reporting category code +reduces_tax: true (boolean, required) - Whether this deduction reduces taxable income +reduces_workcover: false (boolean, required) - Whether this deduction reduces workcover calculations +reduces_payroll_tax: false (boolean, required) - Whether this deduction reduces payroll tax calculations +payroll_account_id: 456 (number, required) - The ID of the payroll account this deduction is linked to +bank_detail (object) - Bank account details for the deduction +bsb: 123456 (string) - BSB number +account_name: Health Insurance Ltd (string) - Account name +account_number: 12345678 (string) - Account number +404 +Not Found +(application/json) +Body { “error”: “Deduction Type not found!” } +POSTCreate Deduction Type +Create a new deduction type for the organisation. + +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “name”: “Salary Sacrifice”, “stp_category”: “O”, “reduces_tax”: true, “reduces_workcover”: false, “reduces_payroll_tax”: false, “payroll_account_id”: 123, “bank_detail”: { “bsb”: “123456”, “account_name”: “Salary Sacrifice Ltd”, “account_number”: “12345678” } } + +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction type +name: Health Insurance (string, required) - The name of the deduction type +stp_category: H (string) - The STP reporting category code +reduces_tax: true (boolean, required) - Whether this deduction reduces taxable income +reduces_workcover: false (boolean, required) - Whether this deduction reduces workcover calculations +reduces_payroll_tax: false (boolean, required) - Whether this deduction reduces payroll tax calculations +payroll_account_id: 456 (number, required) - The ID of the payroll account this deduction is linked to +bank_detail (object) - Bank account details for the deduction +bsb: 123456 (string) - BSB number +account_name: Health Insurance Ltd (string) - Account name +account_number: 12345678 (string) - Account number +400 +Bad Request +(application/json) +Body { “error”: “Payroll Account ID is required!” } +Update Deduction Type [PUT /api/v2/payroll/deduction_types/{id}] +Update an existing deduction type. + +Parameters +id: 123 (number, required) - The ID of the deduction type +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “name”: “Updated Salary Sacrifice”, “reduces_tax”: false, “bank_detail”: { “bsb”: “654321”, “account_name”: “New Salary Sacrifice Ltd”, “account_number”: “87654321” } } + +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction type +name: Health Insurance (string, required) - The name of the deduction type +stp_category: H (string) - The STP reporting category code +reduces_tax: true (boolean, required) - Whether this deduction reduces taxable income +reduces_workcover: false (boolean, required) - Whether this deduction reduces workcover calculations +reduces_payroll_tax: false (boolean, required) - Whether this deduction reduces payroll tax calculations +payroll_account_id: 456 (number, required) - The ID of the payroll account this deduction is linked to +bank_detail (object) - Bank account details for the deduction +bsb: 123456 (string) - BSB number +account_name: Health Insurance Ltd (string) - Account name +account_number: 12345678 (string) - Account number +404 +Not Found +(application/json) +Body { “error”: “Deduction Type not found!” } +Delete Deduction Type [DELETE /api/v2/payroll/deduction_types/{id}] +Delete a deduction type. + +Parameters +id: 123 (number, required) - The ID of the deduction type +Request +Headers Authorization: Bearer {access_token} +Response 200 (application/json) + +404 +Not Found +(application/json) +Body { “error”: “Deduction Type not found!” } +Payroll Accounts +Payroll accounts are used to track different types of financial transactions related to payroll, such as expense accounts and liability accounts. + +Accounts [/api/v2/payroll/accounts] +GETGet Accounts +Get all payroll accounts for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body [ { “id”: 123, “name”: “Wages Expense”, “code”: “6000”, “payroll_super_expense_account_id”: 456 }, { “id”: 456, “name”: “Superannuation Expense”, “code”: “6100”, “payroll_super_expense_account_id”: null } ] +Payroll Department Tracking Categories +Department tracking categories link departments (teams) to payroll tracking categories, allowing you to assign specific values to each department for payroll reporting. + +Department Tracking Categories [/api/v2/payroll/department_tracking_categories] +GETGet Department Tracking Categories +Get all department tracking categories for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 34 (number, required) - The department tracking category’s ID +department_id: 44 (number, required) - The department’s ID +payroll_tracking_categories_id: (number, required) - The tracking category’s ID +value: Management (string, required) - The text field shared by the department and the tracking category +POSTCreate Department Tracking Category +Create a new department tracking category. + +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “payroll_tracking_categories_id”: 123, “department_id”: 456, “value”: “Management” } + +200 +OK +(application/json) +Schema +Attributes: +id: 34 (number, required) - The department tracking category’s ID +department_id: 44 (number, required) - The department’s ID +payroll_tracking_categories_id: (number, required) - The tracking category’s ID +value: Management (string, required) - The text field shared by the department and the tracking category +404 +Not Found +(application/json) +Body { “error”: “Tracking category not found!” } +Update Department Tracking Category [PUT /api/v2/payroll/department_tracking_categories/{id}] +Update an existing department tracking category. + +Parameters +id: 123 (number, required) - The ID of the department tracking category +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “value”: “Administration” } + +200 +OK +(application/json) +Schema +Attributes: +id: 34 (number, required) - The department tracking category’s ID +department_id: 44 (number, required) - The department’s ID +payroll_tracking_categories_id: (number, required) - The tracking category’s ID +value: Management (string, required) - The text field shared by the department and the tracking category +404 +Not Found +(application/json) +Body { “error”: “Department tracking category not found!” } +Delete Department Tracking Category [DELETE /api/v2/payroll/department_tracking_categories/{id}] +Delete a department tracking category. + +Parameters +id: 123 (number, required) - The ID of the department tracking category +Request +Headers Authorization: Bearer {access_token} +Response 200 (application/json) + +404 +Not Found +(application/json) +Body { “error”: “Department tracking category not found!” } +Payroll Pay Runs +Pay runs represent a complete payroll processing cycle. They include information about the pay period, payment date, and current status. + +Pay Runs [/api/v2/payroll/pay_runs] +GETGet Pay Runs +Get all pay runs for the current organisation, with optional filtering by date range, status, or pay group. + +Parameters +from: 2023-01-01 (date, optional) - Start date for filtering pay runs +to: 2023-01-31 (date, optional) - End date for filtering pay runs. Must be provided if from is provided +status: open (string, optional) - Filter by pay run status +pay_group_id: 123 (number, optional) - Filter by pay group ID +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body [ { “id”: 123, “company_file_name”: “My Company”, “company_file_id”: 789, “pay_group_id”: 456, “start_date”: “2023-01-01”, “finish_date”: “2023-01-15”, “payment_date”: “2023-01-20”, “status”: “open” }, { “id”: 124, “company_file_name”: “My Company”, “company_file_id”: 789, “pay_group_id”: 456, “start_date”: “2023-01-16”, “finish_date”: “2023-01-31”, “payment_date”: “2023-02-05”, “status”: “draft” } ] +Pay Run [/api/v2/payroll/pay_runs/{id}] +GETGet Pay Run +Get a specific pay run by ID. + +Parameters +id: 123 (number, required) - Pay run ID +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body { “id”: 123, “company_file_name”: “My Company”, “company_file_id”: 789, “pay_group_id”: 456, “start_date”: “2023-01-01”, “finish_date”: “2023-01-15”, “payment_date”: “2023-01-20”, “status”: “open” } +404 +Not Found +(application/json) +Body { “error”: “Pay run not found!” } +Pay Run Journal [/api/v2/payroll/pay_runs/{id}/journal] +GETGet Pay Run Journal +Get the accounting journal entries for a specific pay run. The journal includes all debit and credit lines that would be posted to your accounting system for this pay run. + +Parameters +id: 123 (number, required) - Pay run ID +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body { “id”: 123, “company_file_name”: “My Company”, “company_file_id”: 789, “pay_group_id”: 456, “start_date”: “2023-01-01”, “finish_date”: “2023-01-15”, “payment_date”: “2023-01-20”, “status”: “open”, “debit_lines”: [ { “account”: { “id”: 1001, “name”: “Wages Expense”, “code”: “WAG-01” }, “amount”: “2500.00”, “tracking_categories”: [“Sydney”, “Manager”], “narration”: “” }, { “account”: { “id”: 1002, “name”: “Overtime”, “code”: “WAG-02” }, “amount”: “375.00”, “tracking_categories”: [“Sydney”, “Manager”], “narration”: “” }, { “account”: { “id”: 1003, “name”: “Superannuation”, “code”: “SUP-01” }, “amount”: “287.50”, “tracking_categories”: [“Sydney”, “Manager”], “narration”: “” } ], “credit_lines”: [ { “account”: { “id”: 2001, “name”: “Wages Payable”, “code”: “LIB-01” }, “amount”: “2300.00”, “tracking_categories”: [””, “”], “narration”: “” }, { “account”: { “id”: 2002, “name”: “Withholding Payable”, “code”: “LIB-02” }, “amount”: “575.00”, “tracking_categories”: [””, “”], “narration”: “” }, { “account”: { “id”: 2003, “name”: “Super Payable”, “code”: “LIB-03” }, “amount”: “287.50”, “tracking_categories”: [””, “”], “narration”: “” } ] } +404 +Not Found +(application/json) +Body { “error”: “Pay run not found!” } +Journal Response Fields +id: The ID of the pay run +company_file_name: The name of the payroll company file +company_file_id: The ID of the payroll company file +pay_group_id: The ID of the pay group +start_date: Start date of the pay period +finish_date: End date of the pay period +payment_date: The date when payment will be made +status: The current status of the pay run +debit_lines: Array of journal entries that represent debits (expenses and assets) +credit_lines: Array of journal entries that represent credits (liabilities and income) +Each journal line includes: + +account: Object containing account details +id: The ID of the payroll account +name: Name of the payroll account +code: Code/reference for the account +amount: Monetary amount in dollars, as a string +tracking_categories: Array of tracking category values (empty strings if no tracking categories are configured) +narration: Additional description or notes for the journal entry +Pay Run Payslips [/api/v2/payroll/pay_runs/{id}/payslips] +GETGet Payslips for Pay Run +Get all payslips belonging to a specific pay run. Results are paginated and sorted by payslip ID for consistent ordering. + +Parameters +id: 123 (number, required) - Pay run ID +page: 1 (number, optional) - The page of results to return +page_size: 100 (number, optional) - The number of results per page (max 100) +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the payslip +pay_run_id: 456 (number, required) - The ID of the pay run this payslip belongs to +user_id: 789 (number, required) - The ID of the user this payslip is for +from_date: 2023-01-01 (string, required) - The start date of the pay period +to_date: 2023-01-15 (string, required) - The end date of the pay period +payment_date: 2023-01-20 (string, required) - The date when payment will be made +gross_pay: 1500.00 (number, required) - The total gross pay amount +tax: 300.00 (number, required) - The total tax amount +deductions: 100.00 (number, required) - The total deductions amount +super: 142.50 (number, required) - The total superannuation amount +net_pay: 1100.00 (number, required) - The net pay amount (after tax and deductions) +email_sent: true (boolean, required) - Whether the payslip has been emailed to the employee +kiwi_saver: 90.00 (number) - NZ only. The total KiwiSaver amount (employee deduction plus employer contribution) +employee_kiwi_saver_deduction: 45.00 (number) - NZ only. The total KiwiSaver amount deducted from the employee +employer_kiwi_saver_contribution: 45.00 (number) - NZ only. The total KiwiSaver amount contributed by the employer +404 +Not Found +(application/json) +Body { “error”: “Pay run not found!” } +Payroll Tracking Categories +Tracking categories are used in payroll systems to categorize expenses and income for reporting purposes. They can be linked to departments via department tracking categories. + +Tracking Categories [/api/v2/payroll/tracking_categories] +GETGet Tracking Categories +Get all tracking categories for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 34 (number, required) - The tracking category’s ID +payroll_integration_id: 29 (number, required) - The associated payroll integration ID +name: Cost Centre (string, required) - The name of the tracking category +Announcements +Announcements are messages posted to announcement rooms within your organisation. Use these endpoints to manage announcements and announcement rooms. + +Announcement List [/api/v2/announcements] +GETGet Announcements +This method requires the communication scope (see Scopes for more information). +Parameters +communication_room_id: 456 (number, optional) - Filter by announcement room ID +updated_after: 1451606400 (number, optional) - Only return announcements updated after this unix timestamp +page: 1 (number, optional) - Page number for pagination +page_size: 25 (number, optional) - Number of results per page (1-100) +200 +OK +(application/json) +Attributes (array): +id: 789 (number, required) - The announcement message ID +title: Office Closure (string, required) - The title of the announcement +content: The office will be closed on Friday for maintenance. (string, optional) - The plain text body content of the announcement +rich_content:
The office will be closed on Friday for maintenance.
(string, optional) - The rich text HTML content of the announcement +sender_id: 123456 (number, required) - The ID of the user who posted the announcement +communication_room_id: 456 (number, required) - The ID of the announcement room +communication_room_name: Company Announcements (string, required) - The name of the announcement room +created_at: 1456902000 (number, required) - When the announcement was created (unix timestamp) +POSTCreate Announcement +This method requires the communication scope (see Scopes for more information). +Request (application/json) +Body + + { + "communication_room_id": 456, + "title": "Office Closure", + "content": "The office will be closed on Friday for maintenance.", + "sender_id": 123456, + "created_at": 1456902000, + "notify": false + } + +201 +Created +(application/json) +Attributes: +id: 789 (number, required) - The announcement message ID +title: Office Closure (string, required) - The title of the announcement +content: The office will be closed on Friday for maintenance. (string, optional) - The plain text body content of the announcement +rich_content:
The office will be closed on Friday for maintenance.
(string, optional) - The rich text HTML content of the announcement +sender_id: 123456 (number, required) - The ID of the user who posted the announcement +communication_room_id: 456 (number, required) - The ID of the announcement room +communication_room_name: Company Announcements (string, required) - The name of the announcement room +created_at: 1456902000 (number, required) - When the announcement was created (unix timestamp) +Announcement [/api/v2/announcements/{id}] +GETGet Announcement +This method requires the communication scope (see Scopes for more information). +Parameters +id: 789 (number) - The ID of the announcement +200 +OK +(application/json) +Attributes: +id: 789 (number, required) - The announcement message ID +title: Office Closure (string, required) - The title of the announcement +content: The office will be closed on Friday for maintenance. (string, optional) - The plain text body content of the announcement +rich_content:
The office will be closed on Friday for maintenance.
(string, optional) - The rich text HTML content of the announcement +sender_id: 123456 (number, required) - The ID of the user who posted the announcement +communication_room_id: 456 (number, required) - The ID of the announcement room +communication_room_name: Company Announcements (string, required) - The name of the announcement room +created_at: 1456902000 (number, required) - When the announcement was created (unix timestamp) +Announcement Room List [/api/v2/announcement_rooms] +GETGet Announcement Rooms +Returns all announcement rooms accessible to the authenticated user. + +This method requires the communication scope (see Scopes for more information). +Parameters +updated_after: 1451606400 (number, optional) - Only return rooms updated after this unix timestamp +page: 1 (number, optional) - Page number for pagination +page_size: 25 (number, optional) - Number of results per page (1-100) +200 +OK +(application/json) +Attributes (array): +id: 456 (number, required) - The announcement room ID +name: Warehouse Announcements (string, required) - The name of the announcement room +organisation_id: 23100 (number, required) - The ID of the organisation the room belongs to +team_ids: 123, 456 (array[number], required) - Team IDs the room is assigned to +location_ids: 78, 90 (array[number], required) - Location IDs the room is assigned to +archived: false (boolean, required) - Whether the room has been archived +created_at: 1456902000 (number, required) - When the room was created (unix timestamp) +updated_at: 1456902000 (number, required) - When the room was last updated (unix timestamp) +POSTCreate Announcement Room +Creates a new announcement room for the authenticated user’s organisation. Specify either team_ids or location_ids (not both) to define which teams or locations the room is visible to. + +The caller must have permission to create announcement rooms in their organisation. + +This method requires the communication scope (see Scopes for more information). +Request (application/json) +Body + + { + "name": "Warehouse Announcements", + "team_ids": [123, 456] + } + +201 +Created +(application/json) +Attributes: +id: 456 (number, required) - The announcement room ID +name: Warehouse Announcements (string, required) - The name of the announcement room +organisation_id: 23100 (number, required) - The ID of the organisation the room belongs to +team_ids: 123, 456 (array[number], required) - Team IDs the room is assigned to +location_ids: 78, 90 (array[number], required) - Location IDs the room is assigned to +archived: false (boolean, required) - Whether the room has been archived +created_at: 1456902000 (number, required) - When the room was created (unix timestamp) +updated_at: 1456902000 (number, required) - When the room was last updated (unix timestamp) +Announcement Room [/api/v2/announcement_rooms/{id}] +GETGet Announcement Room +Returns a single announcement room by ID. + +This method requires the communication scope (see Scopes for more information). +Parameters +id: 456 (number) - The ID of the announcement room +200 +OK +(application/json) +Attributes: +id: 456 (number, required) - The announcement room ID +name: Warehouse Announcements (string, required) - The name of the announcement room +organisation_id: 23100 (number, required) - The ID of the organisation the room belongs to +team_ids: 123, 456 (array[number], required) - Team IDs the room is assigned to +location_ids: 78, 90 (array[number], required) - Location IDs the room is assigned to +archived: false (boolean, required) - Whether the room has been archived +created_at: 1456902000 (number, required) - When the room was created (unix timestamp) +updated_at: 1456902000 (number, required) - When the room was last updated (unix timestamp) +PUTUpdate Announcement Room +Updates an existing announcement room. Specify either team_ids or location_ids (not both) to redefine which teams or locations the room is visible to. Cannot update archived or organisation-wide rooms. + +This method requires the communication scope (see Scopes for more information). +Parameters +id: 456 (number) - The ID of the announcement room +Request (application/json) +Body + + { + "name": "Updated Warehouse Announcements", + "team_ids": [123, 789] + } + +200 +OK +(application/json) +Attributes: +id: 456 (number, required) - The announcement room ID +name: Warehouse Announcements (string, required) - The name of the announcement room +organisation_id: 23100 (number, required) - The ID of the organisation the room belongs to +team_ids: 123, 456 (array[number], required) - Team IDs the room is assigned to +location_ids: 78, 90 (array[number], required) - Location IDs the room is assigned to +archived: false (boolean, required) - Whether the room has been archived +created_at: 1456902000 (number, required) - When the room was created (unix timestamp) +updated_at: 1456902000 (number, required) - When the room was last updated (unix timestamp) +Payroll Deduction Templates +Deduction templates define specific deductions that apply to individual employees. They can be fixed amounts or percentage-based deductions. + +Deduction Templates [/api/v2/payroll/deduction_templates] +GETGet Deduction Templates +Get all deduction templates for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the deduction template +user_id: 456 (number, required) - The ID of the user this deduction template applies to +payroll_deduction_type_id: 789 (number, required) - The ID of the deduction type +fixed_amount: 100.50 (number) - The fixed amount to deduct (mutually exclusive with percent_of_income) +percent_of_income: 5.0 (number) - The percentage of income to deduct (mutually exclusive with fixed_amount) +include_in_aba: true (boolean) - Whether to include this deduction in ABA files +aba_reference: REF123 (string) - Reference number for ABA files +Get Specific Deduction Template [GET /api/v2/payroll/deduction_templates/{id}] +Get a specific deduction template by ID. + +Parameters +id: 123 (number, required) - The ID of the deduction template +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction template +user_id: 456 (number, required) - The ID of the user this deduction template applies to +payroll_deduction_type_id: 789 (number, required) - The ID of the deduction type +fixed_amount: 100.50 (number) - The fixed amount to deduct (mutually exclusive with percent_of_income) +percent_of_income: 5.0 (number) - The percentage of income to deduct (mutually exclusive with fixed_amount) +include_in_aba: true (boolean) - Whether to include this deduction in ABA files +aba_reference: REF123 (string) - Reference number for ABA files +404 +Not Found +(application/json) +Body { “error”: “Deduction Template not found!” } +Get Deduction Templates For User [GET /api/v2/payroll/deduction_templates/for/{user_id}] +Get all deduction templates for a specific user. + +Parameters +user_id: 456 (number, required) - The ID of the user +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Schema +Attributes (array): +id: 123 (number, required) - The ID of the deduction template +user_id: 456 (number, required) - The ID of the user this deduction template applies to +payroll_deduction_type_id: 789 (number, required) - The ID of the deduction type +fixed_amount: 100.50 (number) - The fixed amount to deduct (mutually exclusive with percent_of_income) +percent_of_income: 5.0 (number) - The percentage of income to deduct (mutually exclusive with fixed_amount) +include_in_aba: true (boolean) - Whether to include this deduction in ABA files +aba_reference: REF123 (string) - Reference number for ABA files +404 +Not Found +(application/json) +Body { “error”: “Employee not found!” } +POSTCreate Deduction Template +Create a new deduction template. + +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “user_id”: 456, “payroll_deduction_type_id”: 789, “fixed_amount”: 100.50, “include_in_aba”: true, “aba_reference”: “REF123” } + +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction template +user_id: 456 (number, required) - The ID of the user this deduction template applies to +payroll_deduction_type_id: 789 (number, required) - The ID of the deduction type +fixed_amount: 100.50 (number) - The fixed amount to deduct (mutually exclusive with percent_of_income) +percent_of_income: 5.0 (number) - The percentage of income to deduct (mutually exclusive with fixed_amount) +include_in_aba: true (boolean) - Whether to include this deduction in ABA files +aba_reference: REF123 (string) - Reference number for ABA files +400 +Bad Request +(application/json) +Body { “error”: “Deduction Template can only have fixed_amount OR percent_of_income set!” } +404 +Not Found +(application/json) +Body { “error”: “Employee for deduction template not found!” } +Update Deduction Template [PUT /api/v2/payroll/deduction_templates/{id}] +Update an existing deduction template. + +Parameters +id: 123 (number, required) - The ID of the deduction template +Request (application/json) +Headers Authorization: Bearer {access_token} + +Body { “user_id”: 456, “payroll_deduction_type_id”: 789, “percent_of_income”: 5.0, “fixed_amount”: null, “include_in_aba”: false, “aba_reference”: null } + +200 +OK +(application/json) +Schema +Attributes: +id: 123 (number, required) - The ID of the deduction template +user_id: 456 (number, required) - The ID of the user this deduction template applies to +payroll_deduction_type_id: 789 (number, required) - The ID of the deduction type +fixed_amount: 100.50 (number) - The fixed amount to deduct (mutually exclusive with percent_of_income) +percent_of_income: 5.0 (number) - The percentage of income to deduct (mutually exclusive with fixed_amount) +include_in_aba: true (boolean) - Whether to include this deduction in ABA files +aba_reference: REF123 (string) - Reference number for ABA files +404 +Not Found +(application/json) +Body { “error”: “Deduction Template not found!” } +Delete Deduction Template [DELETE /api/v2/payroll/deduction_templates/{id}] +Delete a deduction template. + +Parameters +id: 123 (number, required) - The ID of the deduction template +Request +Headers Authorization: Bearer {access_token} +Response 200 (application/json) + +404 +Not Found +(application/json) +Body { “error”: “Deduction Template not found!” } +Hire +The Workforce Hire endpoints allow you to manage job postings, applications, candidate data, and hiring workflows. + +All Workforce Hire endpoints require users to have appropriate Workforce Hire permissions. The specific permission requirements are noted for each endpoint. +Jobs [/api/v2/ats/jobs] +GETGet Jobs +Gets information about all accessible job postings. + +This method requires the ats scope and read permissions for jobs. +Parameters +status: status (optional, string) - Filter by application acceptance status. Options: accepting, not_accepting +visibility: public (optional, string) - Filter by job visibility. Options: public, internal +updated_after: 1451570400 (optional, number) - Time filter to find recently modified jobs +page: 1 (optional, number) - The page number for pagination +page_size: 50 (optional, number) - The number of jobs per page. Maximum 100. +200 +OK +(application/json) +Attributes (array): +id: 123 (number) - The job ID +title: Software Engineer (string) - The job title +created_at: 1451570400 (number) - Creation timestamp +Get Single Job [GET /api/v2/ats/jobs/{job_id}] +Gets information about a specific job posting. + +This method requires the ats scope and read permissions for the specific job. +Parameters +job_id: 123 (required, number) - The ID of the job +200 +OK +(application/json) +Attributes: +id: 123 (number) - The job ID +title: Software Engineer (string) - The job title +created_at: 1451570400 (number) - Creation timestamp +Individual Job Applications [/api/v2/ats/applications/{application_id}] +Create Job Application [POST /api/v2/ats/applications] +Creates a new job application with candidate information. + +This method requires the ats scope and create permissions for candidates. +This endpoint creates a job application and handles candidate creation automatically: + +If a candidate with the provided email already exists in the organisation, their information will be updated and they will be linked to the job +If no candidate exists, a new candidate will be created +The application will be created in the specified stage, or the default “Received” stage if no stage is provided +Location assignments for the candidate are handled automatically +Required fields: job_id, legal_first_name, legal_last_name, email + +Optional fields: stage_id, candidate_message, source, skip_new_candidate_notification, legal_middle_names, phone, current_position, address, date_of_birth, right_to_work, location_ids + +The source parameter tracks where the application came from and defaults to “API-V2” if not provided. The skip_new_candidate_notification parameter controls whether new candidate notification emails are sent and defaults to false. + +Request Create Job Application (application/json) +Body + + { + "job_id": 123, + "stage_id": 456, + "candidate_message": "I am very interested in this position and believe I would be a great fit.", + "source": "Company Website", + "skip_new_candidate_notification": true, + "legal_first_name": "Jane", + "legal_last_name": "Smith", + "email": "jane.smith@example.com", + "legal_middle_names": "Marie", + "phone": "0412345678", + "current_position": "Senior Software Developer", + "address": "123 Collins St, Melbourne VIC", + "date_of_birth": "1990-05-15", + "right_to_work": "citizen", + "location_ids": [123, 456] + } + +201 +Created +(application/json) +Attributes: +id: 789 (number) - The application ID +current_stage_id: 456 (JobApplicationStageData) - Current application stage +job_id: 123 (number) - Job ID +candidate (object) - Candidate information (filtered by permissions) +name: Jane Smith (string) - Full name (only if user has PII permissions) +email: jane@example.com (string) - Email (only if user has PII permissions) +phone: +61400000000 (string) - Phone (only if user has PII permissions) +created_at: 1451570400 (number) - Application timestamp +PUTUpdate Application Stage +Updates the current stage of a job application. + +This method requires the ats scope and update permissions for job applications. +This action will update the application’s current stage, and trigger any configured email notifications. It will also send a webhook notification if configured. + +Parameters +application_id: 789 (required, number) - The ID of the application +stage_id: 456 (required, number) - The ID of the new stage +Request Update Application Stage (application/json) +Body + + { + "stage_id": 456 + } + +200 +OK +(application/json) +Get Single Application [GET /api/v2/ats/applications/{application_id}] +Gets detailed information about a specific job application. + +Parameters +application_id: 789 (required, number) - The ID of the application +200 +OK +(application/json) +Attributes: +id: 789 (number) - The application ID +current_stage_id: 456 (JobApplicationStageData) - Current application stage +job_id: 123 (number) - Job ID +candidate (object) - Candidate information (filtered by permissions) +name: Jane Smith (string) - Full name (only if user has PII permissions) +email: jane@example.com (string) - Email (only if user has PII permissions) +phone: +61400000000 (string) - Phone (only if user has PII permissions) +created_at: 1451570400 (number) - Application timestamp +Job Applications by Job [/api/v2/ats/jobs/{job_id}/applications] +GETGet Applications for Job +Gets all applications for a specific job posting. + +This method requires the ats scope and read permissions for both the job and job applications. +Parameters +job_id: 123 (required, number) - The ID of the job +stage_id: 456 (optional, number) - Filter by current application stage +page: 1 (optional, number) - The page number for pagination +page_size: 50 (optional, number) - The number of applications per page. Maximum 100. +200 +OK +(application/json) +Attributes (array): +id: 789 (number) - The application ID +current_stage_id: 456 (JobApplicationStageData) - Current application stage +job_id: 123 (number) - Job ID +candidate (object) - Candidate information (filtered by permissions) +name: Jane Smith (string) - Full name (only if user has PII permissions) +email: jane@example.com (string) - Email (only if user has PII permissions) +phone: +61400000000 (string) - Phone (only if user has PII permissions) +created_at: 1451570400 (number) - Application timestamp +Application Stages [/api/v2/ats/stages] +GETGet Stages +Gets all available application stages for the organization. + +This method requires the ats scope and read permissions for application stages. +200 +OK +(application/json) +Attributes (array): +id: 456 (number) - The stage ID +name: Interview (string) - Stage name +is_active: true (boolean) - Whether the stage is active +created_at: 1451570400 (number) - Creation timestamp +Candidates [/api/v2/ats/candidates] +POSTCreate Candidate +Creates a new candidate in your hiring pipeline. + +This method requires the ats scope and create permissions for candidates. +Required fields: legal_first_name, legal_last_name, email Optional fields: phone, current_position, address, date_of_birth, right_to_work, location_ids + +The location_ids parameter accepts an array of location IDs that the candidate is interested in. Invalid location IDs are ignored. + +Request Create Candidate (application/json) +Body + + { + "legal_first_name": "Jane", + "legal_last_name": "Smith", + "email": "jane.smith@example.com", + "phone": "0412345678", + "current_position": "Software Developer", + "address": "123 Collins St, Melbourne VIC", + "date_of_birth": "1990-05-15", + "right_to_work": "citizen", + "location_ids": [123, 456] + } + +201 +Created +(application/json) +Attributes: +id: 789 (number) - The candidate ID +legal_first_name: Jane (string) - Candidate’s legal first name +legal_last_name: Smith (string) - Candidate’s legal last name +legal_middle_names: Mary (string) - Candidate’s legal middle names +email: jane.smith@example.com (string) - Candidate’s email address +phone: +61412345678 (string) - Candidate’s phone number +current_position: Software Developer (string) - Candidate’s current job position +address: 123 Collins St, Melbourne VIC (string) - Candidate’s address +date_of_birth: 1990-05-15 (string) - Candidate’s date of birth +right_to_work: citizen (string) - Candidate’s right to work status (citizen or visa) +agreed_to_terms_at: 1451570400 (number) - When candidate agreed to terms +created_at: 1451570400 (number) - Creation timestamp +POSTCreate Stage +Creates a new custom application stage. + +This method requires the ats scope and manage permissions for application stages. +Request Create Stage (application/json) +Body + + { + "name": "Technical Interview", + "color": "#F59E0B", + "sort_order": 5, + "email_enabled": true, + "email_subject": "Next Steps - Technical Interview", + "allow_interview_scheduling": true + } + +200 +OK +(application/json) +Attributes: +id: 456 (number) - The stage ID +name: Interview (string) - Stage name +is_active: true (boolean) - Whether the stage is active +created_at: 1451570400 (number) - Creation timestamp +Update Stage [PUT /api/v2/ats/stages/{stage_id}] +Updates an existing application stage. + +Parameters +stage_id: 456 (required, number) - The ID of the stage +Request Update Stage (application/json) +Body + + { + "name": "Final Interview", + "color": "#10B981", + "email_enabled": false, + "is_active": true + } + +200 +OK +(application/json) +Attributes: +id: 456 (number) - The stage ID +name: Interview (string) - Stage name +is_active: true (boolean) - Whether the stage is active +created_at: 1451570400 (number) - Creation timestamp +Delete Stage [DELETE /api/v2/ats/stages/{stage_id}] +Deletes a custom application stage. + +- System stages cannot be deleted - Stages with active applications cannot be deleted - This action is permanent and cannot be undone +Parameters +stage_id: 456 (required, number) - The ID of the stage +204 +No Content +(application/json) +Application Notes +Get Single Note [GET /api/v2/ats/notes/{note_id}] +Gets details of a specific application note. + +Parameters +note_id: 999 (required, number) - The ID of the note +200 +OK +(application/json) +Attributes: +id: 999 (number) - The note ID +content: Great candidate, very experienced (string) - Note content (rich text rendered as string) +date: 1451570400 (number) - Note date timestamp +author_id: 555 (number) - User ID +has_attachments: false (boolean) - Whether the note has file attachments +created_at: 1451570400 (number) - Creation timestamp +Application Notes by Application [/api/v2/ats/applications/{application_id}/notes] +GETGet Notes for Application +Gets all notes for a specific job application. + +Parameters +application_id: 789 (required, number) - The ID of the application +page: 1 (optional, number) - The page number for pagination +page_size: 100 (optional, number) - The number of notes per page. Maximum 100. +200 +OK +(application/json) +Attributes (array): +id: 999 (number) - The note ID +content: Great candidate, very experienced (string) - Note content (rich text rendered as string) +date: 1451570400 (number) - Note date timestamp +author_id: 555 (number) - User ID +has_attachments: false (boolean) - Whether the note has file attachments +created_at: 1451570400 (number) - Creation timestamp +POSTCreate Note +Adds a new note to a job application. + +Notes support rich text content and file attachments. File attachments should be uploaded first using the temporary files endpoint, then referenced by their file IDs. + +This method requires the ats scope and create permissions for application notes. +Parameters +application_id: 789 (required, number) - The ID of the application +Request Create Note (application/json) +Body + + { + "content": "Candidate showed strong technical skills during the phone screen. Recommend moving to technical interview stage.", + "date": "2024-01-15", + "file_ids": ["temp-file-uuid-1", "temp-file-uuid-2"] + } + +200 +OK +(application/json) +Attributes: +id: 999 (number) - The note ID +content: Great candidate, very experienced (string) - Note content (rich text rendered as string) +date: 1451570400 (number) - Note date timestamp +author_id: 555 (number) - User ID +has_attachments: false (boolean) - Whether the note has file attachments +created_at: 1451570400 (number) - Creation timestamp +Payroll Company Files +Company files represent Workforce payroll integrations within your organisation. Each company file corresponds to a separate legal entity or payroll structure and determines which locations or employees are included in its payroll processing. + +Company Files [/api/v2/payroll/company_files] +GETGet Company Files +Get all Workforce payroll company files for the current organisation. + +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body [ { “id”: 123, “legal_name”: “ACME Organisation Pty Ltd”, “trading_name”: “ACME Org”, “applies_to”: “reporting_location”, “applies_to_location_ids”: [45, 67, 89] }, { “id”: 456, “legal_name”: “ACME Services Limited”, “trading_name”: “ACME Services”, “applies_to”: “everyone”, “applies_to_location_ids”: [] } ] + +Attributes (array): +id: 123 (number, required) - The ID of the company file +legal_name: ACME Organisation Pty Ltd (string, required) - The legal registered name of the company +trading_name: ACME Org (string, required) - The trading or business name of the company +applies_to: reporting_location (string, required) - Determines which employees are included. One of everyone, noone, reporting_location, pay_group +applies_to_location_ids: 45, 67, 89 (array[number], required) - Array of location IDs when applies_to is reporting_location, empty array otherwise +Company File [/api/v2/payroll/company_files/{id}] +GETGet Company File +Get a single Workforce payroll company file by ID. + +Parameters +id (number, required) - The company file ID +Request +Headers Authorization: Bearer {access_token} +200 +OK +(application/json) +Body { “id”: 123, “legal_name”: “ACME Organisation Pty Ltd”, “trading_name”: “ACME Org”, “applies_to”: “reporting_location”, “applies_to_location_ids”: [45, 67, 89] } + +Attributes: +id: 123 (number, required) - The ID of the company file +legal_name: ACME Organisation Pty Ltd (string, required) - The legal registered name of the company +trading_name: ACME Org (string, required) - The trading or business name of the company +applies_to: reporting_location (string, required) - Determines which employees are included. One of everyone, noone, reporting_location, pay_group +applies_to_location_ids: 45, 67, 89 (array[number], required) - Array of location IDs when applies_to is reporting_location, empty array otherwise +404 +Not Found +(application/json) +Body { “error”: “Company file not found” } \ No newline at end of file