@mgsoftwarebv/mcp-server-bridge 3.5.9 → 3.5.11
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 +628 -202
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -64421,7 +64421,7 @@ var init_DocTypeReader = __esm({
|
|
|
64421
64421
|
});
|
|
64422
64422
|
|
|
64423
64423
|
// ../../node_modules/strnum/strnum.js
|
|
64424
|
-
function
|
|
64424
|
+
function toNumber2(str, options = {}) {
|
|
64425
64425
|
options = Object.assign({}, consider, options);
|
|
64426
64426
|
if (!str || typeof str !== "string") return str;
|
|
64427
64427
|
let trimmedStr = str.trim();
|
|
@@ -65669,7 +65669,7 @@ function parseValue(val, shouldParse, options) {
|
|
|
65669
65669
|
const newval = val.trim();
|
|
65670
65670
|
if (newval === "true") return true;
|
|
65671
65671
|
else if (newval === "false") return false;
|
|
65672
|
-
else return
|
|
65672
|
+
else return toNumber2(val, options);
|
|
65673
65673
|
} else {
|
|
65674
65674
|
if (isExist(val)) {
|
|
65675
65675
|
return val;
|
|
@@ -99324,6 +99324,10 @@ var githubEvents = pgTable(
|
|
|
99324
99324
|
{
|
|
99325
99325
|
id: uuid3().defaultRandom().primaryKey().notNull(),
|
|
99326
99326
|
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).defaultNow().notNull(),
|
|
99327
|
+
activityDate: timestamp("activity_date", {
|
|
99328
|
+
withTimezone: true,
|
|
99329
|
+
mode: "string"
|
|
99330
|
+
}).defaultNow().notNull(),
|
|
99327
99331
|
// Team and project info
|
|
99328
99332
|
teamId: uuid3("team_id").notNull(),
|
|
99329
99333
|
projectId: uuid3("project_id"),
|
|
@@ -99376,6 +99380,10 @@ var githubEvents = pgTable(
|
|
|
99376
99380
|
"btree",
|
|
99377
99381
|
table.createdAt.desc()
|
|
99378
99382
|
),
|
|
99383
|
+
index("idx_github_events_activity_date").using(
|
|
99384
|
+
"btree",
|
|
99385
|
+
table.activityDate.desc()
|
|
99386
|
+
),
|
|
99379
99387
|
index("idx_github_events_ticket_id").on(table.ticketId),
|
|
99380
99388
|
index("idx_github_events_event_type").on(table.eventType),
|
|
99381
99389
|
index("idx_github_events_team_date").using(
|
|
@@ -99383,6 +99391,11 @@ var githubEvents = pgTable(
|
|
|
99383
99391
|
table.teamId,
|
|
99384
99392
|
table.createdAt.desc()
|
|
99385
99393
|
),
|
|
99394
|
+
index("idx_github_events_team_activity_date").using(
|
|
99395
|
+
"btree",
|
|
99396
|
+
table.teamId,
|
|
99397
|
+
table.activityDate.desc()
|
|
99398
|
+
),
|
|
99386
99399
|
pgPolicy("Team members can view their github events", {
|
|
99387
99400
|
as: "permissive",
|
|
99388
99401
|
for: "select",
|
|
@@ -101247,6 +101260,8 @@ var teams = pgTable(
|
|
|
101247
101260
|
website: text(),
|
|
101248
101261
|
documentsEmailId: text("documents_email_id").default("generate_inbox(10)"),
|
|
101249
101262
|
email: text(),
|
|
101263
|
+
billingFromEmail: text("billing_from_email"),
|
|
101264
|
+
billingFromVerified: boolean3("billing_from_verified").default(false).notNull(),
|
|
101250
101265
|
inboxEmail: text("inbox_email"),
|
|
101251
101266
|
inboxForwarding: boolean3("inbox_forwarding").default(true),
|
|
101252
101267
|
baseCurrency: text("base_currency").default("EUR"),
|
|
@@ -103452,7 +103467,9 @@ var customerRecurringServices = pgTable(
|
|
|
103452
103467
|
mode: "string"
|
|
103453
103468
|
}),
|
|
103454
103469
|
// --- Lifecycle ---
|
|
103455
|
-
// "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.
|
|
103456
103473
|
status: text().$type().default("active").notNull(),
|
|
103457
103474
|
startDate: timestamp("start_date", { withTimezone: true, mode: "string" }),
|
|
103458
103475
|
// Planned end of the agreement (before cancellation/ended).
|
|
@@ -107203,6 +107220,67 @@ var TOOLS = [
|
|
|
107203
107220
|
required: ["workDescription", "estimatedHours"]
|
|
107204
107221
|
}
|
|
107205
107222
|
},
|
|
107223
|
+
{
|
|
107224
|
+
name: "get-time-entries",
|
|
107225
|
+
description: "Read and TOTAL tracked time entries (urenregistratie / tracker entries from the agenda timesheet) so you can answer questions like 'how many hours did I work this month for customer X?'. Scoped to your provider team(s). Filter by period (dateFrom/dateTo, inclusive, Europe/Amsterdam), user (defaults to the API key user; pass 'all' for the whole team), project, customer (matches the event's customer OR its project's customer), ticket, derived status and event type. Worked hours per entry = (endTime - startTime), falling back to the tracked duration. All-day markers and deadlines are excluded (those are agenda items \u2014 see get-calendar-items). Returns accurate aggregate totals over ALL matching entries (totalHours, billableHours, nonBillableHours, invoicedHours, entryCount), an optional grouped breakdown, and a page of detailed entries (entriesTruncated indicates more exist than were listed). Use get-projects / get-customers / get-tickets to resolve ids first.",
|
|
107226
|
+
inputSchema: {
|
|
107227
|
+
type: "object",
|
|
107228
|
+
properties: {
|
|
107229
|
+
teamId: teamIdProp,
|
|
107230
|
+
userId: {
|
|
107231
|
+
type: "string",
|
|
107232
|
+
description: "User whose hours to total. Defaults to the API key user ('me'). Pass a user UUID for someone else, or 'all' for every team member."
|
|
107233
|
+
},
|
|
107234
|
+
projectId: { type: "string", description: "Filter by project UUID." },
|
|
107235
|
+
customerId: {
|
|
107236
|
+
type: "string",
|
|
107237
|
+
description: "Filter by customer UUID. Matches entries whose own customer OR whose project's customer is this customer (project link is the common case)."
|
|
107238
|
+
},
|
|
107239
|
+
ticketId: {
|
|
107240
|
+
type: "string",
|
|
107241
|
+
description: "Only entries linked to this ticket (UUID)."
|
|
107242
|
+
},
|
|
107243
|
+
dateFrom: {
|
|
107244
|
+
type: "string",
|
|
107245
|
+
description: "Inclusive period start (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107246
|
+
},
|
|
107247
|
+
dateTo: {
|
|
107248
|
+
type: "string",
|
|
107249
|
+
description: "Inclusive period end (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107250
|
+
},
|
|
107251
|
+
status: {
|
|
107252
|
+
type: "string",
|
|
107253
|
+
enum: ["draft", "approved", "confirmed", "invoiced", "all"],
|
|
107254
|
+
description: "Derived status filter: draft (not confirmed, not invoiced), approved/confirmed (confirmed, not invoiced), invoiced (linked to an invoice), all (default)."
|
|
107255
|
+
},
|
|
107256
|
+
type: {
|
|
107257
|
+
type: "string",
|
|
107258
|
+
enum: [
|
|
107259
|
+
"meeting",
|
|
107260
|
+
"work",
|
|
107261
|
+
"clocked_work",
|
|
107262
|
+
"appointment",
|
|
107263
|
+
"task",
|
|
107264
|
+
"other"
|
|
107265
|
+
],
|
|
107266
|
+
description: "Filter by timesheet event type. Omit to include all worked-time types (deadlines/all-day markers are always excluded)."
|
|
107267
|
+
},
|
|
107268
|
+
groupBy: {
|
|
107269
|
+
type: "string",
|
|
107270
|
+
enum: ["none", "day", "project", "customer", "user", "ticket"],
|
|
107271
|
+
default: "none",
|
|
107272
|
+
description: "Return an aggregated breakdown by this dimension (in addition to the overall totals). For 'ticket', an entry's hours are attributed in full to each linked ticket."
|
|
107273
|
+
},
|
|
107274
|
+
pageSize: {
|
|
107275
|
+
type: "number",
|
|
107276
|
+
default: 50,
|
|
107277
|
+
maximum: 200,
|
|
107278
|
+
description: "Max detailed entries returned. Totals/groups always cover ALL matching entries regardless of this limit."
|
|
107279
|
+
}
|
|
107280
|
+
},
|
|
107281
|
+
required: []
|
|
107282
|
+
}
|
|
107283
|
+
},
|
|
107206
107284
|
{
|
|
107207
107285
|
name: "get-trips",
|
|
107208
107286
|
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.",
|
|
@@ -107414,7 +107492,7 @@ var TOOLS = [
|
|
|
107414
107492
|
customerId: { type: "string", description: "Filter by customer UUID" },
|
|
107415
107493
|
status: {
|
|
107416
107494
|
type: "string",
|
|
107417
|
-
enum: ["active", "paused", "cancelled", "ended"],
|
|
107495
|
+
enum: ["draft", "active", "paused", "cancelled", "ended"],
|
|
107418
107496
|
description: "Filter by agreement status"
|
|
107419
107497
|
}
|
|
107420
107498
|
}
|
|
@@ -107422,7 +107500,7 @@ var TOOLS = [
|
|
|
107422
107500
|
},
|
|
107423
107501
|
{
|
|
107424
107502
|
name: "create-customer-agreement",
|
|
107425
|
-
description: "Create a customer-specific product agreement
|
|
107503
|
+
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.",
|
|
107426
107504
|
inputSchema: {
|
|
107427
107505
|
type: "object",
|
|
107428
107506
|
properties: {
|
|
@@ -107458,7 +107536,7 @@ var TOOLS = [
|
|
|
107458
107536
|
},
|
|
107459
107537
|
{
|
|
107460
107538
|
name: "update-customer-agreement",
|
|
107461
|
-
description: "Update
|
|
107539
|
+
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.",
|
|
107462
107540
|
inputSchema: {
|
|
107463
107541
|
type: "object",
|
|
107464
107542
|
properties: {
|
|
@@ -107479,11 +107557,7 @@ var TOOLS = [
|
|
|
107479
107557
|
startDate: { type: "string" },
|
|
107480
107558
|
endDate: { type: "string" },
|
|
107481
107559
|
contractPeriodEnd: { type: "string" },
|
|
107482
|
-
proRataFirstInvoice: { type: "boolean" }
|
|
107483
|
-
status: {
|
|
107484
|
-
type: "string",
|
|
107485
|
-
enum: ["active", "paused", "cancelled", "ended"]
|
|
107486
|
-
}
|
|
107560
|
+
proRataFirstInvoice: { type: "boolean" }
|
|
107487
107561
|
},
|
|
107488
107562
|
required: ["id"]
|
|
107489
107563
|
}
|
|
@@ -108777,16 +108851,33 @@ async function handleCreateCustomerAgreement(input) {
|
|
|
108777
108851
|
endDate: input.endDate ?? null,
|
|
108778
108852
|
contractPeriodEnd: input.contractPeriodEnd ?? null,
|
|
108779
108853
|
proRataFirstInvoice: input.proRataFirstInvoice ?? false,
|
|
108780
|
-
|
|
108854
|
+
// MCP-created agreements start as a draft; a human activates them in the
|
|
108855
|
+
// dashboard. Drafts are excluded from every billing/due query.
|
|
108856
|
+
status: "draft"
|
|
108781
108857
|
}).returning();
|
|
108782
|
-
return textResponse2(
|
|
108858
|
+
return textResponse2(
|
|
108859
|
+
`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.
|
|
108783
108860
|
|
|
108784
|
-
${formatAgreement(row)}`
|
|
108861
|
+
${formatAgreement(row)}`
|
|
108862
|
+
);
|
|
108785
108863
|
}
|
|
108786
108864
|
async function handleUpdateCustomerAgreement(input) {
|
|
108787
108865
|
const scope = await resolveTeamScope(input.teamId);
|
|
108788
108866
|
if (!scope.ok) return scope.response;
|
|
108789
|
-
const
|
|
108867
|
+
const [existing] = await db.select().from(schema_exports.customerRecurringServices).where(
|
|
108868
|
+
and(
|
|
108869
|
+
eq(schema_exports.customerRecurringServices.id, input.id),
|
|
108870
|
+
inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
|
|
108871
|
+
)
|
|
108872
|
+
).limit(1);
|
|
108873
|
+
if (!existing) {
|
|
108874
|
+
return textResponse2("Agreement not found.");
|
|
108875
|
+
}
|
|
108876
|
+
if (existing.status !== "draft") {
|
|
108877
|
+
return textResponse2(
|
|
108878
|
+
`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.`
|
|
108879
|
+
);
|
|
108880
|
+
}
|
|
108790
108881
|
const patch = {
|
|
108791
108882
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
108792
108883
|
};
|
|
@@ -108810,17 +108901,16 @@ async function handleUpdateCustomerAgreement(input) {
|
|
|
108810
108901
|
patch.contractPeriodEnd = input.contractPeriodEnd;
|
|
108811
108902
|
if (input.proRataFirstInvoice !== void 0)
|
|
108812
108903
|
patch.proRataFirstInvoice = input.proRataFirstInvoice;
|
|
108813
|
-
if (input.status !== void 0) patch.status = input.status;
|
|
108814
108904
|
const [row] = await db.update(schema_exports.customerRecurringServices).set(patch).where(
|
|
108815
108905
|
and(
|
|
108816
108906
|
eq(schema_exports.customerRecurringServices.id, input.id),
|
|
108817
|
-
|
|
108907
|
+
inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)
|
|
108818
108908
|
)
|
|
108819
108909
|
).returning();
|
|
108820
108910
|
if (!row) {
|
|
108821
108911
|
return textResponse2("Agreement not found.");
|
|
108822
108912
|
}
|
|
108823
|
-
return textResponse2(`Updated
|
|
108913
|
+
return textResponse2(`Updated draft agreement:
|
|
108824
108914
|
|
|
108825
108915
|
${formatAgreement(row)}`);
|
|
108826
108916
|
}
|
|
@@ -114024,6 +114114,338 @@ async function handleLogHours(input) {
|
|
|
114024
114114
|
responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
|
|
114025
114115
|
return { content: [{ type: "text", text: responseText }] };
|
|
114026
114116
|
}
|
|
114117
|
+
var TIME_ENTRY_TYPES = [
|
|
114118
|
+
"meeting",
|
|
114119
|
+
"work",
|
|
114120
|
+
"clocked_work",
|
|
114121
|
+
"appointment",
|
|
114122
|
+
"task",
|
|
114123
|
+
"other"
|
|
114124
|
+
];
|
|
114125
|
+
var TIME_ENTRY_STATUSES = [
|
|
114126
|
+
"draft",
|
|
114127
|
+
"approved",
|
|
114128
|
+
"confirmed",
|
|
114129
|
+
"invoiced",
|
|
114130
|
+
"all"
|
|
114131
|
+
];
|
|
114132
|
+
var TIME_ENTRY_GROUP_BY = [
|
|
114133
|
+
"none",
|
|
114134
|
+
"day",
|
|
114135
|
+
"project",
|
|
114136
|
+
"customer",
|
|
114137
|
+
"user",
|
|
114138
|
+
"ticket"
|
|
114139
|
+
];
|
|
114140
|
+
var TIMEZONE = "Europe/Amsterdam";
|
|
114141
|
+
function textResponse3(text3) {
|
|
114142
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
114143
|
+
}
|
|
114144
|
+
function jsonResponse(payload) {
|
|
114145
|
+
return {
|
|
114146
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
114147
|
+
};
|
|
114148
|
+
}
|
|
114149
|
+
function toNumber(value) {
|
|
114150
|
+
if (value == null) return 0;
|
|
114151
|
+
if (typeof value === "number") return value;
|
|
114152
|
+
const parsed = Number.parseFloat(String(value));
|
|
114153
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
114154
|
+
}
|
|
114155
|
+
function hoursFrom(seconds) {
|
|
114156
|
+
return Math.round(toNumber(seconds) / 3600 * 100) / 100;
|
|
114157
|
+
}
|
|
114158
|
+
function deriveEntryStatus(invoiceId, rawStatus) {
|
|
114159
|
+
if (invoiceId) return "invoiced";
|
|
114160
|
+
if (rawStatus === "confirmed") return "approved";
|
|
114161
|
+
return "draft";
|
|
114162
|
+
}
|
|
114163
|
+
function durationSecondsExpr() {
|
|
114164
|
+
const te = schema_exports.timesheetEvents;
|
|
114165
|
+
return sql`CASE WHEN ${te.endTime} IS NOT NULL THEN GREATEST(0, EXTRACT(EPOCH FROM (${te.endTime} - ${te.startTime}))) ELSE COALESCE(${te.trackedDuration}, 0) END`;
|
|
114166
|
+
}
|
|
114167
|
+
function localDateExpr() {
|
|
114168
|
+
return sql`(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date`;
|
|
114169
|
+
}
|
|
114170
|
+
function localDateTextExpr() {
|
|
114171
|
+
return sql`to_char(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)}, 'YYYY-MM-DD')`;
|
|
114172
|
+
}
|
|
114173
|
+
function effectiveCustomerIdExpr() {
|
|
114174
|
+
return sql`COALESCE(${schema_exports.timesheetEvents.customerId}, ${schema_exports.projects.customerId})`;
|
|
114175
|
+
}
|
|
114176
|
+
async function buildTimeEntryGroups(groupBy, whereExpr) {
|
|
114177
|
+
const te = schema_exports.timesheetEvents;
|
|
114178
|
+
const totalSecondsSel = sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`;
|
|
114179
|
+
const billableSecondsSel = sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`;
|
|
114180
|
+
const countSel = sql`count(*)::int`;
|
|
114181
|
+
const finish = (rows2) => rows2.sort((a6, b7) => b7.totalHours - a6.totalHours);
|
|
114182
|
+
if (groupBy === "day") {
|
|
114183
|
+
const dayExpr = localDateTextExpr();
|
|
114184
|
+
const rows2 = await db.select({
|
|
114185
|
+
day: sql`${dayExpr}`,
|
|
114186
|
+
totalSeconds: totalSecondsSel,
|
|
114187
|
+
billableSeconds: billableSecondsSel,
|
|
114188
|
+
entryCount: countSel
|
|
114189
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(dayExpr);
|
|
114190
|
+
return finish(
|
|
114191
|
+
rows2.map((r6) => ({
|
|
114192
|
+
key: { type: "day", date: r6.day },
|
|
114193
|
+
entryCount: Number(r6.entryCount),
|
|
114194
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114195
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114196
|
+
}))
|
|
114197
|
+
);
|
|
114198
|
+
}
|
|
114199
|
+
if (groupBy === "project") {
|
|
114200
|
+
const rows2 = await db.select({
|
|
114201
|
+
projectId: te.projectId,
|
|
114202
|
+
projectName: schema_exports.projects.name,
|
|
114203
|
+
totalSeconds: totalSecondsSel,
|
|
114204
|
+
billableSeconds: billableSecondsSel,
|
|
114205
|
+
entryCount: countSel
|
|
114206
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(te.projectId, schema_exports.projects.name);
|
|
114207
|
+
return finish(
|
|
114208
|
+
rows2.map((r6) => ({
|
|
114209
|
+
key: {
|
|
114210
|
+
type: "project",
|
|
114211
|
+
id: r6.projectId,
|
|
114212
|
+
name: r6.projectId ? r6.projectName : "(no project)"
|
|
114213
|
+
},
|
|
114214
|
+
entryCount: Number(r6.entryCount),
|
|
114215
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114216
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114217
|
+
}))
|
|
114218
|
+
);
|
|
114219
|
+
}
|
|
114220
|
+
if (groupBy === "customer") {
|
|
114221
|
+
const custId = effectiveCustomerIdExpr();
|
|
114222
|
+
const rows2 = await db.select({
|
|
114223
|
+
customerId: sql`${custId}`,
|
|
114224
|
+
customerName: schema_exports.customers.name,
|
|
114225
|
+
totalSeconds: totalSecondsSel,
|
|
114226
|
+
billableSeconds: billableSecondsSel,
|
|
114227
|
+
entryCount: countSel
|
|
114228
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.customers, sql`${schema_exports.customers.id} = ${custId}`).where(whereExpr).groupBy(custId, schema_exports.customers.name);
|
|
114229
|
+
return finish(
|
|
114230
|
+
rows2.map((r6) => ({
|
|
114231
|
+
key: {
|
|
114232
|
+
type: "customer",
|
|
114233
|
+
id: r6.customerId,
|
|
114234
|
+
name: r6.customerId ? r6.customerName : "(no customer)"
|
|
114235
|
+
},
|
|
114236
|
+
entryCount: Number(r6.entryCount),
|
|
114237
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114238
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114239
|
+
}))
|
|
114240
|
+
);
|
|
114241
|
+
}
|
|
114242
|
+
if (groupBy === "user") {
|
|
114243
|
+
const rows2 = await db.select({
|
|
114244
|
+
userId: te.userId,
|
|
114245
|
+
userName: schema_exports.users.fullName,
|
|
114246
|
+
totalSeconds: totalSecondsSel,
|
|
114247
|
+
billableSeconds: billableSecondsSel,
|
|
114248
|
+
entryCount: countSel
|
|
114249
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.users, eq(te.userId, schema_exports.users.id)).where(whereExpr).groupBy(te.userId, schema_exports.users.fullName);
|
|
114250
|
+
return finish(
|
|
114251
|
+
rows2.map((r6) => ({
|
|
114252
|
+
key: { type: "user", id: r6.userId, name: r6.userName },
|
|
114253
|
+
entryCount: Number(r6.entryCount),
|
|
114254
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114255
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114256
|
+
}))
|
|
114257
|
+
);
|
|
114258
|
+
}
|
|
114259
|
+
const tet = schema_exports.timesheetEventTickets;
|
|
114260
|
+
const rows = await db.select({
|
|
114261
|
+
ticketId: schema_exports.tickets.id,
|
|
114262
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
114263
|
+
totalSeconds: totalSecondsSel,
|
|
114264
|
+
billableSeconds: billableSecondsSel,
|
|
114265
|
+
entryCount: countSel
|
|
114266
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(tet, eq(tet.timesheetEventId, te.id)).leftJoin(schema_exports.tickets, eq(schema_exports.tickets.id, tet.ticketId)).where(whereExpr).groupBy(schema_exports.tickets.id, schema_exports.tickets.ticketNumber);
|
|
114267
|
+
return finish(
|
|
114268
|
+
rows.map((r6) => ({
|
|
114269
|
+
key: {
|
|
114270
|
+
type: "ticket",
|
|
114271
|
+
id: r6.ticketId,
|
|
114272
|
+
ticketNumber: r6.ticketId ? r6.ticketNumber : "(no ticket)"
|
|
114273
|
+
},
|
|
114274
|
+
entryCount: Number(r6.entryCount),
|
|
114275
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114276
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114277
|
+
}))
|
|
114278
|
+
);
|
|
114279
|
+
}
|
|
114280
|
+
async function handleGetTimeEntries(input) {
|
|
114281
|
+
const ctx = getAuthContext();
|
|
114282
|
+
const te = schema_exports.timesheetEvents;
|
|
114283
|
+
const status = input.status ?? "all";
|
|
114284
|
+
if (!TIME_ENTRY_STATUSES.includes(status)) {
|
|
114285
|
+
return textResponse3(
|
|
114286
|
+
`Error: invalid status "${input.status}". Allowed: ${TIME_ENTRY_STATUSES.join(", ")}.`
|
|
114287
|
+
);
|
|
114288
|
+
}
|
|
114289
|
+
const groupBy = input.groupBy ?? "none";
|
|
114290
|
+
if (!TIME_ENTRY_GROUP_BY.includes(groupBy)) {
|
|
114291
|
+
return textResponse3(
|
|
114292
|
+
`Error: invalid groupBy "${input.groupBy}". Allowed: ${TIME_ENTRY_GROUP_BY.join(", ")}.`
|
|
114293
|
+
);
|
|
114294
|
+
}
|
|
114295
|
+
if (input.type && !TIME_ENTRY_TYPES.includes(input.type)) {
|
|
114296
|
+
return textResponse3(
|
|
114297
|
+
`Error: invalid type "${input.type}". Allowed: ${TIME_ENTRY_TYPES.join(", ")}. (Deadlines/all-day markers are agenda items \u2014 use get-calendar-items.)`
|
|
114298
|
+
);
|
|
114299
|
+
}
|
|
114300
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
114301
|
+
if (!scope.ok) return scope.response;
|
|
114302
|
+
if (scope.teamIds.length === 0) {
|
|
114303
|
+
return textResponse3("No accessible teams found.");
|
|
114304
|
+
}
|
|
114305
|
+
if (input.projectId && !scope.projectIds.includes(input.projectId)) {
|
|
114306
|
+
return textResponse3(
|
|
114307
|
+
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
114308
|
+
);
|
|
114309
|
+
}
|
|
114310
|
+
if (input.customerId && !scope.customerIds.includes(input.customerId)) {
|
|
114311
|
+
return textResponse3(
|
|
114312
|
+
`Customer not found or no access: ${input.customerId}. Call get-customers first.`
|
|
114313
|
+
);
|
|
114314
|
+
}
|
|
114315
|
+
const rawUserId = input.userId?.trim();
|
|
114316
|
+
const userId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
|
|
114317
|
+
const conditions = [
|
|
114318
|
+
inArray(te.teamId, scope.teamIds),
|
|
114319
|
+
eq(te.isDeleted, false),
|
|
114320
|
+
// Worked time only: drop all-day markers / deadlines (~24h agenda blocks
|
|
114321
|
+
// that would otherwise massively inflate the totals).
|
|
114322
|
+
eq(te.allDay, false),
|
|
114323
|
+
sql`${te.type}::text <> 'deadline'`
|
|
114324
|
+
];
|
|
114325
|
+
if (userId !== "all") conditions.push(eq(te.userId, userId));
|
|
114326
|
+
if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
|
|
114327
|
+
if (input.customerId) {
|
|
114328
|
+
conditions.push(
|
|
114329
|
+
sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId}) = ${input.customerId}`
|
|
114330
|
+
);
|
|
114331
|
+
}
|
|
114332
|
+
if (input.dateFrom) {
|
|
114333
|
+
conditions.push(sql`${localDateExpr()} >= ${input.dateFrom}::date`);
|
|
114334
|
+
}
|
|
114335
|
+
if (input.dateTo) {
|
|
114336
|
+
conditions.push(sql`${localDateExpr()} <= ${input.dateTo}::date`);
|
|
114337
|
+
}
|
|
114338
|
+
if (input.type) conditions.push(sql`${te.type}::text = ${input.type}`);
|
|
114339
|
+
if (input.ticketId) {
|
|
114340
|
+
conditions.push(
|
|
114341
|
+
sql`EXISTS (SELECT 1 FROM ${schema_exports.timesheetEventTickets} WHERE ${schema_exports.timesheetEventTickets.timesheetEventId} = ${te.id} AND ${schema_exports.timesheetEventTickets.ticketId} = ${input.ticketId})`
|
|
114342
|
+
);
|
|
114343
|
+
}
|
|
114344
|
+
if (status === "draft") {
|
|
114345
|
+
conditions.push(sql`(${te.status}::text = 'draft' AND ${te.invoiceId} IS NULL)`);
|
|
114346
|
+
} else if (status === "approved" || status === "confirmed") {
|
|
114347
|
+
conditions.push(
|
|
114348
|
+
sql`(${te.status}::text = 'confirmed' AND ${te.invoiceId} IS NULL)`
|
|
114349
|
+
);
|
|
114350
|
+
} else if (status === "invoiced") {
|
|
114351
|
+
conditions.push(sql`${te.invoiceId} IS NOT NULL`);
|
|
114352
|
+
}
|
|
114353
|
+
const whereExpr = and(...conditions);
|
|
114354
|
+
const [totalsRow] = await db.select({
|
|
114355
|
+
count: sql`count(*)::int`,
|
|
114356
|
+
totalSeconds: sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`,
|
|
114357
|
+
billableSeconds: sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`,
|
|
114358
|
+
invoicedSeconds: sql`COALESCE(SUM(CASE WHEN ${te.invoiceId} IS NOT NULL THEN ${durationSecondsExpr()} ELSE 0 END), 0)`
|
|
114359
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr);
|
|
114360
|
+
const totalSeconds = toNumber(totalsRow?.totalSeconds);
|
|
114361
|
+
const billableSeconds = toNumber(totalsRow?.billableSeconds);
|
|
114362
|
+
const invoicedSeconds = toNumber(totalsRow?.invoicedSeconds);
|
|
114363
|
+
const entryCount = Number(totalsRow?.count ?? 0);
|
|
114364
|
+
const pageSize = Math.min(input.pageSize ?? 50, 200);
|
|
114365
|
+
const entryRows = await db.select({
|
|
114366
|
+
id: te.id,
|
|
114367
|
+
title: te.title,
|
|
114368
|
+
description: te.description,
|
|
114369
|
+
type: te.type,
|
|
114370
|
+
rawStatus: te.status,
|
|
114371
|
+
billingStatus: te.billingStatus,
|
|
114372
|
+
invoiceId: te.invoiceId,
|
|
114373
|
+
startTime: te.startTime,
|
|
114374
|
+
endTime: te.endTime,
|
|
114375
|
+
date: sql`${localDateTextExpr()}`,
|
|
114376
|
+
durationSeconds: sql`${durationSecondsExpr()}`,
|
|
114377
|
+
userId: te.userId,
|
|
114378
|
+
userName: schema_exports.users.fullName,
|
|
114379
|
+
projectId: te.projectId,
|
|
114380
|
+
projectName: schema_exports.projects.name,
|
|
114381
|
+
customerId: sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`,
|
|
114382
|
+
customerName: schema_exports.customers.name
|
|
114383
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).leftJoin(schema_exports.users, eq(te.userId, schema_exports.users.id)).leftJoin(
|
|
114384
|
+
schema_exports.customers,
|
|
114385
|
+
sql`${schema_exports.customers.id} = COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`
|
|
114386
|
+
).where(whereExpr).orderBy(desc(te.startTime)).limit(pageSize);
|
|
114387
|
+
const entryIds = entryRows.map((r6) => r6.id);
|
|
114388
|
+
const ticketsByEvent = /* @__PURE__ */ new Map();
|
|
114389
|
+
if (entryIds.length > 0) {
|
|
114390
|
+
const links = await db.select({
|
|
114391
|
+
eventId: schema_exports.timesheetEventTickets.timesheetEventId,
|
|
114392
|
+
ticketId: schema_exports.tickets.id,
|
|
114393
|
+
ticketNumber: schema_exports.tickets.ticketNumber
|
|
114394
|
+
}).from(schema_exports.timesheetEventTickets).innerJoin(
|
|
114395
|
+
schema_exports.tickets,
|
|
114396
|
+
eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
|
|
114397
|
+
).where(inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds));
|
|
114398
|
+
for (const link of links) {
|
|
114399
|
+
const list = ticketsByEvent.get(link.eventId) ?? [];
|
|
114400
|
+
list.push({ id: link.ticketId, ticketNumber: link.ticketNumber });
|
|
114401
|
+
ticketsByEvent.set(link.eventId, list);
|
|
114402
|
+
}
|
|
114403
|
+
}
|
|
114404
|
+
const entries = entryRows.map((r6) => ({
|
|
114405
|
+
id: r6.id,
|
|
114406
|
+
date: r6.date,
|
|
114407
|
+
startTime: r6.startTime,
|
|
114408
|
+
endTime: r6.endTime,
|
|
114409
|
+
hours: hoursFrom(r6.durationSeconds),
|
|
114410
|
+
title: r6.title,
|
|
114411
|
+
description: r6.description,
|
|
114412
|
+
type: r6.type,
|
|
114413
|
+
status: deriveEntryStatus(r6.invoiceId, r6.rawStatus),
|
|
114414
|
+
billingStatus: r6.billingStatus,
|
|
114415
|
+
invoiced: r6.invoiceId != null,
|
|
114416
|
+
invoiceId: r6.invoiceId,
|
|
114417
|
+
user: r6.userId ? { id: r6.userId, name: r6.userName } : null,
|
|
114418
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
114419
|
+
customer: r6.customerId ? { id: r6.customerId, name: r6.customerName } : null,
|
|
114420
|
+
tickets: ticketsByEvent.get(r6.id) ?? []
|
|
114421
|
+
}));
|
|
114422
|
+
const groups = groupBy === "none" ? void 0 : await buildTimeEntryGroups(groupBy, whereExpr);
|
|
114423
|
+
return jsonResponse({
|
|
114424
|
+
filters: {
|
|
114425
|
+
userId: userId === "all" ? "all" : userId,
|
|
114426
|
+
teamIds: scope.teamIds,
|
|
114427
|
+
projectId: input.projectId ?? null,
|
|
114428
|
+
customerId: input.customerId ?? null,
|
|
114429
|
+
ticketId: input.ticketId ?? null,
|
|
114430
|
+
dateFrom: input.dateFrom ?? null,
|
|
114431
|
+
dateTo: input.dateTo ?? null,
|
|
114432
|
+
status,
|
|
114433
|
+
type: input.type ?? null,
|
|
114434
|
+
timezone: TIMEZONE
|
|
114435
|
+
},
|
|
114436
|
+
totals: {
|
|
114437
|
+
totalHours: hoursFrom(totalSeconds),
|
|
114438
|
+
billableHours: hoursFrom(billableSeconds),
|
|
114439
|
+
nonBillableHours: hoursFrom(totalSeconds - billableSeconds),
|
|
114440
|
+
invoicedHours: hoursFrom(invoicedSeconds),
|
|
114441
|
+
entryCount
|
|
114442
|
+
},
|
|
114443
|
+
groupBy,
|
|
114444
|
+
...groups ? { groups } : {},
|
|
114445
|
+
entries,
|
|
114446
|
+
entriesTruncated: entryCount > entries.length
|
|
114447
|
+
});
|
|
114448
|
+
}
|
|
114027
114449
|
|
|
114028
114450
|
// ../invoice/src/utils/included-items.ts
|
|
114029
114451
|
function parseIncludedItems(value) {
|
|
@@ -114397,7 +114819,7 @@ var INVOICE_STATUSES = [
|
|
|
114397
114819
|
"refunded"
|
|
114398
114820
|
];
|
|
114399
114821
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
114400
|
-
function
|
|
114822
|
+
function textResponse4(text3) {
|
|
114401
114823
|
return { content: [{ type: "text", text: text3 }] };
|
|
114402
114824
|
}
|
|
114403
114825
|
function tiptapNote(text3) {
|
|
@@ -114515,7 +114937,7 @@ async function resolveInvoiceLineItems(inputs, defaults, teamId) {
|
|
|
114515
114937
|
return { items };
|
|
114516
114938
|
}
|
|
114517
114939
|
function notDraftResponse(invoice) {
|
|
114518
|
-
return
|
|
114940
|
+
return textResponse4(
|
|
114519
114941
|
`Invoice ${invoice.invoiceNumber ?? invoice.id} has status "${invoice.status}", not "draft". These tools only modify draft invoices \u2014 sent/paid/unpaid/overdue invoices are immutable here.`
|
|
114520
114942
|
);
|
|
114521
114943
|
}
|
|
@@ -114532,14 +114954,14 @@ Issue: ${invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString() :
|
|
|
114532
114954
|
async function handleGetInvoices(input) {
|
|
114533
114955
|
const { customerId, status, q: q3, pageSize = 20 } = input;
|
|
114534
114956
|
if (status && !INVOICE_STATUSES.includes(status)) {
|
|
114535
|
-
return
|
|
114957
|
+
return textResponse4(
|
|
114536
114958
|
`Error: invalid status "${status}". Allowed: ${INVOICE_STATUSES.join(", ")}.`
|
|
114537
114959
|
);
|
|
114538
114960
|
}
|
|
114539
114961
|
const scope = await resolveTeamScope(input.teamId);
|
|
114540
114962
|
if (!scope.ok) return scope.response;
|
|
114541
114963
|
if (scope.teamIds.length === 0) {
|
|
114542
|
-
return
|
|
114964
|
+
return textResponse4("No accessible teams found.");
|
|
114543
114965
|
}
|
|
114544
114966
|
const filters = [inArray(schema_exports.invoices.teamId, scope.teamIds)];
|
|
114545
114967
|
if (customerId) filters.push(eq(schema_exports.invoices.customerId, customerId));
|
|
@@ -114566,7 +114988,7 @@ async function handleGetInvoices(input) {
|
|
|
114566
114988
|
createdAt: schema_exports.invoices.createdAt
|
|
114567
114989
|
}).from(schema_exports.invoices).where(and(...filters)).orderBy(desc(schema_exports.invoices.createdAt)).limit(Math.min(pageSize, 100));
|
|
114568
114990
|
if (rows.length === 0) {
|
|
114569
|
-
return
|
|
114991
|
+
return textResponse4("No invoices found.");
|
|
114570
114992
|
}
|
|
114571
114993
|
const list = rows.map(
|
|
114572
114994
|
(inv) => `**${inv.invoiceNumber ?? "(draft, no number)"}**
|
|
@@ -114576,7 +114998,7 @@ Customer: ${inv.customerName ?? inv.customerId ?? "(none)"}
|
|
|
114576
114998
|
Issue date: ${inv.issueDate ? new Date(inv.issueDate).toLocaleDateString() : "-"} | Due: ${inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : "-"}
|
|
114577
114999
|
`
|
|
114578
115000
|
).join("\n");
|
|
114579
|
-
return
|
|
115001
|
+
return textResponse4(
|
|
114580
115002
|
`Found ${rows.length} invoices:
|
|
114581
115003
|
|
|
114582
115004
|
${list}
|
|
@@ -114585,15 +115007,15 @@ Use \`get-invoice-by-id\` for line items and linked documents. Use \`link-docume
|
|
|
114585
115007
|
}
|
|
114586
115008
|
async function handleGetInvoiceById(input) {
|
|
114587
115009
|
const { invoiceId } = input;
|
|
114588
|
-
if (!invoiceId) return
|
|
115010
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114589
115011
|
const scope = await resolveTeamScope(input.teamId);
|
|
114590
115012
|
if (!scope.ok) return scope.response;
|
|
114591
115013
|
if (scope.teamIds.length === 0) {
|
|
114592
|
-
return
|
|
115014
|
+
return textResponse4("No accessible teams found.");
|
|
114593
115015
|
}
|
|
114594
115016
|
const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
|
|
114595
115017
|
if (!invoice) {
|
|
114596
|
-
return
|
|
115018
|
+
return textResponse4(
|
|
114597
115019
|
`Invoice ${invoiceId} not found or you don't have access to it.`
|
|
114598
115020
|
);
|
|
114599
115021
|
}
|
|
@@ -114610,7 +115032,7 @@ async function handleGetInvoiceById(input) {
|
|
|
114610
115032
|
);
|
|
114611
115033
|
const linesText = lineItems.length > 0 ? lineItems.map((line2, i6) => formatLineItemDetail(line2, i6)).join("\n\n") : "(no line items)";
|
|
114612
115034
|
const docsText = linkedDocs.length > 0 ? linkedDocs.map((d6) => `- ${d6.title} (${d6.type ?? "document"}) \u2014 ${d6.id}`).join("\n") : "(none)";
|
|
114613
|
-
return
|
|
115035
|
+
return textResponse4(
|
|
114614
115036
|
`**Invoice ${invoice.invoiceNumber ?? invoice.id}**
|
|
114615
115037
|
|
|
114616
115038
|
ID: ${invoice.id}
|
|
@@ -114632,12 +115054,12 @@ ${docsText}
|
|
|
114632
115054
|
}
|
|
114633
115055
|
async function handleUpdateInvoice(input) {
|
|
114634
115056
|
const { invoiceId } = input;
|
|
114635
|
-
if (!invoiceId) return
|
|
115057
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114636
115058
|
const resolved = await resolveTeamId(input.teamId);
|
|
114637
115059
|
if (!resolved.ok) return resolved.response;
|
|
114638
115060
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114639
115061
|
if (!invoice) {
|
|
114640
|
-
return
|
|
115062
|
+
return textResponse4(
|
|
114641
115063
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114642
115064
|
);
|
|
114643
115065
|
}
|
|
@@ -114664,7 +115086,7 @@ async function handleUpdateInvoice(input) {
|
|
|
114664
115086
|
defaults,
|
|
114665
115087
|
invoice.teamId
|
|
114666
115088
|
);
|
|
114667
|
-
if (error49) return
|
|
115089
|
+
if (error49) return textResponse4(`Error: ${error49}`);
|
|
114668
115090
|
const totals = computeInvoiceTotals(
|
|
114669
115091
|
items,
|
|
114670
115092
|
defaults,
|
|
@@ -114677,28 +115099,28 @@ async function handleUpdateInvoice(input) {
|
|
|
114677
115099
|
updates.amount = totals.amount;
|
|
114678
115100
|
}
|
|
114679
115101
|
if (Object.keys(updates).length === 0) {
|
|
114680
|
-
return
|
|
115102
|
+
return textResponse4(
|
|
114681
115103
|
"No fields to update. Provide at least one of: title, note, internalNote, dueDate, issueDate, lineItems."
|
|
114682
115104
|
);
|
|
114683
115105
|
}
|
|
114684
115106
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
114685
115107
|
const [updated] = await db.update(schema_exports.invoices).set(updates).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
|
|
114686
|
-
if (!updated) return
|
|
114687
|
-
return
|
|
115108
|
+
if (!updated) return textResponse4(`Failed to update invoice ${invoiceId}.`);
|
|
115109
|
+
return textResponse4(`\u2705 **Draft invoice updated**
|
|
114688
115110
|
|
|
114689
115111
|
${formatInvoiceSummary(updated)}`);
|
|
114690
115112
|
}
|
|
114691
115113
|
async function handleUpdateInvoiceLines(input) {
|
|
114692
115114
|
const { invoiceId, lineItems: patches } = input;
|
|
114693
|
-
if (!invoiceId) return
|
|
115115
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114694
115116
|
if (!patches || patches.length === 0) {
|
|
114695
|
-
return
|
|
115117
|
+
return textResponse4("Error: `lineItems` must be a non-empty array.");
|
|
114696
115118
|
}
|
|
114697
115119
|
const resolved = await resolveTeamId(input.teamId);
|
|
114698
115120
|
if (!resolved.ok) return resolved.response;
|
|
114699
115121
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114700
115122
|
if (!invoice) {
|
|
114701
|
-
return
|
|
115123
|
+
return textResponse4(
|
|
114702
115124
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114703
115125
|
);
|
|
114704
115126
|
}
|
|
@@ -114712,7 +115134,7 @@ async function handleUpdateInvoiceLines(input) {
|
|
|
114712
115134
|
for (const patch of patches) {
|
|
114713
115135
|
const index2 = patch.index;
|
|
114714
115136
|
if (index2 < 0 || index2 >= items.length) {
|
|
114715
|
-
return
|
|
115137
|
+
return textResponse4(
|
|
114716
115138
|
`Error: line index ${index2} is out of range (invoice has ${items.length} line(s)).`
|
|
114717
115139
|
);
|
|
114718
115140
|
}
|
|
@@ -114739,13 +115161,13 @@ async function handleUpdateInvoiceLines(input) {
|
|
|
114739
115161
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114740
115162
|
}).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
|
|
114741
115163
|
if (!updated) {
|
|
114742
|
-
return
|
|
115164
|
+
return textResponse4(`Failed to update invoice lines for ${invoiceId}.`);
|
|
114743
115165
|
}
|
|
114744
115166
|
const changedLines = patches.map((p3) => {
|
|
114745
115167
|
const line2 = items[p3.index];
|
|
114746
115168
|
return `[${p3.index}] ${plainTextFromLineItemName(line2.name)}`;
|
|
114747
115169
|
}).join("\n");
|
|
114748
|
-
return
|
|
115170
|
+
return textResponse4(
|
|
114749
115171
|
`\u2705 **Updated ${updatedCount} line item(s) on draft invoice ${updated.invoiceNumber ?? updated.id}**
|
|
114750
115172
|
|
|
114751
115173
|
${changedLines}
|
|
@@ -114755,13 +115177,13 @@ New total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal},
|
|
|
114755
115177
|
}
|
|
114756
115178
|
async function handleAddProductToInvoice(input) {
|
|
114757
115179
|
const { invoiceId, productId } = input;
|
|
114758
|
-
if (!invoiceId) return
|
|
114759
|
-
if (!productId) return
|
|
115180
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
115181
|
+
if (!productId) return textResponse4("Error: `productId` is required.");
|
|
114760
115182
|
const resolved = await resolveTeamId(input.teamId);
|
|
114761
115183
|
if (!resolved.ok) return resolved.response;
|
|
114762
115184
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114763
115185
|
if (!invoice) {
|
|
114764
|
-
return
|
|
115186
|
+
return textResponse4(
|
|
114765
115187
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114766
115188
|
);
|
|
114767
115189
|
}
|
|
@@ -114769,7 +115191,7 @@ async function handleAddProductToInvoice(input) {
|
|
|
114769
115191
|
const products = await loadProductsInTeam([productId], invoice.teamId);
|
|
114770
115192
|
const product = products.get(productId);
|
|
114771
115193
|
if (!product) {
|
|
114772
|
-
return
|
|
115194
|
+
return textResponse4(
|
|
114773
115195
|
`Product ${productId} not found or not owned by this team.`
|
|
114774
115196
|
);
|
|
114775
115197
|
}
|
|
@@ -114801,9 +115223,9 @@ async function handleAddProductToInvoice(input) {
|
|
|
114801
115223
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114802
115224
|
}).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
|
|
114803
115225
|
if (!updated) {
|
|
114804
|
-
return
|
|
115226
|
+
return textResponse4(`Failed to add product to invoice ${invoiceId}.`);
|
|
114805
115227
|
}
|
|
114806
|
-
return
|
|
115228
|
+
return textResponse4(
|
|
114807
115229
|
`\u2705 **Product added to draft invoice ${updated.invoiceNumber ?? updated.id}**
|
|
114808
115230
|
|
|
114809
115231
|
` + formatLineItemDetail(newItem, items.length - 1) + `
|
|
@@ -114815,12 +115237,12 @@ Clause and pricing variant are snapshotted on the line \u2014 later catalog edit
|
|
|
114815
115237
|
async function handleLinkDocumentToInvoice(input) {
|
|
114816
115238
|
const { documentId, invoiceId } = input;
|
|
114817
115239
|
if (!documentId) {
|
|
114818
|
-
return
|
|
115240
|
+
return textResponse4("Error: `documentId` is required.");
|
|
114819
115241
|
}
|
|
114820
115242
|
const scope = await resolveTeamScope(input.teamId);
|
|
114821
115243
|
if (!scope.ok) return scope.response;
|
|
114822
115244
|
if (scope.teamIds.length === 0) {
|
|
114823
|
-
return
|
|
115245
|
+
return textResponse4("No accessible teams found.");
|
|
114824
115246
|
}
|
|
114825
115247
|
const [doc] = await db.select({
|
|
114826
115248
|
id: schema_exports.documents.id,
|
|
@@ -114835,24 +115257,24 @@ async function handleLinkDocumentToInvoice(input) {
|
|
|
114835
115257
|
)
|
|
114836
115258
|
).limit(1);
|
|
114837
115259
|
if (!doc) {
|
|
114838
|
-
return
|
|
115260
|
+
return textResponse4(
|
|
114839
115261
|
`Document ${documentId} not found or you don't have access to it.`
|
|
114840
115262
|
);
|
|
114841
115263
|
}
|
|
114842
115264
|
if (!invoiceId) {
|
|
114843
115265
|
await db.update(schema_exports.documents).set({ invoiceId: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
|
|
114844
|
-
return
|
|
115266
|
+
return textResponse4(
|
|
114845
115267
|
`\u2705 Document "${doc.title}" (${doc.id}) is unlinked from its invoice.`
|
|
114846
115268
|
);
|
|
114847
115269
|
}
|
|
114848
115270
|
const invoice = await findAccessibleInvoice(invoiceId, [doc.teamId]);
|
|
114849
115271
|
if (!invoice) {
|
|
114850
|
-
return
|
|
115272
|
+
return textResponse4(
|
|
114851
115273
|
`Error: invoice ${invoiceId} not found in team ${doc.teamId}. Use get-invoices to find a valid invoice id.`
|
|
114852
115274
|
);
|
|
114853
115275
|
}
|
|
114854
115276
|
await db.update(schema_exports.documents).set({ invoiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
|
|
114855
|
-
return
|
|
115277
|
+
return textResponse4(
|
|
114856
115278
|
`\u2705 **Document linked to invoice!**
|
|
114857
115279
|
|
|
114858
115280
|
Document: ${doc.title} (${doc.id})
|
|
@@ -114985,7 +115407,7 @@ ${description ? `Description: ${description}
|
|
|
114985
115407
|
]
|
|
114986
115408
|
};
|
|
114987
115409
|
}
|
|
114988
|
-
function
|
|
115410
|
+
function textResponse5(text3) {
|
|
114989
115411
|
return { content: [{ type: "text", text: text3 }] };
|
|
114990
115412
|
}
|
|
114991
115413
|
function memberLabel(m4) {
|
|
@@ -114999,7 +115421,7 @@ async function requireTeamOwner2(teamId, userId) {
|
|
|
114999
115421
|
eq(schema_exports.usersOnTeam.teamId, teamId)
|
|
115000
115422
|
)
|
|
115001
115423
|
).limit(1);
|
|
115002
|
-
return membership?.role === "owner" ? null :
|
|
115424
|
+
return membership?.role === "owner" ? null : textResponse5(OWNER_REQUIRED);
|
|
115003
115425
|
}
|
|
115004
115426
|
async function setProjectMemberAccess(params) {
|
|
115005
115427
|
const { projectId, teamId, memberIds, createdBy } = params;
|
|
@@ -115103,7 +115525,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115103
115525
|
if (!match) {
|
|
115104
115526
|
return {
|
|
115105
115527
|
ok: false,
|
|
115106
|
-
response:
|
|
115528
|
+
response: textResponse5(
|
|
115107
115529
|
`User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
|
|
115108
115530
|
)
|
|
115109
115531
|
};
|
|
@@ -115116,7 +115538,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115116
115538
|
if (matches.length === 0) {
|
|
115117
115539
|
return {
|
|
115118
115540
|
ok: false,
|
|
115119
|
-
response:
|
|
115541
|
+
response: textResponse5(
|
|
115120
115542
|
`No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
|
|
115121
115543
|
)
|
|
115122
115544
|
};
|
|
@@ -115124,7 +115546,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115124
115546
|
if (matches.length > 1) {
|
|
115125
115547
|
return {
|
|
115126
115548
|
ok: false,
|
|
115127
|
-
response:
|
|
115549
|
+
response: textResponse5(
|
|
115128
115550
|
`Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
|
|
115129
115551
|
)
|
|
115130
115552
|
};
|
|
@@ -115133,7 +115555,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115133
115555
|
}
|
|
115134
115556
|
return {
|
|
115135
115557
|
ok: false,
|
|
115136
|
-
response:
|
|
115558
|
+
response: textResponse5(
|
|
115137
115559
|
"Provide either a userId or an email to identify the member."
|
|
115138
115560
|
)
|
|
115139
115561
|
};
|
|
@@ -115182,7 +115604,7 @@ async function handleUpdateProject(input) {
|
|
|
115182
115604
|
if (!resolved.ok) return resolved.response;
|
|
115183
115605
|
const existing = await loadProjectInTeam(id, resolved.teamId);
|
|
115184
115606
|
if (!existing) {
|
|
115185
|
-
return
|
|
115607
|
+
return textResponse5(
|
|
115186
115608
|
`Project ${id} not found, or it is not owned by this team.`
|
|
115187
115609
|
);
|
|
115188
115610
|
}
|
|
@@ -115197,7 +115619,7 @@ async function handleUpdateProject(input) {
|
|
|
115197
115619
|
)
|
|
115198
115620
|
).limit(1);
|
|
115199
115621
|
if (dupe) {
|
|
115200
|
-
return
|
|
115622
|
+
return textResponse5(
|
|
115201
115623
|
`A project named "${input.name}" already exists in this team. Choose a different name.`
|
|
115202
115624
|
);
|
|
115203
115625
|
}
|
|
@@ -115262,7 +115684,7 @@ async function handleUpdateProject(input) {
|
|
|
115262
115684
|
customerName: schema_exports.customers.name
|
|
115263
115685
|
}).from(schema_exports.projects).leftJoin(schema_exports.customers, eq(schema_exports.projects.customerId, schema_exports.customers.id)).where(eq(schema_exports.projects.id, id)).limit(1);
|
|
115264
115686
|
if (!updated) {
|
|
115265
|
-
return
|
|
115687
|
+
return textResponse5(`Failed to update project ${id}.`);
|
|
115266
115688
|
}
|
|
115267
115689
|
const lines = [
|
|
115268
115690
|
"\u2705 **Project Updated**",
|
|
@@ -115280,7 +115702,7 @@ async function handleUpdateProject(input) {
|
|
|
115280
115702
|
if (willRename) {
|
|
115281
115703
|
lines.push("", "Note: tickets for this project were renumbered.");
|
|
115282
115704
|
}
|
|
115283
|
-
return
|
|
115705
|
+
return textResponse5(lines.join("\n"));
|
|
115284
115706
|
}
|
|
115285
115707
|
async function handleGetProjectMembers(input) {
|
|
115286
115708
|
const { projectId } = input;
|
|
@@ -115288,7 +115710,7 @@ async function handleGetProjectMembers(input) {
|
|
|
115288
115710
|
if (!resolved.ok) return resolved.response;
|
|
115289
115711
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115290
115712
|
if (!project) {
|
|
115291
|
-
return
|
|
115713
|
+
return textResponse5(
|
|
115292
115714
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115293
115715
|
);
|
|
115294
115716
|
}
|
|
@@ -115317,7 +115739,7 @@ async function handleGetProjectMembers(input) {
|
|
|
115317
115739
|
return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
|
|
115318
115740
|
}).join("\n");
|
|
115319
115741
|
const note = state2.projectMemberIds.size === 0 ? "No members are explicitly assigned to this project, so every owner and every unrestricted member can see it." : `${state2.projectMemberIds.size} member(s) are explicitly assigned to this project.`;
|
|
115320
|
-
return
|
|
115742
|
+
return textResponse5(
|
|
115321
115743
|
`**Project members for "${project.name}"** (ID: ${project.id})
|
|
115322
115744
|
|
|
115323
115745
|
${note}
|
|
@@ -115338,7 +115760,7 @@ async function handleSetProjectMembers(input) {
|
|
|
115338
115760
|
if (ownerError) return ownerError;
|
|
115339
115761
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115340
115762
|
if (!project) {
|
|
115341
|
-
return
|
|
115763
|
+
return textResponse5(
|
|
115342
115764
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115343
115765
|
);
|
|
115344
115766
|
}
|
|
@@ -115376,7 +115798,7 @@ async function handleSetProjectMembers(input) {
|
|
|
115376
115798
|
|
|
115377
115799
|
\u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
|
|
115378
115800
|
}
|
|
115379
|
-
return
|
|
115801
|
+
return textResponse5(
|
|
115380
115802
|
`\u2705 **Project members updated**
|
|
115381
115803
|
|
|
115382
115804
|
Members with explicit access to this project:
|
|
@@ -115392,7 +115814,7 @@ async function handleAddProjectMember(input) {
|
|
|
115392
115814
|
if (ownerError) return ownerError;
|
|
115393
115815
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115394
115816
|
if (!project) {
|
|
115395
|
-
return
|
|
115817
|
+
return textResponse5(
|
|
115396
115818
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115397
115819
|
);
|
|
115398
115820
|
}
|
|
@@ -115403,7 +115825,7 @@ async function handleAddProjectMember(input) {
|
|
|
115403
115825
|
if (!member2.ok) return member2.response;
|
|
115404
115826
|
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
115405
115827
|
if (state2.projectMemberIds.has(member2.member.userId)) {
|
|
115406
|
-
return
|
|
115828
|
+
return textResponse5(
|
|
115407
115829
|
`${memberLabel(member2.member)} already has explicit access to this project.`
|
|
115408
115830
|
);
|
|
115409
115831
|
}
|
|
@@ -115418,7 +115840,7 @@ async function handleAddProjectMember(input) {
|
|
|
115418
115840
|
if (wasUnrestricted) {
|
|
115419
115841
|
text3 += "\n\n\u26A0\uFE0F This member previously had no access restrictions (they could see all projects). They are now restricted to ONLY the projects explicitly assigned to them. Grant any other projects they still need with add-project-member, or remove all their assignments to restore full visibility.";
|
|
115420
115842
|
}
|
|
115421
|
-
return
|
|
115843
|
+
return textResponse5(text3);
|
|
115422
115844
|
}
|
|
115423
115845
|
async function handleRemoveProjectMember(input) {
|
|
115424
115846
|
const ctx = getAuthContext();
|
|
@@ -115429,7 +115851,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115429
115851
|
if (ownerError) return ownerError;
|
|
115430
115852
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115431
115853
|
if (!project) {
|
|
115432
|
-
return
|
|
115854
|
+
return textResponse5(
|
|
115433
115855
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115434
115856
|
);
|
|
115435
115857
|
}
|
|
@@ -115440,7 +115862,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115440
115862
|
if (!member2.ok) return member2.response;
|
|
115441
115863
|
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
115442
115864
|
if (!state2.projectMemberIds.has(member2.member.userId)) {
|
|
115443
|
-
return
|
|
115865
|
+
return textResponse5(
|
|
115444
115866
|
`${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
|
|
115445
115867
|
);
|
|
115446
115868
|
}
|
|
@@ -115456,7 +115878,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115456
115878
|
if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
|
|
115457
115879
|
text3 += "\n\nThis was the member's last project assignment, so their access restrictions were cleared \u2014 they can see all projects in the team again (default behavior).";
|
|
115458
115880
|
}
|
|
115459
|
-
return
|
|
115881
|
+
return textResponse5(text3);
|
|
115460
115882
|
}
|
|
115461
115883
|
async function loadProjectForCleanup(projectId, teamId) {
|
|
115462
115884
|
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
@@ -115484,25 +115906,25 @@ async function countProjectDependencies(projectId) {
|
|
|
115484
115906
|
}
|
|
115485
115907
|
async function handleArchiveProject(input) {
|
|
115486
115908
|
const { projectId, reason } = input;
|
|
115487
|
-
if (!projectId) return
|
|
115909
|
+
if (!projectId) return textResponse5("Error: `projectId` is required.");
|
|
115488
115910
|
const resolved = await resolveTeamId(input.teamId);
|
|
115489
115911
|
if (!resolved.ok) return resolved.response;
|
|
115490
115912
|
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
115491
115913
|
if (!project) {
|
|
115492
|
-
return
|
|
115914
|
+
return textResponse5(
|
|
115493
115915
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115494
115916
|
);
|
|
115495
115917
|
}
|
|
115496
115918
|
const state2 = getProjectArchiveState(project.settings);
|
|
115497
115919
|
if (state2.archived) {
|
|
115498
|
-
return
|
|
115920
|
+
return textResponse5(
|
|
115499
115921
|
`Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
|
|
115500
115922
|
);
|
|
115501
115923
|
}
|
|
115502
115924
|
const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
115503
115925
|
const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
|
|
115504
115926
|
await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
|
|
115505
|
-
return
|
|
115927
|
+
return textResponse5(
|
|
115506
115928
|
`\u2705 **Project archived**
|
|
115507
115929
|
|
|
115508
115930
|
Project: ${project.name}
|
|
@@ -115520,21 +115942,21 @@ Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashbo
|
|
|
115520
115942
|
async function handleDeleteProject(input) {
|
|
115521
115943
|
const ctx = getAuthContext();
|
|
115522
115944
|
const { projectId, confirmEmptyOnly } = input;
|
|
115523
|
-
if (!projectId) return
|
|
115945
|
+
if (!projectId) return textResponse5("Error: `projectId` is required.");
|
|
115524
115946
|
const resolved = await resolveTeamId(input.teamId);
|
|
115525
115947
|
if (!resolved.ok) return resolved.response;
|
|
115526
115948
|
const ownerError = await requireTeamOwner2(resolved.teamId, ctx.userId);
|
|
115527
115949
|
if (ownerError) return ownerError;
|
|
115528
115950
|
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
115529
115951
|
if (!project) {
|
|
115530
|
-
return
|
|
115952
|
+
return textResponse5(
|
|
115531
115953
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115532
115954
|
);
|
|
115533
115955
|
}
|
|
115534
115956
|
const deps = await countProjectDependencies(project.id);
|
|
115535
115957
|
const summary = formatProjectDependencies(deps);
|
|
115536
115958
|
if (!isProjectEmpty(deps)) {
|
|
115537
|
-
return
|
|
115959
|
+
return textResponse5(
|
|
115538
115960
|
`\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
|
|
115539
115961
|
|
|
115540
115962
|
Dependencies: ${summary}.
|
|
@@ -115543,12 +115965,12 @@ A hard delete would orphan these records, so it is not allowed. Use archive-proj
|
|
|
115543
115965
|
);
|
|
115544
115966
|
}
|
|
115545
115967
|
if (confirmEmptyOnly !== true) {
|
|
115546
|
-
return
|
|
115968
|
+
return textResponse5(
|
|
115547
115969
|
`Project "${project.name}" (${project.id}) has no dependencies and can be safely deleted. This is a permanent hard delete. Re-run delete-project with confirmEmptyOnly: true to proceed (or use archive-project to keep the record).`
|
|
115548
115970
|
);
|
|
115549
115971
|
}
|
|
115550
115972
|
await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
|
|
115551
|
-
return
|
|
115973
|
+
return textResponse5(
|
|
115552
115974
|
`\u2705 **Project deleted**
|
|
115553
115975
|
|
|
115554
115976
|
Project: ${project.name}
|
|
@@ -115601,7 +116023,7 @@ var PRODUCT_COLUMNS2 = {
|
|
|
115601
116023
|
createdAt: schema_exports.invoiceProducts.createdAt,
|
|
115602
116024
|
updatedAt: schema_exports.invoiceProducts.updatedAt
|
|
115603
116025
|
};
|
|
115604
|
-
function
|
|
116026
|
+
function textResponse6(text3) {
|
|
115605
116027
|
return { content: [{ type: "text", text: text3 }] };
|
|
115606
116028
|
}
|
|
115607
116029
|
function formatPrice(p3) {
|
|
@@ -115647,14 +116069,14 @@ async function handleGetProducts(input) {
|
|
|
115647
116069
|
const { q: q3, currency, pageSize = 20 } = input;
|
|
115648
116070
|
const status = input.status ?? "active";
|
|
115649
116071
|
if (!PRODUCT_STATUSES.includes(status)) {
|
|
115650
|
-
return
|
|
116072
|
+
return textResponse6(
|
|
115651
116073
|
`Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
|
|
115652
116074
|
);
|
|
115653
116075
|
}
|
|
115654
116076
|
const scope = await resolveTeamScope(input.teamId);
|
|
115655
116077
|
if (!scope.ok) return scope.response;
|
|
115656
116078
|
if (scope.teamIds.length === 0) {
|
|
115657
|
-
return
|
|
116079
|
+
return textResponse6("No accessible teams found.");
|
|
115658
116080
|
}
|
|
115659
116081
|
const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
|
|
115660
116082
|
if (status === "active") {
|
|
@@ -115677,11 +116099,11 @@ async function handleGetProducts(input) {
|
|
|
115677
116099
|
asc(schema_exports.invoiceProducts.name)
|
|
115678
116100
|
).limit(Math.min(pageSize, 100));
|
|
115679
116101
|
if (rows.length === 0) {
|
|
115680
|
-
return
|
|
116102
|
+
return textResponse6(
|
|
115681
116103
|
`No products found${status !== "all" ? ` (status: ${status})` : ""}.`
|
|
115682
116104
|
);
|
|
115683
116105
|
}
|
|
115684
|
-
return
|
|
116106
|
+
return textResponse6(
|
|
115685
116107
|
`Found ${rows.length} product(s):
|
|
115686
116108
|
|
|
115687
116109
|
${rows.map(formatProduct).join("\n")}`
|
|
@@ -115689,11 +116111,11 @@ ${rows.map(formatProduct).join("\n")}`
|
|
|
115689
116111
|
}
|
|
115690
116112
|
async function handleGetProductById(input) {
|
|
115691
116113
|
const { productId } = input;
|
|
115692
|
-
if (!productId) return
|
|
116114
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115693
116115
|
const scope = await resolveTeamScope(input.teamId);
|
|
115694
116116
|
if (!scope.ok) return scope.response;
|
|
115695
116117
|
if (scope.teamIds.length === 0) {
|
|
115696
|
-
return
|
|
116118
|
+
return textResponse6("No accessible teams found.");
|
|
115697
116119
|
}
|
|
115698
116120
|
const [row] = await db.select(PRODUCT_COLUMNS2).from(schema_exports.invoiceProducts).where(
|
|
115699
116121
|
and(
|
|
@@ -115702,11 +116124,11 @@ async function handleGetProductById(input) {
|
|
|
115702
116124
|
)
|
|
115703
116125
|
).limit(1);
|
|
115704
116126
|
if (!row) {
|
|
115705
|
-
return
|
|
116127
|
+
return textResponse6(
|
|
115706
116128
|
`Product ${productId} not found or you don't have access to it.`
|
|
115707
116129
|
);
|
|
115708
116130
|
}
|
|
115709
|
-
return
|
|
116131
|
+
return textResponse6(formatProduct(row));
|
|
115710
116132
|
}
|
|
115711
116133
|
async function loadProductInTeam(productId, teamId) {
|
|
115712
116134
|
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
@@ -115721,10 +116143,10 @@ async function loadProductInTeam(productId, teamId) {
|
|
|
115721
116143
|
async function handleCreateProduct(input) {
|
|
115722
116144
|
const { name: name21, description, price, currency, unit } = input;
|
|
115723
116145
|
if (!name21 || name21.trim().length === 0) {
|
|
115724
|
-
return
|
|
116146
|
+
return textResponse6("Error: `name` is required.");
|
|
115725
116147
|
}
|
|
115726
116148
|
const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
|
|
115727
|
-
if (enumError) return
|
|
116149
|
+
if (enumError) return textResponse6(enumError);
|
|
115728
116150
|
const resolved = await resolveTeamId(input.teamId);
|
|
115729
116151
|
if (!resolved.ok) return resolved.response;
|
|
115730
116152
|
const [created] = await db.insert(schema_exports.invoiceProducts).values({
|
|
@@ -115744,8 +116166,8 @@ async function handleCreateProduct(input) {
|
|
|
115744
116166
|
isActive: true,
|
|
115745
116167
|
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
115746
116168
|
}).returning(PRODUCT_COLUMNS2);
|
|
115747
|
-
if (!created) return
|
|
115748
|
-
return
|
|
116169
|
+
if (!created) return textResponse6("Failed to create product.");
|
|
116170
|
+
return textResponse6(
|
|
115749
116171
|
`\u2705 **Product created**
|
|
115750
116172
|
|
|
115751
116173
|
${formatProduct(created)}`
|
|
@@ -115753,21 +116175,21 @@ ${formatProduct(created)}`
|
|
|
115753
116175
|
}
|
|
115754
116176
|
async function handleUpdateProduct(input) {
|
|
115755
116177
|
const { productId } = input;
|
|
115756
|
-
if (!productId) return
|
|
116178
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115757
116179
|
const resolved = await resolveTeamId(input.teamId);
|
|
115758
116180
|
if (!resolved.ok) return resolved.response;
|
|
115759
116181
|
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
115760
116182
|
if (!existing) {
|
|
115761
|
-
return
|
|
116183
|
+
return textResponse6(
|
|
115762
116184
|
`Product ${productId} not found, or it is not owned by this team.`
|
|
115763
116185
|
);
|
|
115764
116186
|
}
|
|
115765
116187
|
const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
|
|
115766
|
-
if (enumError) return
|
|
116188
|
+
if (enumError) return textResponse6(enumError);
|
|
115767
116189
|
const updates = {};
|
|
115768
116190
|
if (input.name !== void 0) {
|
|
115769
116191
|
if (!input.name || input.name.trim().length === 0) {
|
|
115770
|
-
return
|
|
116192
|
+
return textResponse6("Error: `name` cannot be empty.");
|
|
115771
116193
|
}
|
|
115772
116194
|
updates.name = input.name.trim();
|
|
115773
116195
|
}
|
|
@@ -115788,14 +116210,14 @@ async function handleUpdateProduct(input) {
|
|
|
115788
116210
|
updates.clause = serializeProductClause(input.clause);
|
|
115789
116211
|
}
|
|
115790
116212
|
if (Object.keys(updates).length === 0) {
|
|
115791
|
-
return
|
|
116213
|
+
return textResponse6(
|
|
115792
116214
|
"No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder, clause."
|
|
115793
116215
|
);
|
|
115794
116216
|
}
|
|
115795
116217
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
115796
116218
|
const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS2);
|
|
115797
|
-
if (!updated) return
|
|
115798
|
-
return
|
|
116219
|
+
if (!updated) return textResponse6(`Failed to update product ${productId}.`);
|
|
116220
|
+
return textResponse6(
|
|
115799
116221
|
`\u2705 **Product updated**
|
|
115800
116222
|
|
|
115801
116223
|
${formatProduct(updated)}
|
|
@@ -115804,23 +116226,23 @@ Note: this only affects future invoices/quotes. Existing documents keep their li
|
|
|
115804
116226
|
}
|
|
115805
116227
|
async function handleArchiveProduct(input) {
|
|
115806
116228
|
const { productId, reason } = input;
|
|
115807
|
-
if (!productId) return
|
|
116229
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115808
116230
|
const resolved = await resolveTeamId(input.teamId);
|
|
115809
116231
|
if (!resolved.ok) return resolved.response;
|
|
115810
116232
|
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
115811
116233
|
if (!existing) {
|
|
115812
|
-
return
|
|
116234
|
+
return textResponse6(
|
|
115813
116235
|
`Product ${productId} not found, or it is not owned by this team.`
|
|
115814
116236
|
);
|
|
115815
116237
|
}
|
|
115816
116238
|
if (!existing.isActive) {
|
|
115817
|
-
return
|
|
116239
|
+
return textResponse6(
|
|
115818
116240
|
`Product "${existing.name}" (${existing.id}) is already archived.`
|
|
115819
116241
|
);
|
|
115820
116242
|
}
|
|
115821
116243
|
const [archived] = await db.update(schema_exports.invoiceProducts).set({ isActive: false, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS2);
|
|
115822
|
-
if (!archived) return
|
|
115823
|
-
return
|
|
116244
|
+
if (!archived) return textResponse6(`Failed to archive product ${productId}.`);
|
|
116245
|
+
return textResponse6(
|
|
115824
116246
|
`\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
|
|
115825
116247
|
|
|
115826
116248
|
${formatProduct(archived)}${reason ? `Reason: ${reason}
|
|
@@ -116870,7 +117292,7 @@ var QUOTE_STATUSES = [
|
|
|
116870
117292
|
"expired"
|
|
116871
117293
|
];
|
|
116872
117294
|
var SAFE_DRAFT_STATUSES = /* @__PURE__ */ new Set(["draft"]);
|
|
116873
|
-
function
|
|
117295
|
+
function textResponse7(text3) {
|
|
116874
117296
|
return { content: [{ type: "text", text: text3 }] };
|
|
116875
117297
|
}
|
|
116876
117298
|
async function loadTemplateDefaults(teamId) {
|
|
@@ -117040,14 +117462,14 @@ function tiptapNote2(text3) {
|
|
|
117040
117462
|
async function handleGetQuotes(input) {
|
|
117041
117463
|
const { customerId, status, q: q3, pageSize = 20 } = input;
|
|
117042
117464
|
if (status && !QUOTE_STATUSES.includes(status)) {
|
|
117043
|
-
return
|
|
117465
|
+
return textResponse7(
|
|
117044
117466
|
`Error: invalid status "${status}". Allowed: ${QUOTE_STATUSES.join(", ")}.`
|
|
117045
117467
|
);
|
|
117046
117468
|
}
|
|
117047
117469
|
const scope = await resolveTeamScope(input.teamId);
|
|
117048
117470
|
if (!scope.ok) return scope.response;
|
|
117049
117471
|
if (scope.teamIds.length === 0) {
|
|
117050
|
-
return
|
|
117472
|
+
return textResponse7("No accessible teams found.");
|
|
117051
117473
|
}
|
|
117052
117474
|
const filters = [inArray(schema_exports.quotations.teamId, scope.teamIds)];
|
|
117053
117475
|
if (customerId) filters.push(eq(schema_exports.quotations.customerId, customerId));
|
|
@@ -117062,10 +117484,10 @@ async function handleGetQuotes(input) {
|
|
|
117062
117484
|
}
|
|
117063
117485
|
const rows = await db.select(QUOTE_COLUMNS).from(schema_exports.quotations).where(and(...filters)).orderBy(desc(schema_exports.quotations.createdAt)).limit(Math.min(pageSize, 100));
|
|
117064
117486
|
if (rows.length === 0) {
|
|
117065
|
-
return
|
|
117487
|
+
return textResponse7("No quotes found.");
|
|
117066
117488
|
}
|
|
117067
117489
|
const note = input.projectId ? "\nNote: `projectId` was ignored \u2014 quotations are not linked to projects." : "";
|
|
117068
|
-
return
|
|
117490
|
+
return textResponse7(
|
|
117069
117491
|
`Found ${rows.length} quote(s):
|
|
117070
117492
|
|
|
117071
117493
|
${rows.map(formatQuote).join("\n")}${note}`
|
|
@@ -117073,10 +117495,10 @@ ${rows.map(formatQuote).join("\n")}${note}`
|
|
|
117073
117495
|
}
|
|
117074
117496
|
async function handleCreateQuote(input) {
|
|
117075
117497
|
const { customerId } = input;
|
|
117076
|
-
if (!customerId) return
|
|
117498
|
+
if (!customerId) return textResponse7("Error: `customerId` is required.");
|
|
117077
117499
|
const status = input.status ?? "draft";
|
|
117078
117500
|
if (!SAFE_DRAFT_STATUSES.has(status)) {
|
|
117079
|
-
return
|
|
117501
|
+
return textResponse7(
|
|
117080
117502
|
`Error: this tool only creates draft quotes. Requested status "${status}" is not allowed. Sending/accepting a quote is a manual dashboard action.`
|
|
117081
117503
|
);
|
|
117082
117504
|
}
|
|
@@ -117099,7 +117521,7 @@ async function handleCreateQuote(input) {
|
|
|
117099
117521
|
)
|
|
117100
117522
|
).limit(1);
|
|
117101
117523
|
if (!customer) {
|
|
117102
|
-
return
|
|
117524
|
+
return textResponse7(
|
|
117103
117525
|
`Customer ${customerId} not found or not owned by this team.`
|
|
117104
117526
|
);
|
|
117105
117527
|
}
|
|
@@ -117109,7 +117531,7 @@ async function handleCreateQuote(input) {
|
|
|
117109
117531
|
defaults,
|
|
117110
117532
|
teamId
|
|
117111
117533
|
);
|
|
117112
|
-
if (error49) return
|
|
117534
|
+
if (error49) return textResponse7(`Error: ${error49}`);
|
|
117113
117535
|
const totals = computeTotals(items, defaults);
|
|
117114
117536
|
const quotationNumber = await nextQuotationNumber(teamId);
|
|
117115
117537
|
const template = buildQuoteTemplate(defaults, input.title);
|
|
@@ -117184,8 +117606,8 @@ async function handleCreateQuote(input) {
|
|
|
117184
117606
|
tax: totals.tax,
|
|
117185
117607
|
amount: totals.amount
|
|
117186
117608
|
}).returning(QUOTE_COLUMNS);
|
|
117187
|
-
if (!created) return
|
|
117188
|
-
return
|
|
117609
|
+
if (!created) return textResponse7("Failed to create quote.");
|
|
117610
|
+
return textResponse7(
|
|
117189
117611
|
`\u2705 **Draft quote created**
|
|
117190
117612
|
|
|
117191
117613
|
${formatQuote(created)}
|
|
@@ -117203,15 +117625,15 @@ async function loadQuoteInTeam(id, teamId) {
|
|
|
117203
117625
|
return row ?? null;
|
|
117204
117626
|
}
|
|
117205
117627
|
function notDraftResponse2(quote) {
|
|
117206
|
-
return
|
|
117628
|
+
return textResponse7(
|
|
117207
117629
|
`Quote ${quote.quotationNumber ?? quote.id} has status "${quote.status}", not "draft". These tools only modify draft quotes \u2014 sent/accepted/rejected/expired quotes are immutable here so their product snapshots stay reproducible.`
|
|
117208
117630
|
);
|
|
117209
117631
|
}
|
|
117210
117632
|
async function handleUpdateQuote(input) {
|
|
117211
117633
|
const { id } = input;
|
|
117212
|
-
if (!id) return
|
|
117634
|
+
if (!id) return textResponse7("Error: `id` is required.");
|
|
117213
117635
|
if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
|
|
117214
|
-
return
|
|
117636
|
+
return textResponse7(
|
|
117215
117637
|
`Error: status can only stay within {${[...SAFE_DRAFT_STATUSES].join(", ")}}. "${input.status}" (send/accept/reject/expire) must be done manually from the dashboard.`
|
|
117216
117638
|
);
|
|
117217
117639
|
}
|
|
@@ -117219,7 +117641,7 @@ async function handleUpdateQuote(input) {
|
|
|
117219
117641
|
if (!resolved.ok) return resolved.response;
|
|
117220
117642
|
const quote = await loadQuoteInTeam(id, resolved.teamId);
|
|
117221
117643
|
if (!quote) {
|
|
117222
|
-
return
|
|
117644
|
+
return textResponse7(`Quote ${id} not found or not owned by this team.`);
|
|
117223
117645
|
}
|
|
117224
117646
|
if (quote.status !== "draft") return notDraftResponse2(quote);
|
|
117225
117647
|
const defaults = templateDefaultsFromStored(quote.template, quote.currency);
|
|
@@ -117239,7 +117661,7 @@ async function handleUpdateQuote(input) {
|
|
|
117239
117661
|
defaults,
|
|
117240
117662
|
quote.teamId
|
|
117241
117663
|
);
|
|
117242
|
-
if (error49) return
|
|
117664
|
+
if (error49) return textResponse7(`Error: ${error49}`);
|
|
117243
117665
|
const totals = computeTotals(items, defaults);
|
|
117244
117666
|
updates.lineItems = items;
|
|
117245
117667
|
updates.subtotal = totals.subtotal;
|
|
@@ -117251,32 +117673,32 @@ async function handleUpdateQuote(input) {
|
|
|
117251
117673
|
});
|
|
117252
117674
|
}
|
|
117253
117675
|
if (Object.keys(updates).length === 0) {
|
|
117254
|
-
return
|
|
117676
|
+
return textResponse7(
|
|
117255
117677
|
"No fields to update. Provide at least one of: title, description, validUntil, lineItems."
|
|
117256
117678
|
);
|
|
117257
117679
|
}
|
|
117258
117680
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
117259
117681
|
const [updated] = await db.update(schema_exports.quotations).set(updates).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
|
|
117260
|
-
if (!updated) return
|
|
117261
|
-
return
|
|
117682
|
+
if (!updated) return textResponse7(`Failed to update quote ${id}.`);
|
|
117683
|
+
return textResponse7(`\u2705 **Draft quote updated**
|
|
117262
117684
|
|
|
117263
117685
|
${formatQuote(updated)}`);
|
|
117264
117686
|
}
|
|
117265
117687
|
async function handleAddProductToQuote(input) {
|
|
117266
117688
|
const { quoteId, productId } = input;
|
|
117267
|
-
if (!quoteId) return
|
|
117268
|
-
if (!productId) return
|
|
117689
|
+
if (!quoteId) return textResponse7("Error: `quoteId` is required.");
|
|
117690
|
+
if (!productId) return textResponse7("Error: `productId` is required.");
|
|
117269
117691
|
const resolved = await resolveTeamId(input.teamId);
|
|
117270
117692
|
if (!resolved.ok) return resolved.response;
|
|
117271
117693
|
const quote = await loadQuoteInTeam(quoteId, resolved.teamId);
|
|
117272
117694
|
if (!quote) {
|
|
117273
|
-
return
|
|
117695
|
+
return textResponse7(`Quote ${quoteId} not found or not owned by this team.`);
|
|
117274
117696
|
}
|
|
117275
117697
|
if (quote.status !== "draft") return notDraftResponse2(quote);
|
|
117276
117698
|
const products = await loadProductsInTeam2([productId], quote.teamId);
|
|
117277
117699
|
const product = products.get(productId);
|
|
117278
117700
|
if (!product) {
|
|
117279
|
-
return
|
|
117701
|
+
return textResponse7(
|
|
117280
117702
|
`Product ${productId} not found or not owned by this team.`
|
|
117281
117703
|
);
|
|
117282
117704
|
}
|
|
@@ -117306,7 +117728,7 @@ async function handleAddProductToQuote(input) {
|
|
|
117306
117728
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
117307
117729
|
}).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
|
|
117308
117730
|
if (!updated) {
|
|
117309
|
-
return
|
|
117731
|
+
return textResponse7(`Failed to add product to quote ${quoteId}.`);
|
|
117310
117732
|
}
|
|
117311
117733
|
await db.update(schema_exports.invoiceProducts).set({
|
|
117312
117734
|
usageCount: sql`${schema_exports.invoiceProducts.usageCount} + 1`,
|
|
@@ -117322,7 +117744,7 @@ async function handleAddProductToQuote(input) {
|
|
|
117322
117744
|
if (meta5.includedItems && meta5.includedItems.length > 0) {
|
|
117323
117745
|
metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
|
|
117324
117746
|
}
|
|
117325
|
-
return
|
|
117747
|
+
return textResponse7(
|
|
117326
117748
|
`\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
|
|
117327
117749
|
|
|
117328
117750
|
Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
|
|
@@ -123057,7 +123479,7 @@ function formatDeleteAttachmentRefusal(reason, context2) {
|
|
|
123057
123479
|
}
|
|
123058
123480
|
|
|
123059
123481
|
// src/tools/ticket-attachments.ts
|
|
123060
|
-
function
|
|
123482
|
+
function textResponse8(text3) {
|
|
123061
123483
|
return { content: [{ type: "text", text: text3 }] };
|
|
123062
123484
|
}
|
|
123063
123485
|
async function findAttachment(attachmentId) {
|
|
@@ -123130,7 +123552,7 @@ ${url3}`
|
|
|
123130
123552
|
async function handleUploadTicketAttachment(input) {
|
|
123131
123553
|
const ctx = getAuthContext() ?? authContext;
|
|
123132
123554
|
if (!ctx) {
|
|
123133
|
-
return
|
|
123555
|
+
return textResponse8("Error: Not authenticated.");
|
|
123134
123556
|
}
|
|
123135
123557
|
const access = await loadAccessibleTicket(input.teamId, input.ticketId);
|
|
123136
123558
|
if (!access.ok) return access.response;
|
|
@@ -123146,12 +123568,12 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123146
123568
|
userId: ctx.userId
|
|
123147
123569
|
});
|
|
123148
123570
|
if (!resolved.ok) {
|
|
123149
|
-
return
|
|
123571
|
+
return textResponse8(resolved.message);
|
|
123150
123572
|
}
|
|
123151
123573
|
const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
|
|
123152
123574
|
const validationError = validateAttachmentBuffer(buffer2, mimeType);
|
|
123153
123575
|
if (validationError) {
|
|
123154
|
-
return
|
|
123576
|
+
return textResponse8(validationError.message);
|
|
123155
123577
|
}
|
|
123156
123578
|
const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
|
|
123157
123579
|
try {
|
|
@@ -123162,7 +123584,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123162
123584
|
options: { contentType: mimeType, upsert: true }
|
|
123163
123585
|
});
|
|
123164
123586
|
} catch (error49) {
|
|
123165
|
-
return
|
|
123587
|
+
return textResponse8(
|
|
123166
123588
|
`Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
123167
123589
|
);
|
|
123168
123590
|
}
|
|
@@ -123191,7 +123613,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123191
123613
|
url3 = signed.url;
|
|
123192
123614
|
} catch {
|
|
123193
123615
|
}
|
|
123194
|
-
return
|
|
123616
|
+
return textResponse8(
|
|
123195
123617
|
`\u{1F4CE} **Attached to ${ticket.ticketNumber}**
|
|
123196
123618
|
File: ${fileName}
|
|
123197
123619
|
Type: ${mimeType}
|
|
@@ -123205,18 +123627,18 @@ ${url3}` : "")
|
|
|
123205
123627
|
async function handleDeleteTicketAttachment(input) {
|
|
123206
123628
|
const ctx = getAuthContext() ?? authContext;
|
|
123207
123629
|
if (!ctx) {
|
|
123208
|
-
return
|
|
123630
|
+
return textResponse8("Error: Not authenticated.");
|
|
123209
123631
|
}
|
|
123210
123632
|
const inputError = validateDeleteAttachmentInput(input.attachmentId);
|
|
123211
123633
|
if (inputError) {
|
|
123212
|
-
return
|
|
123634
|
+
return textResponse8(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
|
|
123213
123635
|
}
|
|
123214
123636
|
const access = await loadAccessibleTicket(input.teamId, input.ticketId);
|
|
123215
123637
|
if (!access.ok) return access.response;
|
|
123216
123638
|
const ticket = access.ticket;
|
|
123217
123639
|
const attachment = await findAttachment(input.attachmentId);
|
|
123218
123640
|
if (!attachment) {
|
|
123219
|
-
return
|
|
123641
|
+
return textResponse8(
|
|
123220
123642
|
formatDeleteAttachmentRefusal("attachment_not_found", {
|
|
123221
123643
|
attachmentId: input.attachmentId,
|
|
123222
123644
|
ticketNumber: ticket.ticketNumber
|
|
@@ -123224,7 +123646,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123224
123646
|
);
|
|
123225
123647
|
}
|
|
123226
123648
|
if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
|
|
123227
|
-
return
|
|
123649
|
+
return textResponse8(
|
|
123228
123650
|
formatDeleteAttachmentRefusal("wrong_ticket", {
|
|
123229
123651
|
attachmentId: input.attachmentId,
|
|
123230
123652
|
ticketNumber: ticket.ticketNumber,
|
|
@@ -123236,7 +123658,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123236
123658
|
const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
|
|
123237
123659
|
const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
|
|
123238
123660
|
if (!deletedRow) {
|
|
123239
|
-
return
|
|
123661
|
+
return textResponse8(
|
|
123240
123662
|
`Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
|
|
123241
123663
|
);
|
|
123242
123664
|
}
|
|
@@ -123266,7 +123688,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123266
123688
|
fileName: attachment.fileName,
|
|
123267
123689
|
source: attachment.source
|
|
123268
123690
|
});
|
|
123269
|
-
return
|
|
123691
|
+
return textResponse8(JSON.stringify(result, null, 2));
|
|
123270
123692
|
}
|
|
123271
123693
|
|
|
123272
123694
|
// src/tools/tiptap-text.ts
|
|
@@ -123683,7 +124105,7 @@ function formatTagUsage(usage) {
|
|
|
123683
124105
|
}
|
|
123684
124106
|
|
|
123685
124107
|
// src/tools/tag-management.ts
|
|
123686
|
-
function
|
|
124108
|
+
function textResponse9(text3) {
|
|
123687
124109
|
return { content: [{ type: "text", text: text3 }] };
|
|
123688
124110
|
}
|
|
123689
124111
|
var TAG_COLUMNS = {
|
|
@@ -123724,24 +124146,24 @@ function scopeFilter(projectId) {
|
|
|
123724
124146
|
return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
|
|
123725
124147
|
}
|
|
123726
124148
|
async function handleUpdateTag(input) {
|
|
123727
|
-
if (!input.tagId) return
|
|
124149
|
+
if (!input.tagId) return textResponse9("Error: `tagId` is required.");
|
|
123728
124150
|
const resolved = await resolveTeamId(input.teamId);
|
|
123729
124151
|
if (!resolved.ok) return resolved.response;
|
|
123730
124152
|
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
123731
124153
|
if (!existing) {
|
|
123732
|
-
return
|
|
124154
|
+
return textResponse9(
|
|
123733
124155
|
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
123734
124156
|
);
|
|
123735
124157
|
}
|
|
123736
124158
|
const renaming = input.name !== void 0;
|
|
123737
124159
|
const rescoping = input.projectId !== void 0;
|
|
123738
124160
|
if (!renaming && !rescoping) {
|
|
123739
|
-
return
|
|
124161
|
+
return textResponse9(
|
|
123740
124162
|
"No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
|
|
123741
124163
|
);
|
|
123742
124164
|
}
|
|
123743
124165
|
if (renaming && !isValidTagName(input.name)) {
|
|
123744
|
-
return
|
|
124166
|
+
return textResponse9("Error: `name` cannot be empty.");
|
|
123745
124167
|
}
|
|
123746
124168
|
const nextName = renaming ? input.name.trim() : existing.name;
|
|
123747
124169
|
const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
|
|
@@ -123754,13 +124176,13 @@ async function handleUpdateTag(input) {
|
|
|
123754
124176
|
)
|
|
123755
124177
|
).limit(1);
|
|
123756
124178
|
if (collision) {
|
|
123757
|
-
return
|
|
124179
|
+
return textResponse9(
|
|
123758
124180
|
`\u274C Cannot update: another tag already uses the name "${collision.name}" (id: ${collision.id}) in this scope. Use merge-tags to combine them instead of renaming.`
|
|
123759
124181
|
);
|
|
123760
124182
|
}
|
|
123761
124183
|
const [updated] = await db.update(schema_exports.tags).set({ name: nextName, projectId: nextProjectId }).where(eq(schema_exports.tags.id, existing.id)).returning(TAG_COLUMNS);
|
|
123762
|
-
if (!updated) return
|
|
123763
|
-
return
|
|
124184
|
+
if (!updated) return textResponse9(`Failed to update tag ${input.tagId}.`);
|
|
124185
|
+
return textResponse9(
|
|
123764
124186
|
`\u2705 **Tag updated**
|
|
123765
124187
|
|
|
123766
124188
|
${describeTag(updated)}
|
|
@@ -123769,34 +124191,34 @@ Existing ticket/customer/project/transaction tag relations are preserved.`
|
|
|
123769
124191
|
);
|
|
123770
124192
|
}
|
|
123771
124193
|
async function handleDeleteTag(input) {
|
|
123772
|
-
if (!input.tagId) return
|
|
124194
|
+
if (!input.tagId) return textResponse9("Error: `tagId` is required.");
|
|
123773
124195
|
const mode = input.mode ?? "delete_if_unused";
|
|
123774
124196
|
const resolved = await resolveTeamId(input.teamId);
|
|
123775
124197
|
if (!resolved.ok) return resolved.response;
|
|
123776
124198
|
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
123777
124199
|
if (!existing) {
|
|
123778
|
-
return
|
|
124200
|
+
return textResponse9(
|
|
123779
124201
|
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
123780
124202
|
);
|
|
123781
124203
|
}
|
|
123782
124204
|
const usage = await getTagUsage(existing.id);
|
|
123783
124205
|
const total = totalTagUsage(usage);
|
|
123784
124206
|
if (mode === "archive") {
|
|
123785
|
-
return
|
|
124207
|
+
return textResponse9(
|
|
123786
124208
|
`\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
|
|
123787
124209
|
|
|
123788
124210
|
Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
|
|
123789
124211
|
);
|
|
123790
124212
|
}
|
|
123791
124213
|
if (total > 0) {
|
|
123792
|
-
return
|
|
124214
|
+
return textResponse9(
|
|
123793
124215
|
`\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
|
|
123794
124216
|
|
|
123795
124217
|
Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
|
|
123796
124218
|
);
|
|
123797
124219
|
}
|
|
123798
124220
|
await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
|
|
123799
|
-
return
|
|
124221
|
+
return textResponse9(
|
|
123800
124222
|
`\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
|
|
123801
124223
|
);
|
|
123802
124224
|
}
|
|
@@ -123806,7 +124228,7 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123806
124228
|
if (!tag2) {
|
|
123807
124229
|
return {
|
|
123808
124230
|
ok: false,
|
|
123809
|
-
response:
|
|
124231
|
+
response: textResponse9(
|
|
123810
124232
|
`Target tag ${input.targetTagId} not found, or it is not owned by this team.`
|
|
123811
124233
|
)
|
|
123812
124234
|
};
|
|
@@ -123816,7 +124238,7 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123816
124238
|
if (!isValidTagName(input.targetName)) {
|
|
123817
124239
|
return {
|
|
123818
124240
|
ok: false,
|
|
123819
|
-
response:
|
|
124241
|
+
response: textResponse9(
|
|
123820
124242
|
"Error: provide either `targetTagId` or a non-empty `targetName`."
|
|
123821
124243
|
)
|
|
123822
124244
|
};
|
|
@@ -123834,14 +124256,14 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123834
124256
|
}
|
|
123835
124257
|
const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
|
|
123836
124258
|
if (!created) {
|
|
123837
|
-
return { ok: false, response:
|
|
124259
|
+
return { ok: false, response: textResponse9("Failed to create target tag.") };
|
|
123838
124260
|
}
|
|
123839
124261
|
return { ok: true, tag: created, created: true };
|
|
123840
124262
|
}
|
|
123841
124263
|
async function handleMergeTags(input) {
|
|
123842
124264
|
const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
|
|
123843
124265
|
if (rawSourceIds.length === 0) {
|
|
123844
|
-
return
|
|
124266
|
+
return textResponse9("Error: `sourceTagIds` must contain at least one tag id.");
|
|
123845
124267
|
}
|
|
123846
124268
|
const resolved = await resolveTeamId(input.teamId);
|
|
123847
124269
|
if (!resolved.ok) return resolved.response;
|
|
@@ -123855,7 +124277,7 @@ async function handleMergeTags(input) {
|
|
|
123855
124277
|
const foundIds = new Set(sourceTags.map((t8) => t8.id));
|
|
123856
124278
|
const missing = rawSourceIds.filter((id) => !foundIds.has(id));
|
|
123857
124279
|
if (missing.length > 0) {
|
|
123858
|
-
return
|
|
124280
|
+
return textResponse9(
|
|
123859
124281
|
`Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
|
|
123860
124282
|
);
|
|
123861
124283
|
}
|
|
@@ -123863,7 +124285,7 @@ async function handleMergeTags(input) {
|
|
|
123863
124285
|
if (!target.ok) return target.response;
|
|
123864
124286
|
const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
|
|
123865
124287
|
if (sourcesToMerge.length === 0) {
|
|
123866
|
-
return
|
|
124288
|
+
return textResponse9(
|
|
123867
124289
|
"Error: nothing to merge \u2014 the only source tag is the same as the target tag."
|
|
123868
124290
|
);
|
|
123869
124291
|
}
|
|
@@ -123960,7 +124382,7 @@ async function handleMergeTags(input) {
|
|
|
123960
124382
|
const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
|
|
123961
124383
|
const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
|
|
123962
124384
|
const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
|
|
123963
|
-
return
|
|
124385
|
+
return textResponse9(
|
|
123964
124386
|
`\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
|
|
123965
124387
|
|
|
123966
124388
|
Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
|
|
@@ -124346,15 +124768,15 @@ function attemptedLockedFields(update) {
|
|
|
124346
124768
|
// src/tools/trips.ts
|
|
124347
124769
|
var TRIP_TYPES = ["private", "business"];
|
|
124348
124770
|
var BILLING_TYPES2 = TRIP_BILLING_TYPES;
|
|
124349
|
-
function
|
|
124771
|
+
function textResponse10(text3) {
|
|
124350
124772
|
return { content: [{ type: "text", text: text3 }] };
|
|
124351
124773
|
}
|
|
124352
|
-
function
|
|
124774
|
+
function jsonResponse2(payload) {
|
|
124353
124775
|
return {
|
|
124354
124776
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
124355
124777
|
};
|
|
124356
124778
|
}
|
|
124357
|
-
function
|
|
124779
|
+
function toNumber3(value) {
|
|
124358
124780
|
if (value == null) return 0;
|
|
124359
124781
|
if (typeof value === "number") return value;
|
|
124360
124782
|
const parsed = Number.parseFloat(String(value));
|
|
@@ -124367,9 +124789,9 @@ function formatTrip(t8) {
|
|
|
124367
124789
|
startLocation: t8.startLocation,
|
|
124368
124790
|
endLocation: t8.endLocation,
|
|
124369
124791
|
tripType: t8.tripType,
|
|
124370
|
-
distance: t8.distance != null ?
|
|
124371
|
-
odometerStart: t8.odometerStart != null ?
|
|
124372
|
-
odometerEnd: t8.odometerEnd != null ?
|
|
124792
|
+
distance: t8.distance != null ? toNumber3(t8.distance) : null,
|
|
124793
|
+
odometerStart: t8.odometerStart != null ? toNumber3(t8.odometerStart) : null,
|
|
124794
|
+
odometerEnd: t8.odometerEnd != null ? toNumber3(t8.odometerEnd) : null,
|
|
124373
124795
|
billingType: t8.billingType,
|
|
124374
124796
|
rate: t8.rate,
|
|
124375
124797
|
amount: t8.amount,
|
|
@@ -124402,19 +124824,19 @@ var TRIP_RELATIONS = {
|
|
|
124402
124824
|
};
|
|
124403
124825
|
async function handleGetTrips(input) {
|
|
124404
124826
|
if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
|
|
124405
|
-
return
|
|
124827
|
+
return textResponse10(
|
|
124406
124828
|
`Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
|
|
124407
124829
|
);
|
|
124408
124830
|
}
|
|
124409
124831
|
if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
|
|
124410
|
-
return
|
|
124832
|
+
return textResponse10(
|
|
124411
124833
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124412
124834
|
);
|
|
124413
124835
|
}
|
|
124414
124836
|
const scope = await resolveTeamScope(input.teamId);
|
|
124415
124837
|
if (!scope.ok) return scope.response;
|
|
124416
124838
|
if (scope.teamIds.length === 0) {
|
|
124417
|
-
return
|
|
124839
|
+
return textResponse10("No accessible teams found.");
|
|
124418
124840
|
}
|
|
124419
124841
|
const filters = [inArray(schema_exports.trips.teamId, scope.teamIds)];
|
|
124420
124842
|
if (input.dateFrom) filters.push(gte(schema_exports.trips.date, input.dateFrom));
|
|
@@ -124446,7 +124868,7 @@ async function handleGetTrips(input) {
|
|
|
124446
124868
|
});
|
|
124447
124869
|
const totals = rows.reduce(
|
|
124448
124870
|
(acc, t8) => {
|
|
124449
|
-
const distance =
|
|
124871
|
+
const distance = toNumber3(t8.distance);
|
|
124450
124872
|
const amount = t8.amount ?? 0;
|
|
124451
124873
|
if (t8.tripType === "business") acc.businessKm += distance;
|
|
124452
124874
|
else acc.privateKm += distance;
|
|
@@ -124456,7 +124878,7 @@ async function handleGetTrips(input) {
|
|
|
124456
124878
|
},
|
|
124457
124879
|
{ businessKm: 0, privateKm: 0, totalKm: 0, totalAmount: 0 }
|
|
124458
124880
|
);
|
|
124459
|
-
return
|
|
124881
|
+
return jsonResponse2({
|
|
124460
124882
|
count: rows.length,
|
|
124461
124883
|
totals: {
|
|
124462
124884
|
businessKm: round23(totals.businessKm),
|
|
@@ -124516,20 +124938,20 @@ async function validateInvoice(invoiceId, teamId) {
|
|
|
124516
124938
|
}
|
|
124517
124939
|
async function handleCreateTrip(input) {
|
|
124518
124940
|
const ctx = getAuthContext();
|
|
124519
|
-
if (!input.date) return
|
|
124941
|
+
if (!input.date) return textResponse10("Error: `date` (YYYY-MM-DD) is required.");
|
|
124520
124942
|
if (!input.startLocation || !input.endLocation) {
|
|
124521
|
-
return
|
|
124943
|
+
return textResponse10(
|
|
124522
124944
|
"Error: `startLocation` and `endLocation` are required."
|
|
124523
124945
|
);
|
|
124524
124946
|
}
|
|
124525
124947
|
if (!input.tripType || !TRIP_TYPES.includes(input.tripType)) {
|
|
124526
|
-
return
|
|
124948
|
+
return textResponse10(
|
|
124527
124949
|
`Error: \`tripType\` is required and must be one of: ${TRIP_TYPES.join(", ")}.`
|
|
124528
124950
|
);
|
|
124529
124951
|
}
|
|
124530
124952
|
const billingType = input.billingType ?? "not_billable";
|
|
124531
124953
|
if (!BILLING_TYPES2.includes(billingType)) {
|
|
124532
|
-
return
|
|
124954
|
+
return textResponse10(
|
|
124533
124955
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124534
124956
|
);
|
|
124535
124957
|
}
|
|
@@ -124541,7 +124963,7 @@ async function handleCreateTrip(input) {
|
|
|
124541
124963
|
customerId: input.customerId,
|
|
124542
124964
|
vehicleId: input.vehicleId
|
|
124543
124965
|
});
|
|
124544
|
-
if (linkError) return
|
|
124966
|
+
if (linkError) return textResponse10(`Error: ${linkError}`);
|
|
124545
124967
|
if (!input.allowDuplicate) {
|
|
124546
124968
|
const dupFilters = [
|
|
124547
124969
|
eq(schema_exports.trips.teamId, teamId),
|
|
@@ -124558,8 +124980,8 @@ async function handleCreateTrip(input) {
|
|
|
124558
124980
|
}
|
|
124559
124981
|
const [dup] = await db.select({ id: schema_exports.trips.id, distance: schema_exports.trips.distance }).from(schema_exports.trips).where(and(...dupFilters)).limit(1);
|
|
124560
124982
|
if (dup) {
|
|
124561
|
-
return
|
|
124562
|
-
`\u26A0\uFE0F A matching trip already exists for ${input.date} (${input.startLocation} \u2192 ${input.endLocation}): trip ${dup.id}${dup.distance != null ? ` (${
|
|
124983
|
+
return textResponse10(
|
|
124984
|
+
`\u26A0\uFE0F A matching trip already exists for ${input.date} (${input.startLocation} \u2192 ${input.endLocation}): trip ${dup.id}${dup.distance != null ? ` (${toNumber3(dup.distance)} km)` : ""}. Not creating a duplicate. Use update-trip to adjust it, or re-call create-trip with allowDuplicate: true to record a second trip anyway.`
|
|
124563
124985
|
);
|
|
124564
124986
|
}
|
|
124565
124987
|
}
|
|
@@ -124588,7 +125010,7 @@ async function handleCreateTrip(input) {
|
|
|
124588
125010
|
vehicleId: input.vehicleId ?? null,
|
|
124589
125011
|
snapshotId: input.snapshotId ?? null
|
|
124590
125012
|
}).returning({ id: schema_exports.trips.id });
|
|
124591
|
-
if (!created) return
|
|
125013
|
+
if (!created) return textResponse10("Failed to create trip.");
|
|
124592
125014
|
const trip = await loadTripInTeams(created.id, [teamId]);
|
|
124593
125015
|
return {
|
|
124594
125016
|
content: [
|
|
@@ -124603,14 +125025,14 @@ ${JSON.stringify(formatTrip(trip), null, 2)}`
|
|
|
124603
125025
|
}
|
|
124604
125026
|
async function handleUpdateTrip(input) {
|
|
124605
125027
|
const ctx = getAuthContext();
|
|
124606
|
-
if (!input.id) return
|
|
125028
|
+
if (!input.id) return textResponse10("Error: `id` is required.");
|
|
124607
125029
|
if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
|
|
124608
|
-
return
|
|
125030
|
+
return textResponse10(
|
|
124609
125031
|
`Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
|
|
124610
125032
|
);
|
|
124611
125033
|
}
|
|
124612
125034
|
if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
|
|
124613
|
-
return
|
|
125035
|
+
return textResponse10(
|
|
124614
125036
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124615
125037
|
);
|
|
124616
125038
|
}
|
|
@@ -124619,7 +125041,7 @@ async function handleUpdateTrip(input) {
|
|
|
124619
125041
|
const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
|
|
124620
125042
|
const existing = await loadTripInTeams(input.id, accessibleTeamIds);
|
|
124621
125043
|
if (!existing) {
|
|
124622
|
-
return
|
|
125044
|
+
return textResponse10(
|
|
124623
125045
|
`Trip ${input.id} not found or you don't have access to it. Call get-trips to find a valid id.`
|
|
124624
125046
|
);
|
|
124625
125047
|
}
|
|
@@ -124630,7 +125052,7 @@ async function handleUpdateTrip(input) {
|
|
|
124630
125052
|
input
|
|
124631
125053
|
);
|
|
124632
125054
|
if (attempted.length > 0) {
|
|
124633
|
-
return
|
|
125055
|
+
return textResponse10(
|
|
124634
125056
|
`Error: trip ${input.id} is invoiced${existing.invoiceId ? ` (invoice ${existing.invoiceId})` : ""}. Financial/distance fields are locked: ${attempted.join(", ")}. Re-call with allowInvoicedOverride: true to change them anyway, or only update project/customer/notes/vehicle links.`
|
|
124635
125057
|
);
|
|
124636
125058
|
}
|
|
@@ -124640,10 +125062,10 @@ async function handleUpdateTrip(input) {
|
|
|
124640
125062
|
customerId: input.customerId ?? void 0,
|
|
124641
125063
|
vehicleId: input.vehicleId ?? void 0
|
|
124642
125064
|
});
|
|
124643
|
-
if (linkError) return
|
|
125065
|
+
if (linkError) return textResponse10(`Error: ${linkError}`);
|
|
124644
125066
|
if (input.invoiceId) {
|
|
124645
125067
|
const invoiceError = await validateInvoice(input.invoiceId, teamId);
|
|
124646
|
-
if (invoiceError) return
|
|
125068
|
+
if (invoiceError) return textResponse10(`Error: ${invoiceError}`);
|
|
124647
125069
|
}
|
|
124648
125070
|
const updates = {};
|
|
124649
125071
|
if (input.date !== void 0) updates.date = input.date;
|
|
@@ -124680,7 +125102,7 @@ async function handleUpdateTrip(input) {
|
|
|
124680
125102
|
if (input.isInvoiced !== void 0) updates.isInvoiced = input.isInvoiced;
|
|
124681
125103
|
if (input.amount === void 0 && (input.distance !== void 0 || input.rate !== void 0 || input.billingType !== void 0)) {
|
|
124682
125104
|
const nextBilling = input.billingType ?? existing.billingType;
|
|
124683
|
-
const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ?
|
|
125105
|
+
const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber3(existing.distance) : null;
|
|
124684
125106
|
const nextRate = input.rate !== void 0 ? input.rate : existing.rate;
|
|
124685
125107
|
const derived = deriveTripAmount({
|
|
124686
125108
|
billingType: nextBilling,
|
|
@@ -124691,7 +125113,7 @@ async function handleUpdateTrip(input) {
|
|
|
124691
125113
|
if (derived != null) updates.amount = derived;
|
|
124692
125114
|
}
|
|
124693
125115
|
if (Object.keys(updates).length === 0) {
|
|
124694
|
-
return
|
|
125116
|
+
return textResponse10(
|
|
124695
125117
|
"No fields to update. Provide at least one editable field."
|
|
124696
125118
|
);
|
|
124697
125119
|
}
|
|
@@ -124718,7 +125140,7 @@ async function handleGetVehicles(input) {
|
|
|
124718
125140
|
const scope = await resolveTeamScope(input.teamId);
|
|
124719
125141
|
if (!scope.ok) return scope.response;
|
|
124720
125142
|
if (scope.teamIds.length === 0) {
|
|
124721
|
-
return
|
|
125143
|
+
return textResponse10("No accessible teams found.");
|
|
124722
125144
|
}
|
|
124723
125145
|
const filters = [inArray(schema_exports.vehicles.teamId, scope.teamIds)];
|
|
124724
125146
|
if (input.q) filters.push(ilike(schema_exports.vehicles.name, `%${input.q}%`));
|
|
@@ -124729,13 +125151,13 @@ async function handleGetVehicles(input) {
|
|
|
124729
125151
|
currentOdometer: schema_exports.vehicles.currentOdometer,
|
|
124730
125152
|
teamId: schema_exports.vehicles.teamId
|
|
124731
125153
|
}).from(schema_exports.vehicles).where(and(...filters)).orderBy(asc(schema_exports.vehicles.name)).limit(Math.min(input.pageSize ?? 50, 200));
|
|
124732
|
-
return
|
|
125154
|
+
return jsonResponse2({
|
|
124733
125155
|
count: rows.length,
|
|
124734
125156
|
vehicles: rows.map((v2) => ({
|
|
124735
125157
|
id: v2.id,
|
|
124736
125158
|
name: v2.name,
|
|
124737
125159
|
licensePlate: v2.licensePlate,
|
|
124738
|
-
currentOdometer: v2.currentOdometer != null ?
|
|
125160
|
+
currentOdometer: v2.currentOdometer != null ? toNumber3(v2.currentOdometer) : null
|
|
124739
125161
|
}))
|
|
124740
125162
|
});
|
|
124741
125163
|
}
|
|
@@ -124744,7 +125166,7 @@ async function handleGetTripTemplates(input) {
|
|
|
124744
125166
|
const scope = await resolveTeamScope(input.teamId);
|
|
124745
125167
|
if (!scope.ok) return scope.response;
|
|
124746
125168
|
if (scope.teamIds.length === 0) {
|
|
124747
|
-
return
|
|
125169
|
+
return textResponse10("No accessible teams found.");
|
|
124748
125170
|
}
|
|
124749
125171
|
const filters = [inArray(schema_exports.tripTemplates.teamId, scope.teamIds)];
|
|
124750
125172
|
const userId = input.userId ?? ctx.userId;
|
|
@@ -124768,26 +125190,26 @@ async function handleGetTripTemplates(input) {
|
|
|
124768
125190
|
vehicleId: schema_exports.tripTemplates.vehicleId,
|
|
124769
125191
|
notes: schema_exports.tripTemplates.notes
|
|
124770
125192
|
}).from(schema_exports.tripTemplates).where(and(...filters)).orderBy(asc(schema_exports.tripTemplates.name)).limit(Math.min(input.pageSize ?? 50, 200));
|
|
124771
|
-
return
|
|
125193
|
+
return jsonResponse2({
|
|
124772
125194
|
count: rows.length,
|
|
124773
125195
|
templates: rows.map((t8) => ({
|
|
124774
125196
|
...t8,
|
|
124775
|
-
distance: t8.distance != null ?
|
|
124776
|
-
returnDistance: t8.returnDistance != null ?
|
|
125197
|
+
distance: t8.distance != null ? toNumber3(t8.distance) : null,
|
|
125198
|
+
returnDistance: t8.returnDistance != null ? toNumber3(t8.returnDistance) : null
|
|
124777
125199
|
}))
|
|
124778
125200
|
});
|
|
124779
125201
|
}
|
|
124780
125202
|
async function handleGetFrequentTripsForProject(input) {
|
|
124781
125203
|
const ctx = getAuthContext();
|
|
124782
125204
|
if (!input.projectId) {
|
|
124783
|
-
return
|
|
125205
|
+
return textResponse10("Error: `projectId` is required.");
|
|
124784
125206
|
}
|
|
124785
125207
|
const resolved = await resolveTeamId(input.teamId);
|
|
124786
125208
|
if (!resolved.ok) return resolved.response;
|
|
124787
125209
|
const teamId = resolved.teamId;
|
|
124788
125210
|
const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
|
|
124789
125211
|
if (!projectIds.includes(input.projectId)) {
|
|
124790
|
-
return
|
|
125212
|
+
return textResponse10(
|
|
124791
125213
|
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
124792
125214
|
);
|
|
124793
125215
|
}
|
|
@@ -124814,7 +125236,7 @@ async function handleGetFrequentTripsForProject(input) {
|
|
|
124814
125236
|
schema_exports.trips.endLocation,
|
|
124815
125237
|
schema_exports.trips.tripType
|
|
124816
125238
|
).orderBy(desc(sql`count(*)`), desc(sql`max(${schema_exports.trips.date})`)).limit(limitN);
|
|
124817
|
-
return
|
|
125239
|
+
return jsonResponse2({
|
|
124818
125240
|
count: groups.length,
|
|
124819
125241
|
daysBack,
|
|
124820
125242
|
frequentTrips: groups.map((g6) => ({
|
|
@@ -124822,7 +125244,7 @@ async function handleGetFrequentTripsForProject(input) {
|
|
|
124822
125244
|
endLocation: g6.endLocation,
|
|
124823
125245
|
tripType: g6.tripType,
|
|
124824
125246
|
count: g6.count,
|
|
124825
|
-
avgDistance: g6.avgDistance != null ? round23(
|
|
125247
|
+
avgDistance: g6.avgDistance != null ? round23(toNumber3(g6.avgDistance)) : null,
|
|
124826
125248
|
lastUsedDate: g6.lastUsedDate
|
|
124827
125249
|
}))
|
|
124828
125250
|
});
|
|
@@ -125490,6 +125912,10 @@ function createMcpServer() {
|
|
|
125490
125912
|
);
|
|
125491
125913
|
case "log-hours":
|
|
125492
125914
|
return await handleLogHours(asToolArgs(toolArgs));
|
|
125915
|
+
case "get-time-entries":
|
|
125916
|
+
return await handleGetTimeEntries(
|
|
125917
|
+
asToolArgs(toolArgs)
|
|
125918
|
+
);
|
|
125493
125919
|
case "get-github-file":
|
|
125494
125920
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
125495
125921
|
case "list-github-directory":
|