@mgsoftwarebv/mcp-server-bridge 3.5.11 → 3.5.13
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 +667 -18
- 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';
|
|
@@ -106576,7 +106576,7 @@ var TOOLS = [
|
|
|
106576
106576
|
},
|
|
106577
106577
|
{
|
|
106578
106578
|
name: "get-document",
|
|
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.",
|
|
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.",
|
|
106580
106580
|
inputSchema: {
|
|
106581
106581
|
type: "object",
|
|
106582
106582
|
properties: {
|
|
@@ -106586,9 +106586,27 @@ var TOOLS = [
|
|
|
106586
106586
|
required: ["id"]
|
|
106587
106587
|
}
|
|
106588
106588
|
},
|
|
106589
|
+
{
|
|
106590
|
+
name: "get-document-link",
|
|
106591
|
+
description: "Get a safe, usable link for a document (use after create-document to share it). Never leaks an auth/session URL. Choose `linkType`:\n- dashboard (default): internal editor deep link for logged-in Refront users, plus the current PDF status and a public link when the document is already shared publicly.\n- pdf_signed (alias: pdf): a temporary signed PDF download URL when a compiled PDF exists; if it is still compiling it returns pdfStatus 'pending' with pdfUrl null and triggers a (re)compile instead of failing.\n- public_share: enables a public customer share (token + visibility=public, recompiling only when no fresh PDF exists) and returns the /p/<token> link. Only use when the document may be shared publicly. Revoke later from the dashboard.\nReturns a JSON object: { documentId, title, status, dashboardUrl, pdfStatus (ready|stale|pending|unavailable), pdfUrl, publicShareUrl, expiresAt }. Respects team/provider access; draft/private documents never expose a public share URL unless they are actually shared.",
|
|
106592
|
+
inputSchema: {
|
|
106593
|
+
type: "object",
|
|
106594
|
+
properties: {
|
|
106595
|
+
teamId: teamIdProp,
|
|
106596
|
+
documentId: { type: "string", description: "Document ID (UUID)" },
|
|
106597
|
+
linkType: {
|
|
106598
|
+
type: "string",
|
|
106599
|
+
enum: ["dashboard", "pdf_signed", "pdf", "public_share"],
|
|
106600
|
+
default: "dashboard",
|
|
106601
|
+
description: "dashboard (default) = editor URL + PDF status; pdf_signed (alias pdf) = temporary signed PDF download URL; public_share = enable + return the public /p/<token> link."
|
|
106602
|
+
}
|
|
106603
|
+
},
|
|
106604
|
+
required: ["documentId"]
|
|
106605
|
+
}
|
|
106606
|
+
},
|
|
106589
106607
|
{
|
|
106590
106608
|
name: "create-document",
|
|
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`.',
|
|
106609
|
+
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.',
|
|
106592
106610
|
inputSchema: {
|
|
106593
106611
|
type: "object",
|
|
106594
106612
|
properties: {
|
|
@@ -106632,7 +106650,7 @@ var TOOLS = [
|
|
|
106632
106650
|
},
|
|
106633
106651
|
{
|
|
106634
106652
|
name: "update-document",
|
|
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`).',
|
|
106653
|
+
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.',
|
|
106636
106654
|
inputSchema: {
|
|
106637
106655
|
type: "object",
|
|
106638
106656
|
properties: {
|
|
@@ -106712,6 +106730,24 @@ var TOOLS = [
|
|
|
106712
106730
|
required: ["id"]
|
|
106713
106731
|
}
|
|
106714
106732
|
},
|
|
106733
|
+
{
|
|
106734
|
+
name: "get-document-link",
|
|
106735
|
+
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.",
|
|
106736
|
+
inputSchema: {
|
|
106737
|
+
type: "object",
|
|
106738
|
+
properties: {
|
|
106739
|
+
teamId: teamIdProp,
|
|
106740
|
+
id: { type: "string", description: "Document ID" },
|
|
106741
|
+
linkType: {
|
|
106742
|
+
type: "string",
|
|
106743
|
+
enum: ["dashboard", "pdf", "public_share"],
|
|
106744
|
+
default: "dashboard",
|
|
106745
|
+
description: "dashboard = internal editor link (login required); pdf = PDF download link; public_share = enable + return a public customer link."
|
|
106746
|
+
}
|
|
106747
|
+
},
|
|
106748
|
+
required: ["id"]
|
|
106749
|
+
}
|
|
106750
|
+
},
|
|
106715
106751
|
{
|
|
106716
106752
|
name: "get-invoices",
|
|
106717
106753
|
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.",
|
|
@@ -107281,6 +107317,89 @@ var TOOLS = [
|
|
|
107281
107317
|
required: []
|
|
107282
107318
|
}
|
|
107283
107319
|
},
|
|
107320
|
+
{
|
|
107321
|
+
name: "create-time-entries",
|
|
107322
|
+
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.",
|
|
107323
|
+
inputSchema: {
|
|
107324
|
+
type: "object",
|
|
107325
|
+
properties: {
|
|
107326
|
+
teamId: teamIdProp,
|
|
107327
|
+
projectId: {
|
|
107328
|
+
type: "string",
|
|
107329
|
+
description: "Shared project UUID applied to every entry (an entry's own projectId overrides it)."
|
|
107330
|
+
},
|
|
107331
|
+
ticketId: {
|
|
107332
|
+
type: "string",
|
|
107333
|
+
description: "Shared ticket UUID linked to every entry (an entry's own ticketId overrides it)."
|
|
107334
|
+
},
|
|
107335
|
+
userId: {
|
|
107336
|
+
type: "string",
|
|
107337
|
+
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)."
|
|
107338
|
+
},
|
|
107339
|
+
description: {
|
|
107340
|
+
type: "string",
|
|
107341
|
+
description: "Shared description used for entries that do not set their own."
|
|
107342
|
+
},
|
|
107343
|
+
billable: {
|
|
107344
|
+
type: "boolean",
|
|
107345
|
+
default: true,
|
|
107346
|
+
description: "Shared billable default. true -> billable (to_bill), false -> unbillable."
|
|
107347
|
+
},
|
|
107348
|
+
status: {
|
|
107349
|
+
type: "string",
|
|
107350
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107351
|
+
default: "draft",
|
|
107352
|
+
description: "Shared status default. 'submitted' is treated as 'confirmed'."
|
|
107353
|
+
},
|
|
107354
|
+
allowDuplicate: {
|
|
107355
|
+
type: "boolean",
|
|
107356
|
+
default: false,
|
|
107357
|
+
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."
|
|
107358
|
+
},
|
|
107359
|
+
entries: {
|
|
107360
|
+
type: "array",
|
|
107361
|
+
minItems: 1,
|
|
107362
|
+
description: "The dated hour lines to create (one timesheet event each).",
|
|
107363
|
+
items: {
|
|
107364
|
+
type: "object",
|
|
107365
|
+
properties: {
|
|
107366
|
+
date: {
|
|
107367
|
+
type: "string",
|
|
107368
|
+
description: "Calendar date of the hours (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107369
|
+
},
|
|
107370
|
+
hours: {
|
|
107371
|
+
type: "number",
|
|
107372
|
+
description: "Worked hours for this date (e.g. 8 or 2.5). Must be > 0."
|
|
107373
|
+
},
|
|
107374
|
+
description: {
|
|
107375
|
+
type: "string",
|
|
107376
|
+
description: "Line description. Falls back to the shared top-level description."
|
|
107377
|
+
},
|
|
107378
|
+
projectId: {
|
|
107379
|
+
type: "string",
|
|
107380
|
+
description: "Project override for this line."
|
|
107381
|
+
},
|
|
107382
|
+
ticketId: {
|
|
107383
|
+
type: "string",
|
|
107384
|
+
description: "Ticket link override for this line."
|
|
107385
|
+
},
|
|
107386
|
+
billable: {
|
|
107387
|
+
type: "boolean",
|
|
107388
|
+
description: "Billable override for this line."
|
|
107389
|
+
},
|
|
107390
|
+
status: {
|
|
107391
|
+
type: "string",
|
|
107392
|
+
enum: ["draft", "confirmed", "submitted"],
|
|
107393
|
+
description: "Status override for this line."
|
|
107394
|
+
}
|
|
107395
|
+
},
|
|
107396
|
+
required: ["date", "hours"]
|
|
107397
|
+
}
|
|
107398
|
+
}
|
|
107399
|
+
},
|
|
107400
|
+
required: ["entries"]
|
|
107401
|
+
}
|
|
107402
|
+
},
|
|
107284
107403
|
{
|
|
107285
107404
|
name: "get-trips",
|
|
107286
107405
|
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.",
|
|
@@ -107782,7 +107901,8 @@ function parseDueDate(dueDate) {
|
|
|
107782
107901
|
const dateOnly = dueDate.split("T")[0];
|
|
107783
107902
|
return {
|
|
107784
107903
|
startTime: `${dateOnly}T00:00:00.000Z`,
|
|
107785
|
-
endTime: `${dateOnly}T23:59:59.999Z
|
|
107904
|
+
endTime: `${dateOnly}T23:59:59.999Z`,
|
|
107905
|
+
allDay: true
|
|
107786
107906
|
};
|
|
107787
107907
|
}
|
|
107788
107908
|
function resolveEventTimes(input) {
|
|
@@ -108914,6 +109034,16 @@ async function handleUpdateCustomerAgreement(input) {
|
|
|
108914
109034
|
|
|
108915
109035
|
${formatAgreement(row)}`);
|
|
108916
109036
|
}
|
|
109037
|
+
function computeDocumentContentHash(params) {
|
|
109038
|
+
const payload = JSON.stringify({
|
|
109039
|
+
blocks: params.blocks ?? [],
|
|
109040
|
+
branding: params.branding ?? {},
|
|
109041
|
+
locale: params.locale,
|
|
109042
|
+
pageSize: params.pageSize,
|
|
109043
|
+
title: params.title
|
|
109044
|
+
});
|
|
109045
|
+
return createHash("sha256").update(payload, "utf8").digest("hex");
|
|
109046
|
+
}
|
|
108917
109047
|
|
|
108918
109048
|
// ../document/src/humanizer/rules.ts
|
|
108919
109049
|
var REPLACEMENTS = [
|
|
@@ -109619,6 +109749,57 @@ function ensureTipTapFormat(text3) {
|
|
|
109619
109749
|
return JSON.stringify(plainTextToTipTap(text3));
|
|
109620
109750
|
}
|
|
109621
109751
|
|
|
109752
|
+
// src/tools/document-links.ts
|
|
109753
|
+
function derivePdfStatus(doc) {
|
|
109754
|
+
const currentHash = computeDocumentContentHash({
|
|
109755
|
+
blocks: doc.blocks,
|
|
109756
|
+
branding: doc.branding,
|
|
109757
|
+
pageSize: doc.pageSize,
|
|
109758
|
+
locale: doc.locale,
|
|
109759
|
+
title: doc.title
|
|
109760
|
+
});
|
|
109761
|
+
if (doc.filePath) {
|
|
109762
|
+
if (doc.compiledHash && doc.compiledHash === currentHash) {
|
|
109763
|
+
return { status: "ready", currentHash };
|
|
109764
|
+
}
|
|
109765
|
+
return { status: "stale", currentHash };
|
|
109766
|
+
}
|
|
109767
|
+
if (doc.compiledHash) return { status: "pending", currentHash };
|
|
109768
|
+
return { status: "unavailable", currentHash };
|
|
109769
|
+
}
|
|
109770
|
+
function getDashboardBaseUrl() {
|
|
109771
|
+
return (process.env.DASHBOARD_URL || "https://app.refront.nl").replace(
|
|
109772
|
+
/\/+$/,
|
|
109773
|
+
""
|
|
109774
|
+
);
|
|
109775
|
+
}
|
|
109776
|
+
function buildDashboardUrl(documentId) {
|
|
109777
|
+
return `${getDashboardBaseUrl()}/quotations/documents?editDocument=true&documentId=${encodeURIComponent(
|
|
109778
|
+
documentId
|
|
109779
|
+
)}`;
|
|
109780
|
+
}
|
|
109781
|
+
function buildPublicShareUrl(token) {
|
|
109782
|
+
return `${getDashboardBaseUrl()}/p/${encodeURIComponent(token)}`;
|
|
109783
|
+
}
|
|
109784
|
+
function isPubliclyShared(doc) {
|
|
109785
|
+
return !!doc.token && doc.shareVisibility === "public";
|
|
109786
|
+
}
|
|
109787
|
+
function formatDocumentLinksSummary(doc) {
|
|
109788
|
+
const { status: pdfStatus } = derivePdfStatus(doc);
|
|
109789
|
+
const lines = [
|
|
109790
|
+
"\u{1F517} Links:",
|
|
109791
|
+
`- Dashboard: ${buildDashboardUrl(doc.id)}`,
|
|
109792
|
+
`- PDF status: ${pdfStatus}`
|
|
109793
|
+
];
|
|
109794
|
+
if (isPubliclyShared(doc) && doc.token) {
|
|
109795
|
+
lines.push(`- Public share: ${buildPublicShareUrl(doc.token)}`);
|
|
109796
|
+
}
|
|
109797
|
+
lines.push(
|
|
109798
|
+
"- Use get-document-link (linkType pdf_signed for a temporary PDF download URL, public_share to publish a public link)."
|
|
109799
|
+
);
|
|
109800
|
+
return lines.join("\n");
|
|
109801
|
+
}
|
|
109802
|
+
|
|
109622
109803
|
// src/tools/documents.ts
|
|
109623
109804
|
var DOCUMENT_TYPES = [
|
|
109624
109805
|
"proposal",
|
|
@@ -109630,6 +109811,53 @@ var DOCUMENT_TYPES = [
|
|
|
109630
109811
|
"other"
|
|
109631
109812
|
];
|
|
109632
109813
|
var DOCUMENT_STATUSES = ["draft", "published", "archived"];
|
|
109814
|
+
var DOCUMENT_LINK_TYPES = ["dashboard", "pdf", "public_share"];
|
|
109815
|
+
var DASHBOARD_BASE_URL = process.env.DASHBOARD_URL || "https://app.refront.nl";
|
|
109816
|
+
function documentDashboardUrl(id) {
|
|
109817
|
+
return `${DASHBOARD_BASE_URL}/quotations/documents?documentId=${id}&editDocument=true`;
|
|
109818
|
+
}
|
|
109819
|
+
function documentPdfUrl(opts) {
|
|
109820
|
+
return opts.token ? `${DASHBOARD_BASE_URL}/api/download/document?documentToken=${encodeURIComponent(opts.token)}` : `${DASHBOARD_BASE_URL}/api/download/document?id=${opts.id}`;
|
|
109821
|
+
}
|
|
109822
|
+
function documentPublicShareUrl(token) {
|
|
109823
|
+
return `${DASHBOARD_BASE_URL}/p/${encodeURIComponent(token)}`;
|
|
109824
|
+
}
|
|
109825
|
+
function computePdfStatus(doc) {
|
|
109826
|
+
const currentHash = computeDocumentContentHash({
|
|
109827
|
+
blocks: doc.blocks,
|
|
109828
|
+
branding: doc.branding,
|
|
109829
|
+
pageSize: doc.pageSize,
|
|
109830
|
+
locale: doc.locale,
|
|
109831
|
+
title: doc.title
|
|
109832
|
+
});
|
|
109833
|
+
const fresh = doc.compiledHash === currentHash;
|
|
109834
|
+
if (doc.filePath) return fresh ? "ready" : "stale";
|
|
109835
|
+
return fresh ? "pending" : "none";
|
|
109836
|
+
}
|
|
109837
|
+
var PDF_STATUS_LABEL = {
|
|
109838
|
+
ready: "ready",
|
|
109839
|
+
stale: "stale (recompiles on open/download)",
|
|
109840
|
+
pending: "compiling\u2026",
|
|
109841
|
+
none: "not compiled yet"
|
|
109842
|
+
};
|
|
109843
|
+
function buildLinksSection(doc, opts) {
|
|
109844
|
+
let status = computePdfStatus(doc);
|
|
109845
|
+
const lines = [
|
|
109846
|
+
"Links:",
|
|
109847
|
+
`- Dashboard (internal, login required): ${documentDashboardUrl(doc.id)}`,
|
|
109848
|
+
`- PDF: ${PDF_STATUS_LABEL[status]}${status === "ready" ? ` \u2014 ${documentPdfUrl({ id: doc.id, token: doc.token })}` : ""}`
|
|
109849
|
+
];
|
|
109850
|
+
if (doc.token && doc.shareVisibility === "public") {
|
|
109851
|
+
lines.push(
|
|
109852
|
+
`- Public share (no login, safe to send to a customer): ${documentPublicShareUrl(doc.token)}`
|
|
109853
|
+
);
|
|
109854
|
+
} else {
|
|
109855
|
+
lines.push(
|
|
109856
|
+
"- Public share: not shared \u2014 call get-document-link with linkType=public_share to publish a customer link."
|
|
109857
|
+
);
|
|
109858
|
+
}
|
|
109859
|
+
return lines.join("\n");
|
|
109860
|
+
}
|
|
109633
109861
|
var blockIdCounter = 0;
|
|
109634
109862
|
function generateBlockId() {
|
|
109635
109863
|
blockIdCounter++;
|
|
@@ -109714,7 +109942,10 @@ var TRIGGER_API_URL = process.env.TRIGGER_API_URL || "https://trigger-dev.mgsoft
|
|
|
109714
109942
|
async function maybeTriggerPdfCompile(documentId, teamId) {
|
|
109715
109943
|
const secret = process.env.TRIGGER_SECRET_KEY;
|
|
109716
109944
|
if (!secret) {
|
|
109717
|
-
return
|
|
109945
|
+
return {
|
|
109946
|
+
triggered: false,
|
|
109947
|
+
message: "PDF: niet gecompileerd (TRIGGER_SECRET_KEY ontbreekt); de dashboard-editor compileert bij openen/delen."
|
|
109948
|
+
};
|
|
109718
109949
|
}
|
|
109719
109950
|
try {
|
|
109720
109951
|
const response = await fetch(
|
|
@@ -109730,12 +109961,21 @@ async function maybeTriggerPdfCompile(documentId, teamId) {
|
|
|
109730
109961
|
);
|
|
109731
109962
|
if (!response.ok) {
|
|
109732
109963
|
const body = await response.text();
|
|
109733
|
-
return
|
|
109964
|
+
return {
|
|
109965
|
+
triggered: false,
|
|
109966
|
+
message: `PDF: compile-trigger mislukt (HTTP ${response.status}): ${body.slice(0, 200)}`
|
|
109967
|
+
};
|
|
109734
109968
|
}
|
|
109735
109969
|
const json5 = await response.json();
|
|
109736
|
-
return
|
|
109970
|
+
return {
|
|
109971
|
+
triggered: true,
|
|
109972
|
+
message: `PDF: compile-document-pdf getriggerd (run ${json5.id ?? "?"}).`
|
|
109973
|
+
};
|
|
109737
109974
|
} catch (error49) {
|
|
109738
|
-
return
|
|
109975
|
+
return {
|
|
109976
|
+
triggered: false,
|
|
109977
|
+
message: `PDF: compile-trigger mislukt: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
109978
|
+
};
|
|
109739
109979
|
}
|
|
109740
109980
|
}
|
|
109741
109981
|
async function applyBlockOps(existing, ops, humanize) {
|
|
@@ -109920,6 +110160,8 @@ Type: ${doc.type} | Status: ${doc.status} | Locale: ${doc.locale}
|
|
|
109920
110160
|
Team: ${doc.teamId}${doc.customerId ? ` | Customer: ${doc.customerId}` : ""}${doc.invoiceId ? ` | Invoice: ${doc.invoiceId}` : ""}
|
|
109921
110161
|
Blocks (${blocks.length}): ${blockTypeSummary(blocks) || "none"}
|
|
109922
110162
|
|
|
110163
|
+
${formatDocumentLinksSummary(doc)}
|
|
110164
|
+
|
|
109923
110165
|
Blocks JSON:
|
|
109924
110166
|
\`\`\`json
|
|
109925
110167
|
${JSON.stringify(blocks, null, 2)}
|
|
@@ -109928,6 +110170,104 @@ ${JSON.stringify(blocks, null, 2)}
|
|
|
109928
110170
|
]
|
|
109929
110171
|
};
|
|
109930
110172
|
}
|
|
110173
|
+
async function handleGetDocumentLink(input) {
|
|
110174
|
+
if (!input.id) {
|
|
110175
|
+
return { content: [{ type: "text", text: "Error: `id` is required." }] };
|
|
110176
|
+
}
|
|
110177
|
+
const linkType = input.linkType ?? "dashboard";
|
|
110178
|
+
if (!DOCUMENT_LINK_TYPES.includes(linkType)) {
|
|
110179
|
+
return {
|
|
110180
|
+
content: [
|
|
110181
|
+
{
|
|
110182
|
+
type: "text",
|
|
110183
|
+
text: `Error: invalid linkType "${linkType}". Allowed: ${DOCUMENT_LINK_TYPES.join(", ")}.`
|
|
110184
|
+
}
|
|
110185
|
+
]
|
|
110186
|
+
};
|
|
110187
|
+
}
|
|
110188
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
110189
|
+
if (!scope.ok) return scope.response;
|
|
110190
|
+
const doc = await findAccessibleDocument(input.id, scope.teamIds);
|
|
110191
|
+
if (!doc) {
|
|
110192
|
+
return {
|
|
110193
|
+
content: [
|
|
110194
|
+
{
|
|
110195
|
+
type: "text",
|
|
110196
|
+
text: `Document ${input.id} not found or you don't have access to it.`
|
|
110197
|
+
}
|
|
110198
|
+
]
|
|
110199
|
+
};
|
|
110200
|
+
}
|
|
110201
|
+
const header = `**${doc.title}**
|
|
110202
|
+
ID: ${doc.id}
|
|
110203
|
+
Status: ${doc.status} | Visibility: ${doc.shareVisibility}
|
|
110204
|
+
|
|
110205
|
+
`;
|
|
110206
|
+
const freshHash = computeDocumentContentHash({
|
|
110207
|
+
blocks: doc.blocks,
|
|
110208
|
+
branding: doc.branding,
|
|
110209
|
+
pageSize: doc.pageSize,
|
|
110210
|
+
locale: doc.locale,
|
|
110211
|
+
title: doc.title
|
|
110212
|
+
});
|
|
110213
|
+
if (linkType === "public_share") {
|
|
110214
|
+
const alreadyPublic = Boolean(doc.token) && doc.shareVisibility === "public";
|
|
110215
|
+
const token = doc.token ?? randomBytes(32).toString("base64url");
|
|
110216
|
+
const needsCompile = computePdfStatus(doc) !== "ready";
|
|
110217
|
+
const patch = {
|
|
110218
|
+
token,
|
|
110219
|
+
shareVisibility: "public",
|
|
110220
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110221
|
+
};
|
|
110222
|
+
if (needsCompile) {
|
|
110223
|
+
patch.filePath = null;
|
|
110224
|
+
patch.fileSize = null;
|
|
110225
|
+
patch.compiledHash = freshHash;
|
|
110226
|
+
}
|
|
110227
|
+
await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
|
|
110228
|
+
const pdfLine = needsCompile ? `
|
|
110229
|
+
${await maybeTriggerPdfCompile(doc.id, doc.teamId)}` : "";
|
|
110230
|
+
return {
|
|
110231
|
+
content: [
|
|
110232
|
+
{
|
|
110233
|
+
type: "text",
|
|
110234
|
+
text: header + `${alreadyPublic ? "Public share already enabled." : "\u2705 Public share enabled (visibility \u2192 public)."}
|
|
110235
|
+
Public link (no login, safe to send to a customer): ${documentPublicShareUrl(token)}
|
|
110236
|
+
PDF download: ${documentPdfUrl({ id: doc.id, token })}
|
|
110237
|
+
PDF status: ${needsCompile ? "compiling\u2026 (ready shortly)" : "ready"}` + pdfLine
|
|
110238
|
+
}
|
|
110239
|
+
]
|
|
110240
|
+
};
|
|
110241
|
+
}
|
|
110242
|
+
if (linkType === "pdf") {
|
|
110243
|
+
let status = computePdfStatus(doc);
|
|
110244
|
+
let pdfLine = "";
|
|
110245
|
+
if (status === "none" || status === "stale") {
|
|
110246
|
+
await db.update(schema_exports.documents).set({
|
|
110247
|
+
filePath: null,
|
|
110248
|
+
fileSize: null,
|
|
110249
|
+
compiledHash: freshHash,
|
|
110250
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
110251
|
+
}).where(eq(schema_exports.documents.id, doc.id));
|
|
110252
|
+
pdfLine = `
|
|
110253
|
+
${await maybeTriggerPdfCompile(doc.id, doc.teamId)}`;
|
|
110254
|
+
status = "pending";
|
|
110255
|
+
}
|
|
110256
|
+
return {
|
|
110257
|
+
content: [
|
|
110258
|
+
{
|
|
110259
|
+
type: "text",
|
|
110260
|
+
text: header + `PDF status: ${PDF_STATUS_LABEL[status]}
|
|
110261
|
+
PDF download: ${documentPdfUrl({ id: doc.id, token: doc.token })}
|
|
110262
|
+
The endpoint streams the PDF via a short-lived signed storage URL and recompiles on demand if needed.` + pdfLine
|
|
110263
|
+
}
|
|
110264
|
+
]
|
|
110265
|
+
};
|
|
110266
|
+
}
|
|
110267
|
+
return {
|
|
110268
|
+
content: [{ type: "text", text: header + buildLinksSection(doc) }]
|
|
110269
|
+
};
|
|
110270
|
+
}
|
|
109931
110271
|
async function handleCreateDocument(input) {
|
|
109932
110272
|
const {
|
|
109933
110273
|
title,
|
|
@@ -110007,7 +110347,19 @@ async function handleCreateDocument(input) {
|
|
|
110007
110347
|
content: [{ type: "text", text: "Error: document insert failed." }]
|
|
110008
110348
|
};
|
|
110009
110349
|
}
|
|
110010
|
-
const
|
|
110350
|
+
const pdfResult = compilePdf ? await maybeTriggerPdfCompile(created.id, resolved.teamId) : null;
|
|
110351
|
+
const linksSummary = formatDocumentLinksSummary({
|
|
110352
|
+
id: created.id,
|
|
110353
|
+
title: title.trim(),
|
|
110354
|
+
blocks: humanized.blocks,
|
|
110355
|
+
branding: {},
|
|
110356
|
+
pageSize: "a4",
|
|
110357
|
+
locale,
|
|
110358
|
+
filePath: null,
|
|
110359
|
+
compiledHash: null,
|
|
110360
|
+
token: null,
|
|
110361
|
+
shareVisibility: "private"
|
|
110362
|
+
});
|
|
110011
110363
|
return {
|
|
110012
110364
|
content: [
|
|
110013
110365
|
{
|
|
@@ -110021,8 +110373,10 @@ Team: ${resolved.teamId}${customerId ? ` | Customer: ${customerId}` : ""}
|
|
|
110021
110373
|
${invoiceLine ? `${invoiceLine}
|
|
110022
110374
|
` : ""}Blocks (${humanized.blocks.length}): ${blockTypeSummary(humanized.blocks)}
|
|
110023
110375
|
|
|
110024
|
-
${humanized.summary}${
|
|
110025
|
-
${
|
|
110376
|
+
${humanized.summary}${pdfResult ? `
|
|
110377
|
+
${pdfResult.message}` : ""}
|
|
110378
|
+
|
|
110379
|
+
${linksSummary}`
|
|
110026
110380
|
}
|
|
110027
110381
|
]
|
|
110028
110382
|
};
|
|
@@ -110184,7 +110538,20 @@ async function handleUpdateDocument(input) {
|
|
|
110184
110538
|
};
|
|
110185
110539
|
}
|
|
110186
110540
|
await db.update(schema_exports.documents).set(patch).where(eq(schema_exports.documents.id, doc.id));
|
|
110187
|
-
const
|
|
110541
|
+
const pdfResult = compilePdf ? await maybeTriggerPdfCompile(doc.id, doc.teamId) : null;
|
|
110542
|
+
const linksSummary = formatDocumentLinksSummary({
|
|
110543
|
+
id: doc.id,
|
|
110544
|
+
title: patch.title ?? doc.title,
|
|
110545
|
+
status: patch.status ?? doc.status,
|
|
110546
|
+
blocks: "blocks" in patch ? patch.blocks : doc.blocks,
|
|
110547
|
+
branding: doc.branding,
|
|
110548
|
+
pageSize: doc.pageSize,
|
|
110549
|
+
locale: doc.locale,
|
|
110550
|
+
filePath: doc.filePath,
|
|
110551
|
+
compiledHash: doc.compiledHash,
|
|
110552
|
+
token: doc.token,
|
|
110553
|
+
shareVisibility: doc.shareVisibility
|
|
110554
|
+
});
|
|
110188
110555
|
return {
|
|
110189
110556
|
content: [
|
|
110190
110557
|
{
|
|
@@ -110194,8 +110561,10 @@ async function handleUpdateDocument(input) {
|
|
|
110194
110561
|
ID: ${doc.id}
|
|
110195
110562
|
${changeLines.map((l4) => `- ${l4}`).join("\n")}${humanizeSummary ? `
|
|
110196
110563
|
|
|
110197
|
-
${humanizeSummary}` : ""}${
|
|
110198
|
-
${
|
|
110564
|
+
${humanizeSummary}` : ""}${pdfResult ? `
|
|
110565
|
+
${pdfResult.message}` : ""}
|
|
110566
|
+
|
|
110567
|
+
${linksSummary}`
|
|
110199
110568
|
}
|
|
110200
110569
|
]
|
|
110201
110570
|
};
|
|
@@ -114446,6 +114815,278 @@ async function handleGetTimeEntries(input) {
|
|
|
114446
114815
|
entriesTruncated: entryCount > entries.length
|
|
114447
114816
|
});
|
|
114448
114817
|
}
|
|
114818
|
+
var MAX_CREATE_ENTRIES = 100;
|
|
114819
|
+
var DEFAULT_START_HOUR = "09:00:00";
|
|
114820
|
+
function mapCreateStatus(status) {
|
|
114821
|
+
if (status == null) return "draft";
|
|
114822
|
+
if (status === "draft") return "draft";
|
|
114823
|
+
if (status === "confirmed" || status === "submitted") return "confirmed";
|
|
114824
|
+
return null;
|
|
114825
|
+
}
|
|
114826
|
+
function isValidIsoDate(value) {
|
|
114827
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
114828
|
+
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
114829
|
+
if (Number.isNaN(parsed.getTime())) return false;
|
|
114830
|
+
return parsed.toISOString().slice(0, 10) === value;
|
|
114831
|
+
}
|
|
114832
|
+
async function handleCreateTimeEntries(input) {
|
|
114833
|
+
const ctx = getAuthContext();
|
|
114834
|
+
const te = schema_exports.timesheetEvents;
|
|
114835
|
+
if (!Array.isArray(input.entries) || input.entries.length === 0) {
|
|
114836
|
+
return textResponse3(
|
|
114837
|
+
"Error: `entries` must be a non-empty array of { date, hours, description? } objects."
|
|
114838
|
+
);
|
|
114839
|
+
}
|
|
114840
|
+
if (input.entries.length > MAX_CREATE_ENTRIES) {
|
|
114841
|
+
return textResponse3(
|
|
114842
|
+
`Error: too many entries (${input.entries.length}). Max ${MAX_CREATE_ENTRIES} per call.`
|
|
114843
|
+
);
|
|
114844
|
+
}
|
|
114845
|
+
const sharedStatus = mapCreateStatus(input.status);
|
|
114846
|
+
if (sharedStatus === null) {
|
|
114847
|
+
return textResponse3(
|
|
114848
|
+
`Error: invalid status "${input.status}". Allowed: draft, confirmed (submitted is treated as confirmed).`
|
|
114849
|
+
);
|
|
114850
|
+
}
|
|
114851
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
114852
|
+
if (!scope.ok) return scope.response;
|
|
114853
|
+
if (scope.teamIds.length === 0) {
|
|
114854
|
+
return textResponse3("No accessible teams found.");
|
|
114855
|
+
}
|
|
114856
|
+
const projectIds = /* @__PURE__ */ new Set();
|
|
114857
|
+
const ticketIds = /* @__PURE__ */ new Set();
|
|
114858
|
+
if (input.projectId) projectIds.add(input.projectId);
|
|
114859
|
+
if (input.ticketId) ticketIds.add(input.ticketId);
|
|
114860
|
+
for (const entry of input.entries) {
|
|
114861
|
+
if (entry?.projectId) projectIds.add(entry.projectId);
|
|
114862
|
+
if (entry?.ticketId) ticketIds.add(entry.ticketId);
|
|
114863
|
+
}
|
|
114864
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
114865
|
+
for (const projectId of projectIds) {
|
|
114866
|
+
if (!scope.projectIds.includes(projectId)) {
|
|
114867
|
+
return textResponse3(
|
|
114868
|
+
`Project not found or no access: ${projectId}. Call get-projects first.`
|
|
114869
|
+
);
|
|
114870
|
+
}
|
|
114871
|
+
const [project] = await db.select({
|
|
114872
|
+
id: schema_exports.projects.id,
|
|
114873
|
+
name: schema_exports.projects.name,
|
|
114874
|
+
teamId: schema_exports.projects.teamId
|
|
114875
|
+
}).from(schema_exports.projects).where(eq(schema_exports.projects.id, projectId)).limit(1);
|
|
114876
|
+
if (!project) return textResponse3(`Project not found: ${projectId}.`);
|
|
114877
|
+
projectMap.set(projectId, project);
|
|
114878
|
+
}
|
|
114879
|
+
const ticketMap = /* @__PURE__ */ new Map();
|
|
114880
|
+
for (const ticketId of ticketIds) {
|
|
114881
|
+
const [ticket] = await db.select({
|
|
114882
|
+
id: schema_exports.tickets.id,
|
|
114883
|
+
title: schema_exports.tickets.title,
|
|
114884
|
+
teamId: schema_exports.tickets.teamId,
|
|
114885
|
+
projectId: schema_exports.tickets.projectId,
|
|
114886
|
+
customerId: schema_exports.tickets.customerId
|
|
114887
|
+
}).from(schema_exports.tickets).where(eq(schema_exports.tickets.id, ticketId)).limit(1);
|
|
114888
|
+
if (!ticket) {
|
|
114889
|
+
return textResponse3(`Ticket not found: ${ticketId}. Call get-tickets first.`);
|
|
114890
|
+
}
|
|
114891
|
+
let hasAccess = scope.teamIds.includes(ticket.teamId);
|
|
114892
|
+
if (!hasAccess && ticket.projectId)
|
|
114893
|
+
hasAccess = scope.projectIds.includes(ticket.projectId);
|
|
114894
|
+
if (!hasAccess && ticket.customerId)
|
|
114895
|
+
hasAccess = scope.customerIds.includes(ticket.customerId);
|
|
114896
|
+
if (!hasAccess) {
|
|
114897
|
+
return textResponse3(`No access to ticket: ${ticketId}. Call get-tickets first.`);
|
|
114898
|
+
}
|
|
114899
|
+
ticketMap.set(ticketId, ticket);
|
|
114900
|
+
}
|
|
114901
|
+
const needsFallbackTeam = input.entries.some((entry) => {
|
|
114902
|
+
const ticketId = entry?.ticketId ?? input.ticketId;
|
|
114903
|
+
const projectId = entry?.projectId ?? input.projectId;
|
|
114904
|
+
const ticket = ticketId ? ticketMap.get(ticketId) : null;
|
|
114905
|
+
const project = projectId ? projectMap.get(projectId) : null;
|
|
114906
|
+
return !(ticket?.teamId ?? project?.teamId);
|
|
114907
|
+
});
|
|
114908
|
+
let fallbackTeamId = null;
|
|
114909
|
+
if (needsFallbackTeam) {
|
|
114910
|
+
const resolved = await resolveTeamId(input.teamId);
|
|
114911
|
+
if (!resolved.ok) return resolved.response;
|
|
114912
|
+
fallbackTeamId = resolved.teamId;
|
|
114913
|
+
}
|
|
114914
|
+
const rawUserId = input.userId?.trim();
|
|
114915
|
+
const targetUserId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
|
|
114916
|
+
const membershipCache = /* @__PURE__ */ new Map();
|
|
114917
|
+
const isTargetMember = async (teamId) => {
|
|
114918
|
+
if (targetUserId === ctx.userId) return true;
|
|
114919
|
+
const cached3 = membershipCache.get(teamId);
|
|
114920
|
+
if (cached3 !== void 0) return cached3;
|
|
114921
|
+
const member2 = await isUserTeamMember(targetUserId, teamId);
|
|
114922
|
+
membershipCache.set(teamId, member2);
|
|
114923
|
+
return member2;
|
|
114924
|
+
};
|
|
114925
|
+
const created = [];
|
|
114926
|
+
const skipped = [];
|
|
114927
|
+
const errors = [];
|
|
114928
|
+
for (let index2 = 0; index2 < input.entries.length; index2++) {
|
|
114929
|
+
const entry = input.entries[index2];
|
|
114930
|
+
const date10 = typeof entry?.date === "string" ? entry.date.trim() : "";
|
|
114931
|
+
try {
|
|
114932
|
+
if (!entry || typeof entry !== "object") {
|
|
114933
|
+
errors.push({ index: index2, date: null, message: "Entry must be an object." });
|
|
114934
|
+
continue;
|
|
114935
|
+
}
|
|
114936
|
+
if (!isValidIsoDate(date10)) {
|
|
114937
|
+
errors.push({
|
|
114938
|
+
index: index2,
|
|
114939
|
+
date: date10 || null,
|
|
114940
|
+
message: "Invalid or missing `date`; expected YYYY-MM-DD."
|
|
114941
|
+
});
|
|
114942
|
+
continue;
|
|
114943
|
+
}
|
|
114944
|
+
const hours = toNumber(entry.hours);
|
|
114945
|
+
if (!(hours > 0)) {
|
|
114946
|
+
errors.push({
|
|
114947
|
+
index: index2,
|
|
114948
|
+
date: date10,
|
|
114949
|
+
message: "`hours` must be a number greater than 0."
|
|
114950
|
+
});
|
|
114951
|
+
continue;
|
|
114952
|
+
}
|
|
114953
|
+
if (hours > 24) {
|
|
114954
|
+
errors.push({
|
|
114955
|
+
index: index2,
|
|
114956
|
+
date: date10,
|
|
114957
|
+
message: "`hours` must not exceed 24 for a single day."
|
|
114958
|
+
});
|
|
114959
|
+
continue;
|
|
114960
|
+
}
|
|
114961
|
+
const description = (entry.description ?? input.description ?? "").trim();
|
|
114962
|
+
if (!description) {
|
|
114963
|
+
errors.push({
|
|
114964
|
+
index: index2,
|
|
114965
|
+
date: date10,
|
|
114966
|
+
message: "A description is required (set entry.description or a shared description)."
|
|
114967
|
+
});
|
|
114968
|
+
continue;
|
|
114969
|
+
}
|
|
114970
|
+
const status = entry.status === void 0 ? sharedStatus : mapCreateStatus(entry.status);
|
|
114971
|
+
if (status === null) {
|
|
114972
|
+
errors.push({
|
|
114973
|
+
index: index2,
|
|
114974
|
+
date: date10,
|
|
114975
|
+
message: `Invalid status "${entry.status}". Allowed: draft, confirmed.`
|
|
114976
|
+
});
|
|
114977
|
+
continue;
|
|
114978
|
+
}
|
|
114979
|
+
const billable = entry.billable ?? input.billable ?? true;
|
|
114980
|
+
const billingStatus = billable ? "to_bill" : "unbillable";
|
|
114981
|
+
const ticketId = entry.ticketId ?? input.ticketId ?? null;
|
|
114982
|
+
const projectId = entry.projectId ?? input.projectId ?? null;
|
|
114983
|
+
const ticket = ticketId ? ticketMap.get(ticketId) : null;
|
|
114984
|
+
const project = projectId ? projectMap.get(projectId) : null;
|
|
114985
|
+
const insertTeamId = ticket?.teamId ?? project?.teamId ?? fallbackTeamId;
|
|
114986
|
+
if (!insertTeamId) {
|
|
114987
|
+
errors.push({
|
|
114988
|
+
index: index2,
|
|
114989
|
+
date: date10,
|
|
114990
|
+
message: "Could not resolve a team for this entry; pass teamId, projectId or ticketId."
|
|
114991
|
+
});
|
|
114992
|
+
continue;
|
|
114993
|
+
}
|
|
114994
|
+
if (!await isTargetMember(insertTeamId)) {
|
|
114995
|
+
errors.push({
|
|
114996
|
+
index: index2,
|
|
114997
|
+
date: date10,
|
|
114998
|
+
message: `User ${targetUserId} is not a member of team ${insertTeamId}.`
|
|
114999
|
+
});
|
|
115000
|
+
continue;
|
|
115001
|
+
}
|
|
115002
|
+
const effectiveProjectId = project?.id ?? ticket?.projectId ?? null;
|
|
115003
|
+
if (!input.allowDuplicate) {
|
|
115004
|
+
const dupConditions = [
|
|
115005
|
+
eq(te.teamId, insertTeamId),
|
|
115006
|
+
eq(te.userId, targetUserId),
|
|
115007
|
+
eq(te.isDeleted, false),
|
|
115008
|
+
sql`(${te.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date = ${date10}::date`,
|
|
115009
|
+
sql`lower(${te.title}) = lower(${description})`
|
|
115010
|
+
];
|
|
115011
|
+
dupConditions.push(
|
|
115012
|
+
effectiveProjectId ? eq(te.projectId, effectiveProjectId) : sql`${te.projectId} IS NULL`
|
|
115013
|
+
);
|
|
115014
|
+
const [dup] = await db.select({ id: te.id }).from(te).where(and(...dupConditions)).limit(1);
|
|
115015
|
+
if (dup) {
|
|
115016
|
+
skipped.push({
|
|
115017
|
+
index: index2,
|
|
115018
|
+
date: date10,
|
|
115019
|
+
hours: Math.round(hours * 100) / 100,
|
|
115020
|
+
description,
|
|
115021
|
+
reason: "duplicate",
|
|
115022
|
+
existingId: dup.id
|
|
115023
|
+
});
|
|
115024
|
+
continue;
|
|
115025
|
+
}
|
|
115026
|
+
}
|
|
115027
|
+
const localStart = `${date10} ${DEFAULT_START_HOUR}`;
|
|
115028
|
+
const startExpr = sql`(${localStart}::timestamp AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})`;
|
|
115029
|
+
const endExpr = sql`(${startExpr} + ${`${hours} hours`}::interval)`;
|
|
115030
|
+
const [row] = await db.insert(te).values({
|
|
115031
|
+
teamId: insertTeamId,
|
|
115032
|
+
userId: targetUserId,
|
|
115033
|
+
title: description,
|
|
115034
|
+
description,
|
|
115035
|
+
type: "work",
|
|
115036
|
+
status,
|
|
115037
|
+
startTime: startExpr,
|
|
115038
|
+
endTime: endExpr,
|
|
115039
|
+
allDay: false,
|
|
115040
|
+
isTracked: true,
|
|
115041
|
+
trackedDuration: Math.round(hours * 3600),
|
|
115042
|
+
projectId: effectiveProjectId,
|
|
115043
|
+
customerId: ticket?.customerId ?? null,
|
|
115044
|
+
billingStatus
|
|
115045
|
+
}).returning({ id: te.id });
|
|
115046
|
+
if (!row) {
|
|
115047
|
+
errors.push({ index: index2, date: date10, message: "Insert failed." });
|
|
115048
|
+
continue;
|
|
115049
|
+
}
|
|
115050
|
+
if (ticketId) {
|
|
115051
|
+
await db.insert(schema_exports.timesheetEventTickets).values({ timesheetEventId: row.id, ticketId }).onConflictDoNothing();
|
|
115052
|
+
}
|
|
115053
|
+
created.push({
|
|
115054
|
+
id: row.id,
|
|
115055
|
+
date: date10,
|
|
115056
|
+
hours: Math.round(hours * 100) / 100,
|
|
115057
|
+
description,
|
|
115058
|
+
status,
|
|
115059
|
+
billable,
|
|
115060
|
+
project: effectiveProjectId ? {
|
|
115061
|
+
id: effectiveProjectId,
|
|
115062
|
+
name: projectMap.get(effectiveProjectId)?.name ?? null
|
|
115063
|
+
} : null,
|
|
115064
|
+
ticket: ticket ? { id: ticket.id, title: ticket.title } : null
|
|
115065
|
+
});
|
|
115066
|
+
} catch (error49) {
|
|
115067
|
+
errors.push({
|
|
115068
|
+
index: index2,
|
|
115069
|
+
date: date10 || null,
|
|
115070
|
+
message: error49 instanceof Error ? error49.message : String(error49)
|
|
115071
|
+
});
|
|
115072
|
+
}
|
|
115073
|
+
}
|
|
115074
|
+
const totalHours = Math.round(created.reduce((sum, c6) => sum + c6.hours, 0) * 100) / 100;
|
|
115075
|
+
return jsonResponse({
|
|
115076
|
+
teamScope: scope.teamIds,
|
|
115077
|
+
bookedForUserId: targetUserId,
|
|
115078
|
+
timezone: TIMEZONE,
|
|
115079
|
+
created,
|
|
115080
|
+
skipped,
|
|
115081
|
+
errors,
|
|
115082
|
+
totals: {
|
|
115083
|
+
createdCount: created.length,
|
|
115084
|
+
skippedCount: skipped.length,
|
|
115085
|
+
errorCount: errors.length,
|
|
115086
|
+
totalHours
|
|
115087
|
+
}
|
|
115088
|
+
});
|
|
115089
|
+
}
|
|
114449
115090
|
|
|
114450
115091
|
// ../invoice/src/utils/included-items.ts
|
|
114451
115092
|
function parseIncludedItems(value) {
|
|
@@ -117280,9 +117921,9 @@ function templateDefaultsFromStored(template, currency) {
|
|
|
117280
117921
|
}
|
|
117281
117922
|
|
|
117282
117923
|
// src/tools/quotes.ts
|
|
117283
|
-
var
|
|
117924
|
+
var DASHBOARD_BASE_URL2 = "https://app.refront.nl";
|
|
117284
117925
|
function quoteDashboardUrl(id) {
|
|
117285
|
-
return `${
|
|
117926
|
+
return `${DASHBOARD_BASE_URL2}/quotations?quotationId=${id}"ationDetails=true`;
|
|
117286
117927
|
}
|
|
117287
117928
|
var QUOTE_STATUSES = [
|
|
117288
117929
|
"draft",
|
|
@@ -125684,7 +126325,7 @@ ${tagErrors.map((e6) => ` \u2022 ${e6}`).join("\n")}
|
|
|
125684
126325
|
}
|
|
125685
126326
|
|
|
125686
126327
|
// src/server.ts
|
|
125687
|
-
var SERVER_VERSION = "3.5.
|
|
126328
|
+
var SERVER_VERSION = "3.5.12";
|
|
125688
126329
|
function createMcpServer() {
|
|
125689
126330
|
const server = new Server(
|
|
125690
126331
|
{
|
|
@@ -125820,6 +126461,10 @@ function createMcpServer() {
|
|
|
125820
126461
|
return await handleListDocuments(asToolArgs(toolArgs));
|
|
125821
126462
|
case "get-document":
|
|
125822
126463
|
return await handleGetDocument(asToolArgs(toolArgs));
|
|
126464
|
+
case "get-document-link":
|
|
126465
|
+
return await handleGetDocumentLink(
|
|
126466
|
+
asToolArgs(toolArgs)
|
|
126467
|
+
);
|
|
125823
126468
|
case "create-document":
|
|
125824
126469
|
return await handleCreateDocument(
|
|
125825
126470
|
asToolArgs(toolArgs)
|
|
@@ -125916,6 +126561,10 @@ function createMcpServer() {
|
|
|
125916
126561
|
return await handleGetTimeEntries(
|
|
125917
126562
|
asToolArgs(toolArgs)
|
|
125918
126563
|
);
|
|
126564
|
+
case "create-time-entries":
|
|
126565
|
+
return await handleCreateTimeEntries(
|
|
126566
|
+
asToolArgs(toolArgs)
|
|
126567
|
+
);
|
|
125919
126568
|
case "get-github-file":
|
|
125920
126569
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
125921
126570
|
case "list-github-directory":
|