@mgsoftwarebv/mcp-server-bridge 3.5.10 → 3.5.12
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/README.md +1 -0
- package/dist/index.js +615 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs, { ReadStream, lstatSync, fstatSync, readFileSync, promises } from 'fs
|
|
|
3
3
|
import os, { platform, release, homedir } from 'os';
|
|
4
4
|
import { join, basename, dirname, sep as sep$1, normalize } from 'path';
|
|
5
5
|
import * as crypto2 from 'crypto';
|
|
6
|
-
import crypto2__default, { createHash, randomUUID, createSecretKey, KeyObject, constants, createHmac, createPrivateKey, createPublicKey, sign, getRandomValues } from 'crypto';
|
|
6
|
+
import crypto2__default, { createHash, randomUUID, randomBytes, createSecretKey, KeyObject, constants, createHmac, createPrivateKey, createPublicKey, sign, getRandomValues } from 'crypto';
|
|
7
7
|
import fs2, { readFile } from 'fs/promises';
|
|
8
8
|
import Stream, { Readable, Writable, Duplex } from 'stream';
|
|
9
9
|
import { Buffer as Buffer$1 } from 'buffer';
|
|
@@ -103467,7 +103467,9 @@ var customerRecurringServices = pgTable(
|
|
|
103467
103467
|
mode: "string"
|
|
103468
103468
|
}),
|
|
103469
103469
|
// --- Lifecycle ---
|
|
103470
|
-
// "active" | "paused" | "cancelled" | "ended"
|
|
103470
|
+
// "draft" | "active" | "paused" | "cancelled" | "ended"
|
|
103471
|
+
// "draft" = created but not yet activated (e.g. via MCP); excluded from
|
|
103472
|
+
// all billing/due queries until a human activates it in the dashboard.
|
|
103471
103473
|
status: text().$type().default("active").notNull(),
|
|
103472
103474
|
startDate: timestamp("start_date", { withTimezone: true, mode: "string" }),
|
|
103473
103475
|
// Planned end of the agreement (before cancellation/ended).
|
|
@@ -106574,7 +106576,7 @@ var TOOLS = [
|
|
|
106574
106576
|
},
|
|
106575
106577
|
{
|
|
106576
106578
|
name: "get-document",
|
|
106577
|
-
description: "Get a document by its ID, including metadata and the full blocks JSON. Use this before update-document to inspect the current structure.",
|
|
106579
|
+
description: "Get a document by its ID, including metadata and the full blocks JSON. Use this before update-document to inspect the current structure. Also returns ready-to-use links (internal dashboard URL, PDF status, and a public link when the document is shared); use get-document-link for a PDF download or to publish a public customer link.",
|
|
106578
106580
|
inputSchema: {
|
|
106579
106581
|
type: "object",
|
|
106580
106582
|
properties: {
|
|
@@ -106586,7 +106588,7 @@ var TOOLS = [
|
|
|
106586
106588
|
},
|
|
106587
106589
|
{
|
|
106588
106590
|
name: "create-document",
|
|
106589
|
-
description: 'Create a document (proposal, report, deliverables/opleverdocument, ...) from an array of content blocks. Use type \'deliverables\' for a document describing what was delivered for a customer; optionally pass `invoiceId` to link it to an invoice so it can be sent along as a PDF attachment. Each block is `{ id?, type, data }` (ids are auto-generated when omitted). Supported block types and their `data` shapes:\n- cover: { title, subtitle?, showLogo: bool, showDate: bool, clientName?, clientCompany?, date? }\n- toc: { title, maxDepth: 1-6 }\n- heading: { text, level: 1|2|3, numbered: bool }\n- text: { content: TipTapDoc | plain string (auto-converted; supports paragraphs and "- " bullet lists) }\n- callout: { variant: "info"|"warning"|"tip"|"important", title?, content: TipTapDoc | string }\n- table: { title?, headers: string[], rows: string[][] }\n- chart: { title?, chartType: "bar"|"line"|"pie", points: [{label, value}], seriesName?, series?: [{name, color?, points: [{label, value}]}], unit? ("\u20AC"|"%"|...), showValues: bool, color? }. Multi-series (bar/line only): extra series values are matched to the primary points by index; a legend is rendered automatically.\n- pricing: { title?, items: [{description, quantity?, unit?, price}], currency: "EUR", showTotal: bool, vatRate?, includeVat: bool }\n- timeline: { title?, phases: [{name, description?, duration, deliverables?: string[]}] }\n- two_column: { left: TipTapDoc|string, right: TipTapDoc|string, ratio?: "50_50"|"60_40"|"40_60"|"70_30"|"30_70" }\n- divider: { style: "page_break"|"line"|"space" }\n- signature: { title?, description?, signers: [{id, label, name?, company?, role?}] }\nAll text content runs through a humanizer that strips AI-sounding patterns (em-dashes, clich\xE9s, filler phrases); control it with `humanize`.',
|
|
106591
|
+
description: 'Create a document (proposal, report, deliverables/opleverdocument, ...) from an array of content blocks. Use type \'deliverables\' for a document describing what was delivered for a customer; optionally pass `invoiceId` to link it to an invoice so it can be sent along as a PDF attachment. Each block is `{ id?, type, data }` (ids are auto-generated when omitted). Supported block types and their `data` shapes:\n- cover: { title, subtitle?, showLogo: bool, showDate: bool, clientName?, clientCompany?, date? }\n- toc: { title, maxDepth: 1-6 }\n- heading: { text, level: 1|2|3, numbered: bool }\n- text: { content: TipTapDoc | plain string (auto-converted; supports paragraphs and "- " bullet lists) }\n- callout: { variant: "info"|"warning"|"tip"|"important", title?, content: TipTapDoc | string }\n- table: { title?, headers: string[], rows: string[][] }\n- chart: { title?, chartType: "bar"|"line"|"pie", points: [{label, value}], seriesName?, series?: [{name, color?, points: [{label, value}]}], unit? ("\u20AC"|"%"|...), showValues: bool, color? }. Multi-series (bar/line only): extra series values are matched to the primary points by index; a legend is rendered automatically.\n- pricing: { title?, items: [{description, quantity?, unit?, price}], currency: "EUR", showTotal: bool, vatRate?, includeVat: bool }\n- timeline: { title?, phases: [{name, description?, duration, deliverables?: string[]}] }\n- two_column: { left: TipTapDoc|string, right: TipTapDoc|string, ratio?: "50_50"|"60_40"|"40_60"|"70_30"|"30_70" }\n- divider: { style: "page_break"|"line"|"space" }\n- signature: { title?, description?, signers: [{id, label, name?, company?, role?}] }\nAll text content runs through a humanizer that strips AI-sounding patterns (em-dashes, clich\xE9s, filler phrases); control it with `humanize`. The result includes ready-to-use links (internal dashboard editor URL + PDF status). Use get-document-link afterwards for a PDF download link or to publish a public customer share link.',
|
|
106590
106592
|
inputSchema: {
|
|
106591
106593
|
type: "object",
|
|
106592
106594
|
properties: {
|
|
@@ -106630,7 +106632,7 @@ var TOOLS = [
|
|
|
106630
106632
|
},
|
|
106631
106633
|
{
|
|
106632
106634
|
name: "update-document",
|
|
106633
|
-
description: 'Update an existing document: change title/status/customer/invoice link, replace all blocks (`blocks`), append blocks to the end (`appendBlocks`), or apply targeted per-block edits (`blockOps`). Use `blockOps` for surgical edits without resending the whole document: each op targets an existing block id (see get-document for ids) and ops are applied in order. Op shapes: { op: "insert", blockId, position?: "before"|"after" (default "after"), blocks: [...] } | { op: "replace", blockId, blocks: [one block] } | { op: "remove", blockId }. Block shapes are identical to create-document. Provided blocks run through the humanizer (see `humanize`).',
|
|
106635
|
+
description: 'Update an existing document: change title/status/customer/invoice link, replace all blocks (`blocks`), append blocks to the end (`appendBlocks`), or apply targeted per-block edits (`blockOps`). Use `blockOps` for surgical edits without resending the whole document: each op targets an existing block id (see get-document for ids) and ops are applied in order. Op shapes: { op: "insert", blockId, position?: "before"|"after" (default "after"), blocks: [...] } | { op: "replace", blockId, blocks: [one block] } | { op: "remove", blockId }. Block shapes are identical to create-document. Provided blocks run through the humanizer (see `humanize`). The result includes ready-to-use links (internal dashboard URL + PDF status); use get-document-link for a PDF download link or a public customer share link.',
|
|
106634
106636
|
inputSchema: {
|
|
106635
106637
|
type: "object",
|
|
106636
106638
|
properties: {
|
|
@@ -106710,6 +106712,24 @@ var TOOLS = [
|
|
|
106710
106712
|
required: ["id"]
|
|
106711
106713
|
}
|
|
106712
106714
|
},
|
|
106715
|
+
{
|
|
106716
|
+
name: "get-document-link",
|
|
106717
|
+
description: "Get usable links for a document so you can hand them back in chat. `linkType` selects what you get:\n- dashboard (default): the internal, login-required editor link, the PDF status, and an existing public link when one is active. For logged-in Refront users.\n- pdf: a PDF download link (no login; the endpoint streams the file via a short-lived signed URL). Triggers a compile when no fresh PDF exists yet, so the link may need a few seconds before it serves the PDF.\n- public_share: ENABLES a public customer share \u2014 sets a share token + visibility=public and triggers a PDF compile \u2014 then returns the /p/<token> link that is safe to send to a customer (e.g. via Telegram). Reuses an existing token so previously shared links keep working. Use only when the document may be shared publicly.\nNever present the dashboard link as a public/customer link: it requires a Refront login.",
|
|
106718
|
+
inputSchema: {
|
|
106719
|
+
type: "object",
|
|
106720
|
+
properties: {
|
|
106721
|
+
teamId: teamIdProp,
|
|
106722
|
+
id: { type: "string", description: "Document ID" },
|
|
106723
|
+
linkType: {
|
|
106724
|
+
type: "string",
|
|
106725
|
+
enum: ["dashboard", "pdf", "public_share"],
|
|
106726
|
+
default: "dashboard",
|
|
106727
|
+
description: "dashboard = internal editor link (login required); pdf = PDF download link; public_share = enable + return a public customer link."
|
|
106728
|
+
}
|
|
106729
|
+
},
|
|
106730
|
+
required: ["id"]
|
|
106731
|
+
}
|
|
106732
|
+
},
|
|
106713
106733
|
{
|
|
106714
106734
|
name: "get-invoices",
|
|
106715
106735
|
description: "List invoices with optional filtering by customer, status, or a search on invoice number / customer name. Use `get-invoice-by-id` for full detail including line items, product snapshots and linked documents. Use this listing to find a (draft) invoice before linking a deliverables document with `invoiceId` on create-document or link-document-to-invoice.",
|
|
@@ -107279,6 +107299,89 @@ var TOOLS = [
|
|
|
107279
107299
|
required: []
|
|
107280
107300
|
}
|
|
107281
107301
|
},
|
|
107302
|
+
{
|
|
107303
|
+
name: "create-time-entries",
|
|
107304
|
+
description: "Create one or more individual tracked time entries (urenregels), one timesheet event per date, so dated hours land as separate draft lines (reliable for agenda/urenoverzicht/facturatie) instead of one lumped log-hours entry. Top-level projectId/ticketId/userId/description/billable/status act as shared defaults; each entry in `entries` can override them. Each entry is stored at 09:00 Europe/Amsterdam on its date for the given number of hours. Processing is per-entry: the result reports created lines, skipped duplicates (same user/team/date/project/description unless allowDuplicate) and per-entry errors, plus totalHours. Dates must be ISO YYYY-MM-DD (normalise relative/partial dates before calling). Use get-projects / get-tickets to resolve ids first; for reading/totalling existing hours use get-time-entries.",
|
|
107305
|
+
inputSchema: {
|
|
107306
|
+
type: "object",
|
|
107307
|
+
properties: {
|
|
107308
|
+
teamId: teamIdProp,
|
|
107309
|
+
projectId: {
|
|
107310
|
+
type: "string",
|
|
107311
|
+
description: "Shared project UUID applied to every entry (an entry's own projectId overrides it)."
|
|
107312
|
+
},
|
|
107313
|
+
ticketId: {
|
|
107314
|
+
type: "string",
|
|
107315
|
+
description: "Shared ticket UUID linked to every entry (an entry's own ticketId overrides it)."
|
|
107316
|
+
},
|
|
107317
|
+
userId: {
|
|
107318
|
+
type: "string",
|
|
107319
|
+
description: "User the entries are booked for. Defaults to the API key user; pass a user UUID to book for another team member (must be a member of the entry's team)."
|
|
107320
|
+
},
|
|
107321
|
+
description: {
|
|
107322
|
+
type: "string",
|
|
107323
|
+
description: "Shared description used for entries that do not set their own."
|
|
107324
|
+
},
|
|
107325
|
+
billable: {
|
|
107326
|
+
type: "boolean",
|
|
107327
|
+
default: true,
|
|
107328
|
+
description: "Shared billable default. true -> billable (to_bill), false -> unbillable."
|
|
107329
|
+
},
|
|
107330
|
+
status: {
|
|
107331
|
+
type: "string",
|
|
107332
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107333
|
+
default: "draft",
|
|
107334
|
+
description: "Shared status default. 'submitted' is treated as 'confirmed'."
|
|
107335
|
+
},
|
|
107336
|
+
allowDuplicate: {
|
|
107337
|
+
type: "boolean",
|
|
107338
|
+
default: false,
|
|
107339
|
+
description: "When false (default) an entry matching an existing line (same user/team/date/project/description) is skipped with a warning instead of creating a duplicate."
|
|
107340
|
+
},
|
|
107341
|
+
entries: {
|
|
107342
|
+
type: "array",
|
|
107343
|
+
minItems: 1,
|
|
107344
|
+
description: "The dated hour lines to create (one timesheet event each).",
|
|
107345
|
+
items: {
|
|
107346
|
+
type: "object",
|
|
107347
|
+
properties: {
|
|
107348
|
+
date: {
|
|
107349
|
+
type: "string",
|
|
107350
|
+
description: "Calendar date of the hours (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107351
|
+
},
|
|
107352
|
+
hours: {
|
|
107353
|
+
type: "number",
|
|
107354
|
+
description: "Worked hours for this date (e.g. 8 or 2.5). Must be > 0."
|
|
107355
|
+
},
|
|
107356
|
+
description: {
|
|
107357
|
+
type: "string",
|
|
107358
|
+
description: "Line description. Falls back to the shared top-level description."
|
|
107359
|
+
},
|
|
107360
|
+
projectId: {
|
|
107361
|
+
type: "string",
|
|
107362
|
+
description: "Project override for this line."
|
|
107363
|
+
},
|
|
107364
|
+
ticketId: {
|
|
107365
|
+
type: "string",
|
|
107366
|
+
description: "Ticket link override for this line."
|
|
107367
|
+
},
|
|
107368
|
+
billable: {
|
|
107369
|
+
type: "boolean",
|
|
107370
|
+
description: "Billable override for this line."
|
|
107371
|
+
},
|
|
107372
|
+
status: {
|
|
107373
|
+
type: "string",
|
|
107374
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107375
|
+
description: "Status override for this line."
|
|
107376
|
+
}
|
|
107377
|
+
},
|
|
107378
|
+
required: ["date", "hours"]
|
|
107379
|
+
}
|
|
107380
|
+
}
|
|
107381
|
+
},
|
|
107382
|
+
required: ["entries"]
|
|
107383
|
+
}
|
|
107384
|
+
},
|
|
107282
107385
|
{
|
|
107283
107386
|
name: "get-trips",
|
|
107284
107387
|
description: "List trips / kilometer registration entries (rides) scoped to your provider team(s), with optional filters by period (dateFrom/dateTo), user, project, customer, trip type (business/private), billing type, and invoiced status. Returns each trip's id, date, start/end location, distance (km), odometer readings, trip type, billing type, rate/amount, linked user/project/customer/invoice/vehicle, plus aggregate business/private/total km and total amount.",
|
|
@@ -107490,7 +107593,7 @@ var TOOLS = [
|
|
|
107490
107593
|
customerId: { type: "string", description: "Filter by customer UUID" },
|
|
107491
107594
|
status: {
|
|
107492
107595
|
type: "string",
|
|
107493
|
-
enum: ["active", "paused", "cancelled", "ended"],
|
|
107596
|
+
enum: ["draft", "active", "paused", "cancelled", "ended"],
|
|
107494
107597
|
description: "Filter by agreement status"
|
|
107495
107598
|
}
|
|
107496
107599
|
}
|
|
@@ -107498,7 +107601,7 @@ var TOOLS = [
|
|
|
107498
107601
|
},
|
|
107499
107602
|
{
|
|
107500
107603
|
name: "create-customer-agreement",
|
|
107501
|
-
description: "Create a customer-specific product agreement
|
|
107604
|
+
description: "Create a customer-specific product agreement as a DRAFT. Snapshots price, discount, clause and invoice line text without mutating the catalog product. Drafts are never invoiced; a human activates them in the dashboard. A draft can still be edited via update-customer-agreement, but once activated it is locked for MCP writes.",
|
|
107502
107605
|
inputSchema: {
|
|
107503
107606
|
type: "object",
|
|
107504
107607
|
properties: {
|
|
@@ -107534,7 +107637,7 @@ var TOOLS = [
|
|
|
107534
107637
|
},
|
|
107535
107638
|
{
|
|
107536
107639
|
name: "update-customer-agreement",
|
|
107537
|
-
description: "Update
|
|
107640
|
+
description: "Update a customer product agreement by id. Allowed ONLY while it is a DRAFT; once activated it is locked and this tool returns an error. It cannot change status (activation is a dashboard action) and there is no delete tool by design.",
|
|
107538
107641
|
inputSchema: {
|
|
107539
107642
|
type: "object",
|
|
107540
107643
|
properties: {
|
|
@@ -107555,11 +107658,7 @@ var TOOLS = [
|
|
|
107555
107658
|
startDate: { type: "string" },
|
|
107556
107659
|
endDate: { type: "string" },
|
|
107557
107660
|
contractPeriodEnd: { type: "string" },
|
|
107558
|
-
proRataFirstInvoice: { type: "boolean" }
|
|
107559
|
-
status: {
|
|
107560
|
-
type: "string",
|
|
107561
|
-
enum: ["active", "paused", "cancelled", "ended"]
|
|
107562
|
-
}
|
|
107661
|
+
proRataFirstInvoice: { type: "boolean" }
|
|
107563
107662
|
},
|
|
107564
107663
|
required: ["id"]
|
|
107565
107664
|
}
|
|
@@ -107784,7 +107883,8 @@ function parseDueDate(dueDate) {
|
|
|
107784
107883
|
const dateOnly = dueDate.split("T")[0];
|
|
107785
107884
|
return {
|
|
107786
107885
|
startTime: `${dateOnly}T00:00:00.000Z`,
|
|
107787
|
-
endTime: `${dateOnly}T23:59:59.999Z
|
|
107886
|
+
endTime: `${dateOnly}T23:59:59.999Z`,
|
|
107887
|
+
allDay: true
|
|
107788
107888
|
};
|
|
107789
107889
|
}
|
|
107790
107890
|
function resolveEventTimes(input) {
|
|
@@ -108853,16 +108953,33 @@ async function handleCreateCustomerAgreement(input) {
|
|
|
108853
108953
|
endDate: input.endDate ?? null,
|
|
108854
108954
|
contractPeriodEnd: input.contractPeriodEnd ?? null,
|
|
108855
108955
|
proRataFirstInvoice: input.proRataFirstInvoice ?? false,
|
|
108856
|
-
|
|
108956
|
+
// MCP-created agreements start as a draft; a human activates them in the
|
|
108957
|
+
// dashboard. Drafts are excluded from every billing/due query.
|
|
108958
|
+
status: "draft"
|
|
108857
108959
|
}).returning();
|
|
108858
|
-
return textResponse2(
|
|
108960
|
+
return textResponse2(
|
|
108961
|
+
`Created customer agreement as **draft**. It will not be invoiced until a human activates it in the dashboard. You can keep editing it via update-customer-agreement while it stays a draft.
|
|
108859
108962
|
|
|
108860
|
-
${formatAgreement(row)}`
|
|
108963
|
+
${formatAgreement(row)}`
|
|
108964
|
+
);
|
|
108861
108965
|
}
|
|
108862
108966
|
async function handleUpdateCustomerAgreement(input) {
|
|
108863
108967
|
const scope = await resolveTeamScope(input.teamId);
|
|
108864
108968
|
if (!scope.ok) return scope.response;
|
|
108865
|
-
const
|
|
108969
|
+
const [existing] = await db.select().from(schema_exports.customerRecurringServices).where(
|
|
108970
|
+
and(
|
|
108971
|
+
eq(schema_exports.customerRecurringServices.id, input.id),
|
|
108972
|
+
inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
|
|
108973
|
+
)
|
|
108974
|
+
).limit(1);
|
|
108975
|
+
if (!existing) {
|
|
108976
|
+
return textResponse2("Agreement not found.");
|
|
108977
|
+
}
|
|
108978
|
+
if (existing.status !== "draft") {
|
|
108979
|
+
return textResponse2(
|
|
108980
|
+
`This agreement is '${existing.status}' and is locked: only drafts can be edited via MCP. Activation and any change to a live agreement happen in the dashboard.`
|
|
108981
|
+
);
|
|
108982
|
+
}
|
|
108866
108983
|
const patch = {
|
|
108867
108984
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
108868
108985
|
};
|
|
@@ -108886,20 +109003,29 @@ async function handleUpdateCustomerAgreement(input) {
|
|
|
108886
109003
|
patch.contractPeriodEnd = input.contractPeriodEnd;
|
|
108887
109004
|
if (input.proRataFirstInvoice !== void 0)
|
|
108888
109005
|
patch.proRataFirstInvoice = input.proRataFirstInvoice;
|
|
108889
|
-
if (input.status !== void 0) patch.status = input.status;
|
|
108890
109006
|
const [row] = await db.update(schema_exports.customerRecurringServices).set(patch).where(
|
|
108891
109007
|
and(
|
|
108892
109008
|
eq(schema_exports.customerRecurringServices.id, input.id),
|
|
108893
|
-
|
|
109009
|
+
inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
|
|
108894
109010
|
)
|
|
108895
109011
|
).returning();
|
|
108896
109012
|
if (!row) {
|
|
108897
109013
|
return textResponse2("Agreement not found.");
|
|
108898
109014
|
}
|
|
108899
|
-
return textResponse2(`Updated
|
|
109015
|
+
return textResponse2(`Updated draft agreement:
|
|
108900
109016
|
|
|
108901
109017
|
${formatAgreement(row)}`);
|
|
108902
109018
|
}
|
|
109019
|
+
function computeDocumentContentHash(params) {
|
|
109020
|
+
const payload = JSON.stringify({
|
|
109021
|
+
blocks: params.blocks ?? [],
|
|
109022
|
+
branding: params.branding ?? {},
|
|
109023
|
+
locale: params.locale,
|
|
109024
|
+
pageSize: params.pageSize,
|
|
109025
|
+
title: params.title
|
|
109026
|
+
});
|
|
109027
|
+
return createHash("sha256").update(payload, "utf8").digest("hex");
|
|
109028
|
+
}
|
|
108903
109029
|
|
|
108904
109030
|
// ../document/src/humanizer/rules.ts
|
|
108905
109031
|
var REPLACEMENTS = [
|
|
@@ -109604,8 +109730,6 @@ function ensureTipTapFormat(text3) {
|
|
|
109604
109730
|
if (isTipTapJson(text3)) return text3;
|
|
109605
109731
|
return JSON.stringify(plainTextToTipTap(text3));
|
|
109606
109732
|
}
|
|
109607
|
-
|
|
109608
|
-
// src/tools/documents.ts
|
|
109609
109733
|
var DOCUMENT_TYPES = [
|
|
109610
109734
|
"proposal",
|
|
109611
109735
|
"plan_of_action",
|
|
@@ -109616,6 +109740,54 @@ var DOCUMENT_TYPES = [
|
|
|
109616
109740
|
"other"
|
|
109617
109741
|
];
|
|
109618
109742
|
var DOCUMENT_STATUSES = ["draft", "published", "archived"];
|
|
109743
|
+
var DOCUMENT_LINK_TYPES = ["dashboard", "pdf", "public_share"];
|
|
109744
|
+
var DASHBOARD_BASE_URL = process.env.DASHBOARD_URL || "https://app.refront.nl";
|
|
109745
|
+
function documentDashboardUrl(id) {
|
|
109746
|
+
return `${DASHBOARD_BASE_URL}/quotations/documents?documentId=${id}&editDocument=true`;
|
|
109747
|
+
}
|
|
109748
|
+
function documentPdfUrl(opts) {
|
|
109749
|
+
return opts.token ? `${DASHBOARD_BASE_URL}/api/download/document?documentToken=${encodeURIComponent(opts.token)}` : `${DASHBOARD_BASE_URL}/api/download/document?id=${opts.id}`;
|
|
109750
|
+
}
|
|
109751
|
+
function documentPublicShareUrl(token) {
|
|
109752
|
+
return `${DASHBOARD_BASE_URL}/p/${encodeURIComponent(token)}`;
|
|
109753
|
+
}
|
|
109754
|
+
function computePdfStatus(doc) {
|
|
109755
|
+
const currentHash = computeDocumentContentHash({
|
|
109756
|
+
blocks: doc.blocks,
|
|
109757
|
+
branding: doc.branding,
|
|
109758
|
+
pageSize: doc.pageSize,
|
|
109759
|
+
locale: doc.locale,
|
|
109760
|
+
title: doc.title
|
|
109761
|
+
});
|
|
109762
|
+
const fresh = doc.compiledHash === currentHash;
|
|
109763
|
+
if (doc.filePath) return fresh ? "ready" : "stale";
|
|
109764
|
+
return fresh ? "pending" : "none";
|
|
109765
|
+
}
|
|
109766
|
+
var PDF_STATUS_LABEL = {
|
|
109767
|
+
ready: "ready",
|
|
109768
|
+
stale: "stale (recompiles on open/download)",
|
|
109769
|
+
pending: "compiling\u2026",
|
|
109770
|
+
none: "not compiled yet"
|
|
109771
|
+
};
|
|
109772
|
+
function buildLinksSection(doc, opts) {
|
|
109773
|
+
let status = computePdfStatus(doc);
|
|
109774
|
+
if (opts?.compileTriggered && status !== "ready") status = "pending";
|
|
109775
|
+
const lines = [
|
|
109776
|
+
"Links:",
|
|
109777
|
+
`- Dashboard (internal, login required): ${documentDashboardUrl(doc.id)}`,
|
|
109778
|
+
`- PDF: ${PDF_STATUS_LABEL[status]}${status === "ready" ? ` \u2014 ${documentPdfUrl({ id: doc.id, token: doc.token })}` : ""}`
|
|
109779
|
+
];
|
|
109780
|
+
if (doc.token && doc.shareVisibility === "public") {
|
|
109781
|
+
lines.push(
|
|
109782
|
+
`- Public share (no login, safe to send to a customer): ${documentPublicShareUrl(doc.token)}`
|
|
109783
|
+
);
|
|
109784
|
+
} else {
|
|
109785
|
+
lines.push(
|
|
109786
|
+
"- Public share: not shared \u2014 call get-document-link with linkType=public_share to publish a customer link."
|
|
109787
|
+
);
|
|
109788
|
+
}
|
|
109789
|
+
return lines.join("\n");
|
|
109790
|
+
}
|
|
109619
109791
|
var blockIdCounter = 0;
|
|
109620
109792
|
function generateBlockId() {
|
|
109621
109793
|
blockIdCounter++;
|
|
@@ -109906,6 +110078,8 @@ Type: ${doc.type} | Status: ${doc.status} | Locale: ${doc.locale}
|
|
|
109906
110078
|
Team: ${doc.teamId}${doc.customerId ? ` | Customer: ${doc.customerId}` : ""}${doc.invoiceId ? ` | Invoice: ${doc.invoiceId}` : ""}
|
|
109907
110079
|
Blocks (${blocks.length}): ${blockTypeSummary(blocks) || "none"}
|
|
109908
110080
|
|
|
110081
|
+
${buildLinksSection(doc)}
|
|
110082
|
+
|
|
109909
110083
|
Blocks JSON:
|
|
109910
110084
|
\`\`\`json
|
|
109911
110085
|
${JSON.stringify(blocks, null, 2)}
|
|
@@ -109914,6 +110088,104 @@ ${JSON.stringify(blocks, null, 2)}
|
|
|
109914
110088
|
]
|
|
109915
110089
|
};
|
|
109916
110090
|
}
|
|
110091
|
+
async function handleGetDocumentLink(input) {
|
|
110092
|
+
if (!input.id) {
|
|
110093
|
+
return { content: [{ type: "text", text: "Error: `id` is required." }] };
|
|
110094
|
+
}
|
|
110095
|
+
const linkType = input.linkType ?? "dashboard";
|
|
110096
|
+
if (!DOCUMENT_LINK_TYPES.includes(linkType)) {
|
|
110097
|
+
return {
|
|
110098
|
+
content: [
|
|
110099
|
+
{
|
|
110100
|
+
type: "text",
|
|
110101
|
+
text: `Error: invalid linkType "${linkType}". Allowed: ${DOCUMENT_LINK_TYPES.join(", ")}.`
|
|
110102
|
+
}
|
|
110103
|
+
]
|
|
110104
|
+
};
|
|
110105
|
+
}
|
|
110106
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
110107
|
+
if (!scope.ok) return scope.response;
|
|
110108
|
+
const doc = await findAccessibleDocument(input.id, scope.teamIds);
|
|
110109
|
+
if (!doc) {
|
|
110110
|
+
return {
|
|
110111
|
+
content: [
|
|
110112
|
+
{
|
|
110113
|
+
type: "text",
|
|
110114
|
+
text: `Document ${input.id} not found or you don't have access to it.`
|
|
110115
|
+
}
|
|
110116
|
+
]
|
|
110117
|
+
};
|
|
110118
|
+
}
|
|
110119
|
+
const header = `**${doc.title}**
|
|
110120
|
+
ID: ${doc.id}
|
|
110121
|
+
Status: ${doc.status} | Visibility: ${doc.shareVisibility}
|
|
110122
|
+
|
|
110123
|
+
`;
|
|
110124
|
+
const freshHash = computeDocumentContentHash({
|
|
110125
|
+
blocks: doc.blocks,
|
|
110126
|
+
branding: doc.branding,
|
|
110127
|
+
pageSize: doc.pageSize,
|
|
110128
|
+
locale: doc.locale,
|
|
110129
|
+
title: doc.title
|
|
110130
|
+
});
|
|
110131
|
+
if (linkType === "public_share") {
|
|
110132
|
+
const alreadyPublic = Boolean(doc.token) && doc.shareVisibility === "public";
|
|
110133
|
+
const token = doc.token ?? randomBytes(32).toString("base64url");
|
|
110134
|
+
const needsCompile = computePdfStatus(doc) !== "ready";
|
|
110135
|
+
const patch = {
|
|
110136
|
+
token,
|
|
110137
|
+
shareVisibility: "public",
|
|
110138
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110139
|
+
};
|
|
110140
|
+
if (needsCompile) {
|
|
110141
|
+
patch.filePath = null;
|
|
110142
|
+
patch.fileSize = null;
|
|
110143
|
+
patch.compiledHash = freshHash;
|
|
110144
|
+
}
|
|
110145
|
+
await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
|
|
110146
|
+
const pdfLine = needsCompile ? `
|
|
110147
|
+
${await maybeTriggerPdfCompile(doc.id, doc.teamId)}` : "";
|
|
110148
|
+
return {
|
|
110149
|
+
content: [
|
|
110150
|
+
{
|
|
110151
|
+
type: "text",
|
|
110152
|
+
text: header + `${alreadyPublic ? "Public share already enabled." : "\u2705 Public share enabled (visibility \u2192 public)."}
|
|
110153
|
+
Public link (no login, safe to send to a customer): ${documentPublicShareUrl(token)}
|
|
110154
|
+
PDF download: ${documentPdfUrl({ id: doc.id, token })}
|
|
110155
|
+
PDF status: ${needsCompile ? "compiling\u2026 (ready shortly)" : "ready"}` + pdfLine
|
|
110156
|
+
}
|
|
110157
|
+
]
|
|
110158
|
+
};
|
|
110159
|
+
}
|
|
110160
|
+
if (linkType === "pdf") {
|
|
110161
|
+
let status = computePdfStatus(doc);
|
|
110162
|
+
let pdfLine = "";
|
|
110163
|
+
if (status === "none" || status === "stale") {
|
|
110164
|
+
await db.update(schema_exports.documents).set({
|
|
110165
|
+
filePath: null,
|
|
110166
|
+
fileSize: null,
|
|
110167
|
+
compiledHash: freshHash,
|
|
110168
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110169
|
+
}).where(eq(schema_exports.documents.id, doc.id));
|
|
110170
|
+
pdfLine = `
|
|
110171
|
+
${await maybeTriggerPdfCompile(doc.id, doc.teamId)}`;
|
|
110172
|
+
status = "pending";
|
|
110173
|
+
}
|
|
110174
|
+
return {
|
|
110175
|
+
content: [
|
|
110176
|
+
{
|
|
110177
|
+
type: "text",
|
|
110178
|
+
text: header + `PDF status: ${PDF_STATUS_LABEL[status]}
|
|
110179
|
+
PDF download: ${documentPdfUrl({ id: doc.id, token: doc.token })}
|
|
110180
|
+
The endpoint streams the PDF via a short-lived signed storage URL and recompiles on demand if needed.` + pdfLine
|
|
110181
|
+
}
|
|
110182
|
+
]
|
|
110183
|
+
};
|
|
110184
|
+
}
|
|
110185
|
+
return {
|
|
110186
|
+
content: [{ type: "text", text: header + buildLinksSection(doc) }]
|
|
110187
|
+
};
|
|
110188
|
+
}
|
|
109917
110189
|
async function handleCreateDocument(input) {
|
|
109918
110190
|
const {
|
|
109919
110191
|
title,
|
|
@@ -109994,6 +110266,21 @@ async function handleCreateDocument(input) {
|
|
|
109994
110266
|
};
|
|
109995
110267
|
}
|
|
109996
110268
|
const pdfLine = compilePdf ? await maybeTriggerPdfCompile(created.id, resolved.teamId) : null;
|
|
110269
|
+
const linksSection = buildLinksSection(
|
|
110270
|
+
{
|
|
110271
|
+
id: created.id,
|
|
110272
|
+
title: title.trim(),
|
|
110273
|
+
blocks: humanized.blocks,
|
|
110274
|
+
branding: {},
|
|
110275
|
+
pageSize: "a4",
|
|
110276
|
+
locale,
|
|
110277
|
+
filePath: null,
|
|
110278
|
+
compiledHash: null,
|
|
110279
|
+
token: null,
|
|
110280
|
+
shareVisibility: "private"
|
|
110281
|
+
},
|
|
110282
|
+
{ compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
|
|
110283
|
+
);
|
|
109997
110284
|
return {
|
|
109998
110285
|
content: [
|
|
109999
110286
|
{
|
|
@@ -110008,7 +110295,9 @@ ${invoiceLine ? `${invoiceLine}
|
|
|
110008
110295
|
` : ""}Blocks (${humanized.blocks.length}): ${blockTypeSummary(humanized.blocks)}
|
|
110009
110296
|
|
|
110010
110297
|
${humanized.summary}${pdfLine ? `
|
|
110011
|
-
${pdfLine}` : ""}
|
|
110298
|
+
${pdfLine}` : ""}
|
|
110299
|
+
|
|
110300
|
+
` + linksSection
|
|
110012
110301
|
}
|
|
110013
110302
|
]
|
|
110014
110303
|
};
|
|
@@ -110171,6 +110460,22 @@ async function handleUpdateDocument(input) {
|
|
|
110171
110460
|
}
|
|
110172
110461
|
await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
|
|
110173
110462
|
const pdfLine = compilePdf ? await maybeTriggerPdfCompile(doc.id, doc.teamId) : null;
|
|
110463
|
+
const linksSection = buildLinksSection(
|
|
110464
|
+
{
|
|
110465
|
+
id: doc.id,
|
|
110466
|
+
title: patch.title ?? doc.title,
|
|
110467
|
+
status: patch.status ?? doc.status,
|
|
110468
|
+
blocks: patch.blocks ?? doc.blocks,
|
|
110469
|
+
branding: doc.branding,
|
|
110470
|
+
pageSize: doc.pageSize,
|
|
110471
|
+
locale: doc.locale,
|
|
110472
|
+
filePath: doc.filePath,
|
|
110473
|
+
compiledHash: doc.compiledHash,
|
|
110474
|
+
token: doc.token,
|
|
110475
|
+
shareVisibility: doc.shareVisibility
|
|
110476
|
+
},
|
|
110477
|
+
{ compileTriggered: compilePdf && Boolean(process.env.TRIGGER_SECRET_KEY) }
|
|
110478
|
+
);
|
|
110174
110479
|
return {
|
|
110175
110480
|
content: [
|
|
110176
110481
|
{
|
|
@@ -110181,7 +110486,9 @@ ID: ${doc.id}
|
|
|
110181
110486
|
${changeLines.map((l4) => `- ${l4}`).join("\n")}${humanizeSummary ? `
|
|
110182
110487
|
|
|
110183
110488
|
${humanizeSummary}` : ""}${pdfLine ? `
|
|
110184
|
-
${pdfLine}` : ""}
|
|
110489
|
+
${pdfLine}` : ""}
|
|
110490
|
+
|
|
110491
|
+
` + linksSection
|
|
110185
110492
|
}
|
|
110186
110493
|
]
|
|
110187
110494
|
};
|
|
@@ -114432,6 +114739,278 @@ async function handleGetTimeEntries(input) {
|
|
|
114432
114739
|
entriesTruncated: entryCount > entries.length
|
|
114433
114740
|
});
|
|
114434
114741
|
}
|
|
114742
|
+
var MAX_CREATE_ENTRIES = 100;
|
|
114743
|
+
var DEFAULT_START_HOUR = "09:00:00";
|
|
114744
|
+
function mapCreateStatus(status) {
|
|
114745
|
+
if (status == null) return "draft";
|
|
114746
|
+
if (status === "draft") return "draft";
|
|
114747
|
+
if (status === "confirmed" || status === "submitted") return "confirmed";
|
|
114748
|
+
return null;
|
|
114749
|
+
}
|
|
114750
|
+
function isValidIsoDate(value) {
|
|
114751
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
114752
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
114753
|
+
if (Number.isNaN(parsed.getTime())) return false;
|
|
114754
|
+
return parsed.toISOString().slice(0, 10) === value;
|
|
114755
|
+
}
|
|
114756
|
+
async function handleCreateTimeEntries(input) {
|
|
114757
|
+
const ctx = getAuthContext();
|
|
114758
|
+
const te = schema_exports.timesheetEvents;
|
|
114759
|
+
if (!Array.isArray(input.entries) || input.entries.length === 0) {
|
|
114760
|
+
return textResponse3(
|
|
114761
|
+
"Error: `entries` must be a non-empty array of { date, hours, description? } objects."
|
|
114762
|
+
);
|
|
114763
|
+
}
|
|
114764
|
+
if (input.entries.length > MAX_CREATE_ENTRIES) {
|
|
114765
|
+
return textResponse3(
|
|
114766
|
+
`Error: too many entries (${input.entries.length}). Max ${MAX_CREATE_ENTRIES} per call.`
|
|
114767
|
+
);
|
|
114768
|
+
}
|
|
114769
|
+
const sharedStatus = mapCreateStatus(input.status);
|
|
114770
|
+
if (sharedStatus === null) {
|
|
114771
|
+
return textResponse3(
|
|
114772
|
+
`Error: invalid status "${input.status}". Allowed: draft, confirmed (submitted is treated as confirmed).`
|
|
114773
|
+
);
|
|
114774
|
+
}
|
|
114775
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
114776
|
+
if (!scope.ok) return scope.response;
|
|
114777
|
+
if (scope.teamIds.length === 0) {
|
|
114778
|
+
return textResponse3("No accessible teams found.");
|
|
114779
|
+
}
|
|
114780
|
+
const projectIds = /* @__PURE__ */ new Set();
|
|
114781
|
+
const ticketIds = /* @__PURE__ */ new Set();
|
|
114782
|
+
if (input.projectId) projectIds.add(input.projectId);
|
|
114783
|
+
if (input.ticketId) ticketIds.add(input.ticketId);
|
|
114784
|
+
for (const entry of input.entries) {
|
|
114785
|
+
if (entry?.projectId) projectIds.add(entry.projectId);
|
|
114786
|
+
if (entry?.ticketId) ticketIds.add(entry.ticketId);
|
|
114787
|
+
}
|
|
114788
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
114789
|
+
for (const projectId of projectIds) {
|
|
114790
|
+
if (!scope.projectIds.includes(projectId)) {
|
|
114791
|
+
return textResponse3(
|
|
114792
|
+
`Project not found or no access: ${projectId}. Call get-projects first.`
|
|
114793
|
+
);
|
|
114794
|
+
}
|
|
114795
|
+
const [project] = await db.select({
|
|
114796
|
+
id: schema_exports.projects.id,
|
|
114797
|
+
name: schema_exports.projects.name,
|
|
114798
|
+
teamId: schema_exports.projects.teamId
|
|
114799
|
+
}).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
|
|
114800
|
+
if (!project) return textResponse3(`Project not found: ${projectId}.`);
|
|
114801
|
+
projectMap.set(projectId, project);
|
|
114802
|
+
}
|
|
114803
|
+
const ticketMap = /* @__PURE__ */ new Map();
|
|
114804
|
+
for (const ticketId of ticketIds) {
|
|
114805
|
+
const [ticket] = await db.select({
|
|
114806
|
+
id: schema_exports.tickets.id,
|
|
114807
|
+
title: schema_exports.tickets.title,
|
|
114808
|
+
teamId: schema_exports.tickets.teamId,
|
|
114809
|
+
projectId: schema_exports.tickets.projectId,
|
|
114810
|
+
customerId: schema_exports.tickets.customerId
|
|
114811
|
+
}).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, ticketId)).limit(1);
|
|
114812
|
+
if (!ticket) {
|
|
114813
|
+
return textResponse3(`Ticket not found: ${ticketId}. Call get-tickets first.`);
|
|
114814
|
+
}
|
|
114815
|
+
let hasAccess = scope.teamIds.includes(ticket.teamId);
|
|
114816
|
+
if (!hasAccess && ticket.projectId)
|
|
114817
|
+
hasAccess = scope.projectIds.includes(ticket.projectId);
|
|
114818
|
+
if (!hasAccess && ticket.customerId)
|
|
114819
|
+
hasAccess = scope.customerIds.includes(ticket.customerId);
|
|
114820
|
+
if (!hasAccess) {
|
|
114821
|
+
return textResponse3(`No access to ticket: ${ticketId}. Call get-tickets first.`);
|
|
114822
|
+
}
|
|
114823
|
+
ticketMap.set(ticketId, ticket);
|
|
114824
|
+
}
|
|
114825
|
+
const needsFallbackTeam = input.entries.some((entry) => {
|
|
114826
|
+
const ticketId = entry?.ticketId ?? input.ticketId;
|
|
114827
|
+
const projectId = entry?.projectId ?? input.projectId;
|
|
114828
|
+
const ticket = ticketId ? ticketMap.get(ticketId) : null;
|
|
114829
|
+
const project = projectId ? projectMap.get(projectId) : null;
|
|
114830
|
+
return !(ticket?.teamId ?? project?.teamId);
|
|
114831
|
+
});
|
|
114832
|
+
let fallbackTeamId = null;
|
|
114833
|
+
if (needsFallbackTeam) {
|
|
114834
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
114835
|
+
if (!resolved.ok) return resolved.response;
|
|
114836
|
+
fallbackTeamId = resolved.teamId;
|
|
114837
|
+
}
|
|
114838
|
+
const rawUserId = input.userId?.trim();
|
|
114839
|
+
const targetUserId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
|
|
114840
|
+
const membershipCache = /* @__PURE__ */ new Map();
|
|
114841
|
+
const isTargetMember = async (teamId) => {
|
|
114842
|
+
if (targetUserId === ctx.userId) return true;
|
|
114843
|
+
const cached3 = membershipCache.get(teamId);
|
|
114844
|
+
if (cached3 !== void 0) return cached3;
|
|
114845
|
+
const member2 = await isUserTeamMember(targetUserId, teamId);
|
|
114846
|
+
membershipCache.set(teamId, member2);
|
|
114847
|
+
return member2;
|
|
114848
|
+
};
|
|
114849
|
+
const created = [];
|
|
114850
|
+
const skipped = [];
|
|
114851
|
+
const errors = [];
|
|
114852
|
+
for (let index2 = 0; index2 < input.entries.length; index2++) {
|
|
114853
|
+
const entry = input.entries[index2];
|
|
114854
|
+
const date10 = typeof entry?.date === "string" ? entry.date.trim() : "";
|
|
114855
|
+
try {
|
|
114856
|
+
if (!entry || typeof entry !== "object") {
|
|
114857
|
+
errors.push({ index: index2, date: null, message: "Entry must be an object." });
|
|
114858
|
+
continue;
|
|
114859
|
+
}
|
|
114860
|
+
if (!isValidIsoDate(date10)) {
|
|
114861
|
+
errors.push({
|
|
114862
|
+
index: index2,
|
|
114863
|
+
date: date10 || null,
|
|
114864
|
+
message: "Invalid or missing `date`; expected YYYY-MM-DD."
|
|
114865
|
+
});
|
|
114866
|
+
continue;
|
|
114867
|
+
}
|
|
114868
|
+
const hours = toNumber(entry.hours);
|
|
114869
|
+
if (!(hours > 0)) {
|
|
114870
|
+
errors.push({
|
|
114871
|
+
index: index2,
|
|
114872
|
+
date: date10,
|
|
114873
|
+
message: "`hours` must be a number greater than 0."
|
|
114874
|
+
});
|
|
114875
|
+
continue;
|
|
114876
|
+
}
|
|
114877
|
+
if (hours > 24) {
|
|
114878
|
+
errors.push({
|
|
114879
|
+
index: index2,
|
|
114880
|
+
date: date10,
|
|
114881
|
+
message: "`hours` must not exceed 24 for a single day."
|
|
114882
|
+
});
|
|
114883
|
+
continue;
|
|
114884
|
+
}
|
|
114885
|
+
const description = (entry.description ?? input.description ?? "").trim();
|
|
114886
|
+
if (!description) {
|
|
114887
|
+
errors.push({
|
|
114888
|
+
index: index2,
|
|
114889
|
+
date: date10,
|
|
114890
|
+
message: "A description is required (set entry.description or a shared description)."
|
|
114891
|
+
});
|
|
114892
|
+
continue;
|
|
114893
|
+
}
|
|
114894
|
+
const status = entry.status === void 0 ? sharedStatus : mapCreateStatus(entry.status);
|
|
114895
|
+
if (status === null) {
|
|
114896
|
+
errors.push({
|
|
114897
|
+
index: index2,
|
|
114898
|
+
date: date10,
|
|
114899
|
+
message: `Invalid status "${entry.status}". Allowed: draft, confirmed.`
|
|
114900
|
+
});
|
|
114901
|
+
continue;
|
|
114902
|
+
}
|
|
114903
|
+
const billable = entry.billable ?? input.billable ?? true;
|
|
114904
|
+
const billingStatus = billable ? "to_bill" : "unbillable";
|
|
114905
|
+
const ticketId = entry.ticketId ?? input.ticketId ?? null;
|
|
114906
|
+
const projectId = entry.projectId ?? input.projectId ?? null;
|
|
114907
|
+
const ticket = ticketId ? ticketMap.get(ticketId) : null;
|
|
114908
|
+
const project = projectId ? projectMap.get(projectId) : null;
|
|
114909
|
+
const insertTeamId = ticket?.teamId ?? project?.teamId ?? fallbackTeamId;
|
|
114910
|
+
if (!insertTeamId) {
|
|
114911
|
+
errors.push({
|
|
114912
|
+
index: index2,
|
|
114913
|
+
date: date10,
|
|
114914
|
+
message: "Could not resolve a team for this entry; pass teamId, projectId or ticketId."
|
|
114915
|
+
});
|
|
114916
|
+
continue;
|
|
114917
|
+
}
|
|
114918
|
+
if (!await isTargetMember(insertTeamId)) {
|
|
114919
|
+
errors.push({
|
|
114920
|
+
index: index2,
|
|
114921
|
+
date: date10,
|
|
114922
|
+
message: `User ${targetUserId} is not a member of team ${insertTeamId}.`
|
|
114923
|
+
});
|
|
114924
|
+
continue;
|
|
114925
|
+
}
|
|
114926
|
+
const effectiveProjectId = project?.id ?? ticket?.projectId ?? null;
|
|
114927
|
+
if (!input.allowDuplicate) {
|
|
114928
|
+
const dupConditions = [
|
|
114929
|
+
eq(te.teamId, insertTeamId),
|
|
114930
|
+
eq(te.userId, targetUserId),
|
|
114931
|
+
eq(te.isDeleted, false),
|
|
114932
|
+
sql`(${te.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date = ${date10}::date`,
|
|
114933
|
+
sql`lower(${te.title}) = lower(${description})`
|
|
114934
|
+
];
|
|
114935
|
+
dupConditions.push(
|
|
114936
|
+
effectiveProjectId ? eq(te.projectId, effectiveProjectId) : sql`${te.projectId} IS NULL`
|
|
114937
|
+
);
|
|
114938
|
+
const [dup] = await db.select({ id: te.id }).from(te).where(and(...dupConditions)).limit(1);
|
|
114939
|
+
if (dup) {
|
|
114940
|
+
skipped.push({
|
|
114941
|
+
index: index2,
|
|
114942
|
+
date: date10,
|
|
114943
|
+
hours: Math.round(hours * 100) / 100,
|
|
114944
|
+
description,
|
|
114945
|
+
reason: "duplicate",
|
|
114946
|
+
existingId: dup.id
|
|
114947
|
+
});
|
|
114948
|
+
continue;
|
|
114949
|
+
}
|
|
114950
|
+
}
|
|
114951
|
+
const localStart = `${date10} ${DEFAULT_START_HOUR}`;
|
|
114952
|
+
const startExpr = sql`(${localStart}::timestamp AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})`;
|
|
114953
|
+
const endExpr = sql`(${startExpr} + ${`${hours} hours`}::interval)`;
|
|
114954
|
+
const [row] = await db.insert(te).values({
|
|
114955
|
+
teamId: insertTeamId,
|
|
114956
|
+
userId: targetUserId,
|
|
114957
|
+
title: description,
|
|
114958
|
+
description,
|
|
114959
|
+
type: "work",
|
|
114960
|
+
status,
|
|
114961
|
+
startTime: startExpr,
|
|
114962
|
+
endTime: endExpr,
|
|
114963
|
+
allDay: false,
|
|
114964
|
+
isTracked: true,
|
|
114965
|
+
trackedDuration: Math.round(hours * 3600),
|
|
114966
|
+
projectId: effectiveProjectId,
|
|
114967
|
+
customerId: ticket?.customerId ?? null,
|
|
114968
|
+
billingStatus
|
|
114969
|
+
}).returning({ id: te.id });
|
|
114970
|
+
if (!row) {
|
|
114971
|
+
errors.push({ index: index2, date: date10, message: "Insert failed." });
|
|
114972
|
+
continue;
|
|
114973
|
+
}
|
|
114974
|
+
if (ticketId) {
|
|
114975
|
+
await db.insert(schema_exports.timesheetEventTickets).values({ timesheetEventId: row.id, ticketId }).onConflictDoNothing();
|
|
114976
|
+
}
|
|
114977
|
+
created.push({
|
|
114978
|
+
id: row.id,
|
|
114979
|
+
date: date10,
|
|
114980
|
+
hours: Math.round(hours * 100) / 100,
|
|
114981
|
+
description,
|
|
114982
|
+
status,
|
|
114983
|
+
billable,
|
|
114984
|
+
project: effectiveProjectId ? {
|
|
114985
|
+
id: effectiveProjectId,
|
|
114986
|
+
name: projectMap.get(effectiveProjectId)?.name ?? null
|
|
114987
|
+
} : null,
|
|
114988
|
+
ticket: ticket ? { id: ticket.id, title: ticket.title } : null
|
|
114989
|
+
});
|
|
114990
|
+
} catch (error49) {
|
|
114991
|
+
errors.push({
|
|
114992
|
+
index: index2,
|
|
114993
|
+
date: date10 || null,
|
|
114994
|
+
message: error49 instanceof Error ? error49.message : String(error49)
|
|
114995
|
+
});
|
|
114996
|
+
}
|
|
114997
|
+
}
|
|
114998
|
+
const totalHours = Math.round(created.reduce((sum, c6) => sum + c6.hours, 0) * 100) / 100;
|
|
114999
|
+
return jsonResponse({
|
|
115000
|
+
teamScope: scope.teamIds,
|
|
115001
|
+
bookedForUserId: targetUserId,
|
|
115002
|
+
timezone: TIMEZONE,
|
|
115003
|
+
created,
|
|
115004
|
+
skipped,
|
|
115005
|
+
errors,
|
|
115006
|
+
totals: {
|
|
115007
|
+
createdCount: created.length,
|
|
115008
|
+
skippedCount: skipped.length,
|
|
115009
|
+
errorCount: errors.length,
|
|
115010
|
+
totalHours
|
|
115011
|
+
}
|
|
115012
|
+
});
|
|
115013
|
+
}
|
|
114435
115014
|
|
|
114436
115015
|
// ../invoice/src/utils/included-items.ts
|
|
114437
115016
|
function parseIncludedItems(value) {
|
|
@@ -117266,9 +117845,9 @@ function templateDefaultsFromStored(template, currency) {
|
|
|
117266
117845
|
}
|
|
117267
117846
|
|
|
117268
117847
|
// src/tools/quotes.ts
|
|
117269
|
-
var
|
|
117848
|
+
var DASHBOARD_BASE_URL2 = "https://app.refront.nl";
|
|
117270
117849
|
function quoteDashboardUrl(id) {
|
|
117271
|
-
return `${
|
|
117850
|
+
return `${DASHBOARD_BASE_URL2}/quotations?quotationId=${id}"ationDetails=true`;
|
|
117272
117851
|
}
|
|
117273
117852
|
var QUOTE_STATUSES = [
|
|
117274
117853
|
"draft",
|
|
@@ -125670,7 +126249,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
|
|
|
125670
126249
|
}
|
|
125671
126250
|
|
|
125672
126251
|
// src/server.ts
|
|
125673
|
-
var SERVER_VERSION = "3.5.
|
|
126252
|
+
var SERVER_VERSION = "3.5.12";
|
|
125674
126253
|
function createMcpServer() {
|
|
125675
126254
|
const server = new Server(
|
|
125676
126255
|
{
|
|
@@ -125806,6 +126385,10 @@ function createMcpServer() {
|
|
|
125806
126385
|
return await handleListDocuments(asToolArgs(toolArgs));
|
|
125807
126386
|
case "get-document":
|
|
125808
126387
|
return await handleGetDocument(asToolArgs(toolArgs));
|
|
126388
|
+
case "get-document-link":
|
|
126389
|
+
return await handleGetDocumentLink(
|
|
126390
|
+
asToolArgs(toolArgs)
|
|
126391
|
+
);
|
|
125809
126392
|
case "create-document":
|
|
125810
126393
|
return await handleCreateDocument(
|
|
125811
126394
|
asToolArgs(toolArgs)
|
|
@@ -125902,6 +126485,10 @@ function createMcpServer() {
|
|
|
125902
126485
|
return await handleGetTimeEntries(
|
|
125903
126486
|
asToolArgs(toolArgs)
|
|
125904
126487
|
);
|
|
126488
|
+
case "create-time-entries":
|
|
126489
|
+
return await handleCreateTimeEntries(
|
|
126490
|
+
asToolArgs(toolArgs)
|
|
126491
|
+
);
|
|
125905
126492
|
case "get-github-file":
|
|
125906
126493
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
125907
126494
|
case "list-github-directory":
|