@rubytech/create-maxy-code 0.1.575 → 0.1.577
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/preserve-house-cloudflare.test.js +2 -72
- package/dist/index.js +8 -59
- package/dist/preserve-house-cloudflare.js +2 -49
- package/package.json +1 -1
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +32 -1
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
- package/payload/platform/lib/active-rules/src/index.ts +36 -2
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
- package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
- package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
- package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
- package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
- package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
- package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
- package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
- package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
- package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
- package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
- package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
- package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
- package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
- package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
- package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
- package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js +1 -1
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/server.js +670 -155
- package/dist/__tests__/converge-client-admins.test.js +0 -50
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts
CHANGED
|
@@ -17,6 +17,47 @@ export interface VisitRow {
|
|
|
17
17
|
status: string
|
|
18
18
|
startDate: string
|
|
19
19
|
purpose: string
|
|
20
|
+
/**
|
|
21
|
+
* What the worker recorded, as COUNTS (Task 2511).
|
|
22
|
+
*
|
|
23
|
+
* Counts and never contents, which is this interface's own rule above applied
|
|
24
|
+
* rather than relaxed: the contents are files and the Task 1910 exchange
|
|
25
|
+
* carries them. A count says what was recorded and what is still missing,
|
|
26
|
+
* which is what the office could not see at all before, without putting a
|
|
27
|
+
* real person's written note or signature in a mirror on the open web. The
|
|
28
|
+
* contents are Task 2563.
|
|
29
|
+
*
|
|
30
|
+
* `checklistDone` is read against `checklistTotal` and never alone: a count
|
|
31
|
+
* of ticked items with no total says nothing about whether the work is done.
|
|
32
|
+
*/
|
|
33
|
+
notes: number
|
|
34
|
+
media: number
|
|
35
|
+
parts: number
|
|
36
|
+
checklistDone: number
|
|
37
|
+
checklistTotal: number
|
|
38
|
+
signatures: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One candidate for one unallocated visit (Task 2511).
|
|
43
|
+
*
|
|
44
|
+
* Precomputed on the install by the one implementation of qualification and
|
|
45
|
+
* clash, and rendered as returned. This endpoint applies no such rule of its own
|
|
46
|
+
* and neither does the page, for the reason `processVisits` states below.
|
|
47
|
+
*
|
|
48
|
+
* `kind` decides which of the last two fields carries anything. An `offered`
|
|
49
|
+
* row has an empty `reason` and its conflicts, which is `[]` when the worker is
|
|
50
|
+
* free; an `excluded` row has empty conflicts and its reason. A qualified
|
|
51
|
+
* worker already committed is OFFERED with the conflict shown and never
|
|
52
|
+
* dropped, because the office double-books on purpose.
|
|
53
|
+
*/
|
|
54
|
+
export interface CandidateRow {
|
|
55
|
+
visitId: string
|
|
56
|
+
workerId: string
|
|
57
|
+
name: string
|
|
58
|
+
kind: string
|
|
59
|
+
reason: string
|
|
60
|
+
conflicts: Array<{ visitId: string; start: string; end: string }>
|
|
20
61
|
}
|
|
21
62
|
|
|
22
63
|
/**
|
|
@@ -46,6 +87,16 @@ export interface RosterRow {
|
|
|
46
87
|
export interface Meta {
|
|
47
88
|
vocabulary: { job: string; visit: string; worker: string }
|
|
48
89
|
statuses: Array<{ name: string; terminal: boolean }>
|
|
90
|
+
/**
|
|
91
|
+
* The account's legal moves, `[from, to]` (Task 2511).
|
|
92
|
+
*
|
|
93
|
+
* Absent from the list means refused, and an empty list therefore refuses
|
|
94
|
+
* every move, which is what an account declaring none means. The page refuses
|
|
95
|
+
* from this published copy, so an illegal drag is refused while the install is
|
|
96
|
+
* offline; `visit-status.ts` refuses from the same copy, which is the refusal
|
|
97
|
+
* that binds.
|
|
98
|
+
*/
|
|
99
|
+
transitions: Array<[string, string]>
|
|
49
100
|
zone: string
|
|
50
101
|
publishedAt: string
|
|
51
102
|
}
|
|
@@ -100,7 +151,10 @@ export async function processVisits(
|
|
|
100
151
|
// makes against directory_state, for the same reason.
|
|
101
152
|
const sql =
|
|
102
153
|
`SELECT v.visitId AS visitId, v.ownerId AS ownerId, v.jobId AS jobId,
|
|
103
|
-
v.status AS status, v.startDate AS startDate, v.purpose AS purpose
|
|
154
|
+
v.status AS status, v.startDate AS startDate, v.purpose AS purpose,
|
|
155
|
+
v.notes AS notes, v.media AS media, v.parts AS parts,
|
|
156
|
+
v.checklistDone AS checklistDone, v.checklistTotal AS checklistTotal,
|
|
157
|
+
v.signatures AS signatures
|
|
104
158
|
FROM visits v
|
|
105
159
|
JOIN visits_state s
|
|
106
160
|
ON s.accountId = v.accountId AND s.currentGeneration = v.generation
|
|
@@ -143,16 +197,69 @@ export async function processVisits(
|
|
|
143
197
|
workingHours: parseJson<Record<string, [string, string]>>(r.workingHours, {}),
|
|
144
198
|
}))
|
|
145
199
|
|
|
200
|
+
// THE SAME TWO FILTERS, applied through the visits join rather than beside it
|
|
201
|
+
// (Task 2511). A candidate row names a colleague, so an own-scoped person
|
|
202
|
+
// receiving candidates for a visit they cannot see would learn the roster the
|
|
203
|
+
// roster read above deliberately withholds from them. The join IS the owner
|
|
204
|
+
// filter; there is no second check to keep in step with it.
|
|
205
|
+
//
|
|
206
|
+
// Read through the generation pointer, and joined to the visits at the SAME
|
|
207
|
+
// generation, so the offers on screen always belong to the week on screen.
|
|
208
|
+
const candidateSql =
|
|
209
|
+
`SELECT c.visitId AS visitId, c.workerId AS workerId, c.name AS name,
|
|
210
|
+
c.kind AS kind, c.reason AS reason, c.conflicts AS conflicts
|
|
211
|
+
FROM visit_candidates c
|
|
212
|
+
JOIN visits v
|
|
213
|
+
ON v.accountId = c.accountId AND v.visitId = c.visitId
|
|
214
|
+
AND v.generation = c.generation
|
|
215
|
+
JOIN visits_state s
|
|
216
|
+
ON s.accountId = c.accountId AND s.currentGeneration = c.generation
|
|
217
|
+
WHERE c.accountId = ?` +
|
|
218
|
+
(scoped ? ' AND v.ownerId = ?' : '') +
|
|
219
|
+
' ORDER BY c.visitId ASC, c.workerId ASC'
|
|
220
|
+
const candidateStmt = env.DB.prepare(candidateSql)
|
|
221
|
+
const candidateRows = await (
|
|
222
|
+
scoped
|
|
223
|
+
? candidateStmt.bind(env.PORTAL_ACCOUNT_ID, session.ownerId)
|
|
224
|
+
: candidateStmt.bind(env.PORTAL_ACCOUNT_ID)
|
|
225
|
+
).all<{
|
|
226
|
+
visitId: string
|
|
227
|
+
workerId: string
|
|
228
|
+
name: string
|
|
229
|
+
kind: string
|
|
230
|
+
reason: string
|
|
231
|
+
conflicts: string
|
|
232
|
+
}>()
|
|
233
|
+
|
|
234
|
+
// Parsed here, once, for the reason the roster's workingHours is: a column of
|
|
235
|
+
// JSON parsed by every consumer is a shape each of them can disagree about.
|
|
236
|
+
// An excluded row carries no conflicts at all, so its empty column reads as
|
|
237
|
+
// an empty list rather than throwing.
|
|
238
|
+
const candidates: CandidateRow[] = (candidateRows.results ?? []).map((c) => ({
|
|
239
|
+
visitId: c.visitId,
|
|
240
|
+
workerId: c.workerId,
|
|
241
|
+
name: c.name,
|
|
242
|
+
kind: c.kind,
|
|
243
|
+
reason: c.reason,
|
|
244
|
+
conflicts: parseJson<Array<{ visitId: string; start: string; end: string }>>(c.conflicts, []),
|
|
245
|
+
}))
|
|
246
|
+
|
|
146
247
|
const metaRow = await env.DB.prepare(
|
|
147
|
-
`SELECT m.vocabulary AS vocabulary, m.statuses AS statuses, m.
|
|
148
|
-
m.publishedAt AS publishedAt
|
|
248
|
+
`SELECT m.vocabulary AS vocabulary, m.statuses AS statuses, m.transitions AS transitions,
|
|
249
|
+
m.zone AS zone, m.publishedAt AS publishedAt
|
|
149
250
|
FROM dispatch_meta m
|
|
150
251
|
JOIN visits_state s
|
|
151
252
|
ON s.accountId = m.accountId AND s.currentGeneration = m.generation
|
|
152
253
|
WHERE m.accountId = ?`,
|
|
153
254
|
)
|
|
154
255
|
.bind(env.PORTAL_ACCOUNT_ID)
|
|
155
|
-
.first<{
|
|
256
|
+
.first<{
|
|
257
|
+
vocabulary: string
|
|
258
|
+
statuses: string
|
|
259
|
+
transitions: string
|
|
260
|
+
zone: string
|
|
261
|
+
publishedAt: string
|
|
262
|
+
}>()
|
|
156
263
|
|
|
157
264
|
// NOT owner-scoped. The nouns, the statuses and the account's timezone are
|
|
158
265
|
// configuration, not a record about a person, and the page cannot draw a day
|
|
@@ -161,6 +268,11 @@ export async function processVisits(
|
|
|
161
268
|
? {
|
|
162
269
|
vocabulary: parseJson(metaRow.vocabulary, { job: '', visit: '', worker: '' }),
|
|
163
270
|
statuses: parseJson<Array<{ name: string; terminal: boolean }>>(metaRow.statuses, []),
|
|
271
|
+
// A malformed list falls back to EMPTY, which refuses every move. The
|
|
272
|
+
// other direction would be worse in kind rather than in degree: a page
|
|
273
|
+
// that accepts every move enforces nothing and is indistinguishable on
|
|
274
|
+
// screen from one that enforces correctly.
|
|
275
|
+
transitions: parseJson<Array<[string, string]>>(metaRow.transitions, []),
|
|
164
276
|
zone: metaRow.zone,
|
|
165
277
|
publishedAt: metaRow.publishedAt,
|
|
166
278
|
}
|
|
@@ -172,9 +284,9 @@ export async function processVisits(
|
|
|
172
284
|
log(
|
|
173
285
|
`[data-portal] op=visits owner=${q(session.ownerId)} account=${q(session.accountId)} ` +
|
|
174
286
|
`scope=${session.recordScope} visits=${visits.length} roster=${roster.length} ` +
|
|
175
|
-
`meta=${meta !== null} result=ok`,
|
|
287
|
+
`candidates=${candidates.length} meta=${meta !== null} result=ok`,
|
|
176
288
|
)
|
|
177
|
-
return { status: 200, payload: { ok: true, visits, roster, meta } }
|
|
289
|
+
return { status: 200, payload: { ok: true, visits, roster, meta, candidates } }
|
|
178
290
|
}
|
|
179
291
|
|
|
180
292
|
interface PagesContext {
|
|
@@ -123,6 +123,8 @@ A move from one status to another is legal when the account's own transition lis
|
|
|
123
123
|
|
|
124
124
|
Neither refusal is swallowed. A status that quietly failed to change cannot be told apart from one nobody tried to change.
|
|
125
125
|
|
|
126
|
+
The office planner on an account's own portal enforces the same list, from a copy published with the week. The page refuses an undeclared move before sending it, so an illegal drag is refused even while the device is offline, and the portal's write door refuses it again from that same copy. Only the door's refusal binds; the page's exists so the person is told at once rather than a minute later. A status equal to the one already held is not a move on either side, so re-timing or re-assigning a record never has to invent a transition to itself.
|
|
127
|
+
|
|
126
128
|
## A busy worker is offered, not hidden
|
|
127
129
|
|
|
128
130
|
Only a missing qualification excludes a worker from the candidate list, and every exclusion is named with its worker and its reason. A qualified worker who is already committed in the window is offered with the clash shown.
|
|
@@ -1,59 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Who can be sent to this job, and why the others cannot.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `selectCandidates`, `overlaps` and their types moved to
|
|
5
|
+
* `platform/lib/dispatch-read` in Task 2511, so the portal visits push and these
|
|
6
|
+
* tools share one implementation of qualification and clash. The push runs in
|
|
7
|
+
* `platform/ui` and cannot import a plugin's build output, and a second copy of
|
|
8
|
+
* these rules is the defect `functions/api/visits.ts:33-36` names. The queries
|
|
9
|
+
* and the rules moved; they did not change. This module stays as the plugin's
|
|
10
|
+
* import surface, exactly as `lib/read.ts` did for the Cypher reads.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* `Commitment` now comes from that package's `read.ts`, where
|
|
13
|
+
* `readCommitments` declares it. The second structurally-identical declaration
|
|
14
|
+
* that used to live here is gone: two declarations of one contract can drift on
|
|
15
|
+
* a field.
|
|
13
16
|
*
|
|
14
|
-
* Task 2509 added the travel estimate. A candidate carries
|
|
15
|
-
* one could be produced and **omits the key** when it
|
|
16
|
-
* reason in `travelReason` — never a zero and never a null,
|
|
17
|
-
* would read as a measurement. `selectCandidates`
|
|
18
|
-
* untouched by it: no estimator state may change who is offered.
|
|
17
|
+
* Task 2509 added the travel estimate, and it stays here. A candidate carries
|
|
18
|
+
* `travelMinutes` when one could be produced and **omits the key** when it
|
|
19
|
+
* could not, with the reason in `travelReason` — never a zero and never a null,
|
|
20
|
+
* either of which would read as a measurement. `selectCandidates` is
|
|
21
|
+
* deliberately untouched by it: no estimator state may change who is offered.
|
|
19
22
|
*/
|
|
20
|
-
import type {
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
readonly start: string;
|
|
24
|
-
readonly end: string;
|
|
25
|
-
}
|
|
26
|
-
export interface Candidate {
|
|
27
|
-
readonly workerId: string;
|
|
28
|
-
readonly name: string;
|
|
29
|
-
/** Commitments overlapping the requested window. Empty means free. */
|
|
30
|
-
readonly conflicts: readonly Commitment[];
|
|
31
|
-
}
|
|
32
|
-
export interface Exclusion {
|
|
33
|
-
readonly workerId: string;
|
|
34
|
-
readonly reason: "missing-qualification";
|
|
35
|
-
}
|
|
36
|
-
export interface SelectCandidatesParams {
|
|
37
|
-
readonly jobType: string;
|
|
38
|
-
readonly cfg: DispatchConfig;
|
|
39
|
-
/** Committed visits per workerId, already narrowed to the window. */
|
|
40
|
-
readonly commitments: ReadonlyMap<string, readonly Commitment[]>;
|
|
41
|
-
}
|
|
42
|
-
export interface CandidateSelection {
|
|
43
|
-
readonly offered: readonly Candidate[];
|
|
44
|
-
readonly excluded: readonly Exclusion[];
|
|
45
|
-
/** The qualification the job type demands, echoed for the log line. */
|
|
46
|
-
readonly required: string;
|
|
47
|
-
}
|
|
48
|
-
export declare function selectCandidates(params: SelectCandidatesParams): CandidateSelection;
|
|
49
|
-
/** `[a, b)` overlap. Touching endpoints do not overlap. */
|
|
50
|
-
export declare function overlaps(a: {
|
|
51
|
-
start: string;
|
|
52
|
-
end: string;
|
|
53
|
-
}, b: {
|
|
54
|
-
start: string;
|
|
55
|
-
end: string;
|
|
56
|
-
}): boolean;
|
|
23
|
+
import type { Exclusion } from "../../../../../lib/dispatch-read/dist/index.js";
|
|
24
|
+
export { overlaps, selectCandidates, } from "../../../../../lib/dispatch-read/dist/index.js";
|
|
25
|
+
export type { Candidate, CandidateSelection, Commitment, Exclusion, SelectCandidatesParams, } from "../../../../../lib/dispatch-read/dist/index.js";
|
|
57
26
|
/**
|
|
58
27
|
* Why a candidate carries no estimate, or the estimate itself.
|
|
59
28
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"allocate.d.ts","sourceRoot":"","sources":["../../src/lib/allocate.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"allocate.d.ts","sourceRoot":"","sources":["../../src/lib/allocate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gDAAgD,CAAC;AAEhF,OAAO,EACL,QAAQ,EACR,gBAAgB,GACjB,MAAM,gDAAgD,CAAC;AACxD,YAAY,EACV,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,SAAS,EACT,sBAAsB,GACvB,MAAM,gDAAgD,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,mBAAmB,GAAG,aAAa,CAAC;AAExE,wBAAgB,oBAAoB,CAAC,CAAC,EAAE;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,MAAM,CAKT;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;CAChC,GAAG,MAAM,CAST"}
|
|
@@ -1,31 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
const { jobType, cfg, commitments } = params;
|
|
3
|
-
const type = cfg.jobTypes.find((t) => t.name === jobType);
|
|
4
|
-
if (!type) {
|
|
5
|
-
// An unknown job type is the caller's error, not a staffing outcome. Every
|
|
6
|
-
// worker is excluded for a reason that would be a lie, so none are: the
|
|
7
|
-
// caller checks the type before asking.
|
|
8
|
-
return { offered: [], excluded: [], required: "" };
|
|
9
|
-
}
|
|
10
|
-
const offered = [];
|
|
11
|
-
const excluded = [];
|
|
12
|
-
for (const worker of cfg.workers) {
|
|
13
|
-
if (!worker.qualifications.includes(type.requiredQualification)) {
|
|
14
|
-
excluded.push({ workerId: worker.workerId, reason: "missing-qualification" });
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
offered.push({
|
|
18
|
-
workerId: worker.workerId,
|
|
19
|
-
name: worker.name,
|
|
20
|
-
conflicts: commitments.get(worker.workerId) ?? [],
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return { offered, excluded, required: type.requiredQualification };
|
|
24
|
-
}
|
|
25
|
-
/** `[a, b)` overlap. Touching endpoints do not overlap. */
|
|
26
|
-
export function overlaps(a, b) {
|
|
27
|
-
return a.start < b.end && b.start < a.end;
|
|
28
|
-
}
|
|
1
|
+
export { overlaps, selectCandidates, } from "../../../../../lib/dispatch-read/dist/index.js";
|
|
29
2
|
export function formatTravelEstimate(p) {
|
|
30
3
|
return (`[dispatch] op=travel-estimate jobId=${p.jobId} fromVisitId=${p.fromVisitId ?? "none"} ` +
|
|
31
4
|
`provider=${p.provider} result=${p.result} ms=${p.ms}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"allocate.js","sourceRoot":"","sources":["../../src/lib/allocate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"allocate.js","sourceRoot":"","sources":["../../src/lib/allocate.ts"],"names":[],"mappings":"AA0BA,OAAO,EACL,QAAQ,EACR,gBAAgB,GACjB,MAAM,gDAAgD,CAAC;AAmBxD,MAAM,UAAU,oBAAoB,CAAC,CAOpC;IACC,OAAO,CACL,uCAAuC,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC,WAAW,IAAI,MAAM,GAAG;QACxF,YAAY,CAAC,CAAC,QAAQ,WAAW,CAAC,CAAC,MAAM,OAAO,CAAC,CAAC,EAAE,EAAE,CACvD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,CAKxC;IACC,MAAM,UAAU,GACd,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QACrB,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnE,OAAO,CACL,2CAA2C,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,QAAQ,IAAI,MAAM,GAAG;QACtF,WAAW,CAAC,CAAC,OAAO,aAAa,CAAC,CAAC,QAAQ,CAAC,MAAM,eAAe,UAAU,EAAE,CAC9E,CAAC;AACJ,CAAC"}
|
|
@@ -56,15 +56,41 @@ redeploy of a site that gained a page, not a new site.
|
|
|
56
56
|
Nothing else is copied. The endpoint, the session library and the schema are the portal's and are
|
|
57
57
|
already deployed with it.
|
|
58
58
|
|
|
59
|
+
## What the office can do with a day
|
|
60
|
+
|
|
61
|
+
Every action goes through the portal's existing `/api/visit-status` door, and the device carries the
|
|
62
|
+
result into the graph within a minute. There is no second write endpoint and no second credential
|
|
63
|
+
path.
|
|
64
|
+
|
|
65
|
+
- **Advance a status.** Clicking a record moves it to the next status the account declared.
|
|
66
|
+
- **Re-time it.** Dragging it to another day keeps its clock time and changes its date, computed in
|
|
67
|
+
the account's own timezone.
|
|
68
|
+
- **Re-assign it.** Dragging it to another worker's row moves it to that worker. The device replaces
|
|
69
|
+
the attendance; a record two workers share is refused rather than silently losing one of them.
|
|
70
|
+
- **See who could go.** Opening a record's panel lists the workers the install offered, each with
|
|
71
|
+
their commitments in that window, and the ones it excluded with the reason. Both lists are
|
|
72
|
+
computed on the device, never on the page.
|
|
73
|
+
- **See what was recorded.** The panel shows how many notes, media, parts, checklist items and
|
|
74
|
+
signatures the record carries. Counts only; the contents are files and the portal's own file drop
|
|
75
|
+
already carries them.
|
|
76
|
+
|
|
77
|
+
**A move the account did not declare is refused twice.** The page refuses it from the transition
|
|
78
|
+
list published with the week, so an illegal drag is refused even while the device is offline, and
|
|
79
|
+
the door refuses it again. Only the door's refusal binds; the page's exists so the person is told
|
|
80
|
+
now rather than in a minute. A refused move returns the record to where it came from and says why,
|
|
81
|
+
because a card that silently slides back is indistinguishable from a gesture that missed.
|
|
82
|
+
|
|
59
83
|
## What it does not do
|
|
60
84
|
|
|
61
|
-
It does not
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
85
|
+
It does not show a job list, counts of unallocated or past-target work, or the
|
|
86
|
+
pressed-against-observed arrival panel. None of those is on this page today.
|
|
87
|
+
|
|
88
|
+
A candidate carries no estimate of how long it takes to reach the site from where that worker
|
|
89
|
+
already is. The office sees who is qualified and who is busy, and picks between two names with no
|
|
90
|
+
idea one of them is ninety minutes away.
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
The completed-work panel shows counts, not contents. Reading the note a worker wrote, or the
|
|
93
|
+
signature they captured, still means opening the file drop.
|
|
68
94
|
|
|
69
95
|
## Done-gate
|
|
70
96
|
|
|
@@ -80,6 +106,19 @@ The page is not done until all five hold on the live domain:
|
|
|
80
106
|
from a day off.
|
|
81
107
|
5. Clicking a visit changes its status on screen, and `grep '\[portal-visits-pull\]' server.log`
|
|
82
108
|
carries `op=applied visitId=` for it within one minute.
|
|
109
|
+
6. `grep '\[portal-visits-push\]' server.log` shows `transitions=` above zero for this account. Zero
|
|
110
|
+
there is not a quiet account: it is a page that refuses every move while looking identical to one
|
|
111
|
+
that enforces correctly.
|
|
112
|
+
7. Dragging a visit onto another worker's row shows it there, and the pull carries
|
|
113
|
+
`op=applied visitId= field=assignedOwnerId` within one minute.
|
|
114
|
+
8. Attempting a move the account did not declare shows the refusal, returns the card, and the Pages
|
|
115
|
+
log carries `op=rejected reason=illegal-transition` while `grep 'op=applied' server.log` reports
|
|
116
|
+
nothing for that visit. A refusal that still wrote is the failure the transition gate exists to
|
|
117
|
+
prevent.
|
|
118
|
+
|
|
119
|
+
Condition 5 could not pass before the write half shipped: the pull emitted `op=merged` and
|
|
120
|
+
`op=unresolved` and no `op=applied` line existed anywhere. It does now, and it names the visit and
|
|
121
|
+
the field, which is the only way to pair a write the door accepted to one the device applied.
|
|
83
122
|
|
|
84
123
|
The freshness line in the page header states the age of what is on screen. "Never published" there
|
|
85
124
|
means this account has never completed a push, not that the week is empty.
|
|
@@ -87,6 +87,66 @@
|
|
|
87
87
|
font-size: 0.8rem;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/* Task 2511. A card is dragged to re-time or re-assign it. `grab` rather than
|
|
91
|
+
`move`, because the click that advances the status still works. */
|
|
92
|
+
.pl-visit[draggable='true'] {
|
|
93
|
+
cursor: grab;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* The cell under the pointer during a drag. `currentColor` and opacity rather
|
|
97
|
+
than a colour: portal.css owns the palette and rewrites its own :root, so a
|
|
98
|
+
literal here would render one install's colour on every other. */
|
|
99
|
+
.pl-cell[data-over='true'] {
|
|
100
|
+
outline: 2px dashed currentColor;
|
|
101
|
+
outline-offset: -2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.pl-allocate {
|
|
105
|
+
margin-top: 0.75rem;
|
|
106
|
+
padding: 0.6rem 0.75rem;
|
|
107
|
+
border: 1px solid currentColor;
|
|
108
|
+
border-radius: 0.4rem;
|
|
109
|
+
position: relative;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.pl-allocate-close {
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 0.3rem;
|
|
115
|
+
right: 0.4rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pl-allocate-head {
|
|
119
|
+
font-size: 0.82rem;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
margin: 0 0 0.4rem;
|
|
122
|
+
/* Room for the close button, which is positioned over this corner. */
|
|
123
|
+
padding-right: 2rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.pl-allocate-list {
|
|
127
|
+
list-style: none;
|
|
128
|
+
margin: 0;
|
|
129
|
+
padding: 0;
|
|
130
|
+
font-size: 0.78rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* An excluded worker is dimmed rather than hidden. Hiding them would leave
|
|
134
|
+
`offered=0` with no explanation, which is the shape a quiet day and an
|
|
135
|
+
unstaffable job type share. */
|
|
136
|
+
.pl-candidate[data-kind='excluded'] {
|
|
137
|
+
opacity: 0.6;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.pl-candidate + .pl-candidate {
|
|
141
|
+
margin-top: 0.2rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.pl-record {
|
|
145
|
+
font-size: 0.78rem;
|
|
146
|
+
margin: 0.5rem 0 0;
|
|
147
|
+
opacity: 0.85;
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
@media (max-width: 720px) {
|
|
91
151
|
/* Seven readable columns that scroll, rather than seven unreadable slivers
|
|
92
152
|
that do not. The scroll is the GRID's, never the page's. */
|
|
@@ -52,6 +52,19 @@
|
|
|
52
52
|
</header>
|
|
53
53
|
<div class="pl-grid" id="pl-grid"></div>
|
|
54
54
|
<p class="pl-unplaceable" id="pl-unplaceable" hidden></p>
|
|
55
|
+
|
|
56
|
+
<!-- One record at a time: who may be sent to it, why the others may
|
|
57
|
+
not, and what has been recorded against it so far. Everything here
|
|
58
|
+
is rendered from what /api/visits returned. The page holds no rule
|
|
59
|
+
about who is qualified, who clashes, or which moves are legal. -->
|
|
60
|
+
<section id="pl-allocate" class="pl-allocate" hidden>
|
|
61
|
+
<button class="dp-iconbtn pl-allocate-close" id="pl-allocate-close" type="button">
|
|
62
|
+
×
|
|
63
|
+
</button>
|
|
64
|
+
<p class="pl-allocate-head" id="pl-allocate-head"></p>
|
|
65
|
+
<ul class="pl-allocate-list" id="pl-allocate-list"></ul>
|
|
66
|
+
<p class="pl-record" id="pl-record"></p>
|
|
67
|
+
</section>
|
|
55
68
|
</section>
|
|
56
69
|
</main>
|
|
57
70
|
<script src="/planner.js"></script>
|