@mgsoftwarebv/mcp-server-bridge 3.5.9 → 3.5.10
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 +598 -186
- 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"),
|
|
@@ -107203,6 +107218,67 @@ var TOOLS = [
|
|
|
107203
107218
|
required: ["workDescription", "estimatedHours"]
|
|
107204
107219
|
}
|
|
107205
107220
|
},
|
|
107221
|
+
{
|
|
107222
|
+
name: "get-time-entries",
|
|
107223
|
+
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.",
|
|
107224
|
+
inputSchema: {
|
|
107225
|
+
type: "object",
|
|
107226
|
+
properties: {
|
|
107227
|
+
teamId: teamIdProp,
|
|
107228
|
+
userId: {
|
|
107229
|
+
type: "string",
|
|
107230
|
+
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."
|
|
107231
|
+
},
|
|
107232
|
+
projectId: { type: "string", description: "Filter by project UUID." },
|
|
107233
|
+
customerId: {
|
|
107234
|
+
type: "string",
|
|
107235
|
+
description: "Filter by customer UUID. Matches entries whose own customer OR whose project's customer is this customer (project link is the common case)."
|
|
107236
|
+
},
|
|
107237
|
+
ticketId: {
|
|
107238
|
+
type: "string",
|
|
107239
|
+
description: "Only entries linked to this ticket (UUID)."
|
|
107240
|
+
},
|
|
107241
|
+
dateFrom: {
|
|
107242
|
+
type: "string",
|
|
107243
|
+
description: "Inclusive period start (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107244
|
+
},
|
|
107245
|
+
dateTo: {
|
|
107246
|
+
type: "string",
|
|
107247
|
+
description: "Inclusive period end (YYYY-MM-DD, Europe/Amsterdam)."
|
|
107248
|
+
},
|
|
107249
|
+
status: {
|
|
107250
|
+
type: "string",
|
|
107251
|
+
enum: ["draft", "approved", "confirmed", "invoiced", "all"],
|
|
107252
|
+
description: "Derived status filter: draft (not confirmed, not invoiced), approved/confirmed (confirmed, not invoiced), invoiced (linked to an invoice), all (default)."
|
|
107253
|
+
},
|
|
107254
|
+
type: {
|
|
107255
|
+
type: "string",
|
|
107256
|
+
enum: [
|
|
107257
|
+
"meeting",
|
|
107258
|
+
"work",
|
|
107259
|
+
"clocked_work",
|
|
107260
|
+
"appointment",
|
|
107261
|
+
"task",
|
|
107262
|
+
"other"
|
|
107263
|
+
],
|
|
107264
|
+
description: "Filter by timesheet event type. Omit to include all worked-time types (deadlines/all-day markers are always excluded)."
|
|
107265
|
+
},
|
|
107266
|
+
groupBy: {
|
|
107267
|
+
type: "string",
|
|
107268
|
+
enum: ["none", "day", "project", "customer", "user", "ticket"],
|
|
107269
|
+
default: "none",
|
|
107270
|
+
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."
|
|
107271
|
+
},
|
|
107272
|
+
pageSize: {
|
|
107273
|
+
type: "number",
|
|
107274
|
+
default: 50,
|
|
107275
|
+
maximum: 200,
|
|
107276
|
+
description: "Max detailed entries returned. Totals/groups always cover ALL matching entries regardless of this limit."
|
|
107277
|
+
}
|
|
107278
|
+
},
|
|
107279
|
+
required: []
|
|
107280
|
+
}
|
|
107281
|
+
},
|
|
107206
107282
|
{
|
|
107207
107283
|
name: "get-trips",
|
|
107208
107284
|
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.",
|
|
@@ -114024,6 +114100,338 @@ async function handleLogHours(input) {
|
|
|
114024
114100
|
responseText += `\u2705 Time entry ${wasUpdated ? "updated" : "created"} and ready for review in the agenda!`;
|
|
114025
114101
|
return { content: [{ type: "text", text: responseText }] };
|
|
114026
114102
|
}
|
|
114103
|
+
var TIME_ENTRY_TYPES = [
|
|
114104
|
+
"meeting",
|
|
114105
|
+
"work",
|
|
114106
|
+
"clocked_work",
|
|
114107
|
+
"appointment",
|
|
114108
|
+
"task",
|
|
114109
|
+
"other"
|
|
114110
|
+
];
|
|
114111
|
+
var TIME_ENTRY_STATUSES = [
|
|
114112
|
+
"draft",
|
|
114113
|
+
"approved",
|
|
114114
|
+
"confirmed",
|
|
114115
|
+
"invoiced",
|
|
114116
|
+
"all"
|
|
114117
|
+
];
|
|
114118
|
+
var TIME_ENTRY_GROUP_BY = [
|
|
114119
|
+
"none",
|
|
114120
|
+
"day",
|
|
114121
|
+
"project",
|
|
114122
|
+
"customer",
|
|
114123
|
+
"user",
|
|
114124
|
+
"ticket"
|
|
114125
|
+
];
|
|
114126
|
+
var TIMEZONE = "Europe/Amsterdam";
|
|
114127
|
+
function textResponse3(text3) {
|
|
114128
|
+
return { content: [{ type: "text", text: text3 }] };
|
|
114129
|
+
}
|
|
114130
|
+
function jsonResponse(payload) {
|
|
114131
|
+
return {
|
|
114132
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
114133
|
+
};
|
|
114134
|
+
}
|
|
114135
|
+
function toNumber(value) {
|
|
114136
|
+
if (value == null) return 0;
|
|
114137
|
+
if (typeof value === "number") return value;
|
|
114138
|
+
const parsed = Number.parseFloat(String(value));
|
|
114139
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
114140
|
+
}
|
|
114141
|
+
function hoursFrom(seconds) {
|
|
114142
|
+
return Math.round(toNumber(seconds) / 3600 * 100) / 100;
|
|
114143
|
+
}
|
|
114144
|
+
function deriveEntryStatus(invoiceId, rawStatus) {
|
|
114145
|
+
if (invoiceId) return "invoiced";
|
|
114146
|
+
if (rawStatus === "confirmed") return "approved";
|
|
114147
|
+
return "draft";
|
|
114148
|
+
}
|
|
114149
|
+
function durationSecondsExpr() {
|
|
114150
|
+
const te = schema_exports.timesheetEvents;
|
|
114151
|
+
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`;
|
|
114152
|
+
}
|
|
114153
|
+
function localDateExpr() {
|
|
114154
|
+
return sql`(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)})::date`;
|
|
114155
|
+
}
|
|
114156
|
+
function localDateTextExpr() {
|
|
114157
|
+
return sql`to_char(${schema_exports.timesheetEvents.startTime} AT TIME ZONE ${sql.raw(`'${TIMEZONE}'`)}, 'YYYY-MM-DD')`;
|
|
114158
|
+
}
|
|
114159
|
+
function effectiveCustomerIdExpr() {
|
|
114160
|
+
return sql`COALESCE(${schema_exports.timesheetEvents.customerId}, ${schema_exports.projects.customerId})`;
|
|
114161
|
+
}
|
|
114162
|
+
async function buildTimeEntryGroups(groupBy, whereExpr) {
|
|
114163
|
+
const te = schema_exports.timesheetEvents;
|
|
114164
|
+
const totalSecondsSel = sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`;
|
|
114165
|
+
const billableSecondsSel = sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`;
|
|
114166
|
+
const countSel = sql`count(*)::int`;
|
|
114167
|
+
const finish = (rows2) => rows2.sort((a6, b7) => b7.totalHours - a6.totalHours);
|
|
114168
|
+
if (groupBy === "day") {
|
|
114169
|
+
const dayExpr = localDateTextExpr();
|
|
114170
|
+
const rows2 = await db.select({
|
|
114171
|
+
day: sql`${dayExpr}`,
|
|
114172
|
+
totalSeconds: totalSecondsSel,
|
|
114173
|
+
billableSeconds: billableSecondsSel,
|
|
114174
|
+
entryCount: countSel
|
|
114175
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(dayExpr);
|
|
114176
|
+
return finish(
|
|
114177
|
+
rows2.map((r6) => ({
|
|
114178
|
+
key: { type: "day", date: r6.day },
|
|
114179
|
+
entryCount: Number(r6.entryCount),
|
|
114180
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114181
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114182
|
+
}))
|
|
114183
|
+
);
|
|
114184
|
+
}
|
|
114185
|
+
if (groupBy === "project") {
|
|
114186
|
+
const rows2 = await db.select({
|
|
114187
|
+
projectId: te.projectId,
|
|
114188
|
+
projectName: schema_exports.projects.name,
|
|
114189
|
+
totalSeconds: totalSecondsSel,
|
|
114190
|
+
billableSeconds: billableSecondsSel,
|
|
114191
|
+
entryCount: countSel
|
|
114192
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr).groupBy(te.projectId, schema_exports.projects.name);
|
|
114193
|
+
return finish(
|
|
114194
|
+
rows2.map((r6) => ({
|
|
114195
|
+
key: {
|
|
114196
|
+
type: "project",
|
|
114197
|
+
id: r6.projectId,
|
|
114198
|
+
name: r6.projectId ? r6.projectName : "(no project)"
|
|
114199
|
+
},
|
|
114200
|
+
entryCount: Number(r6.entryCount),
|
|
114201
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114202
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114203
|
+
}))
|
|
114204
|
+
);
|
|
114205
|
+
}
|
|
114206
|
+
if (groupBy === "customer") {
|
|
114207
|
+
const custId = effectiveCustomerIdExpr();
|
|
114208
|
+
const rows2 = await db.select({
|
|
114209
|
+
customerId: sql`${custId}`,
|
|
114210
|
+
customerName: schema_exports.customers.name,
|
|
114211
|
+
totalSeconds: totalSecondsSel,
|
|
114212
|
+
billableSeconds: billableSecondsSel,
|
|
114213
|
+
entryCount: countSel
|
|
114214
|
+
}).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);
|
|
114215
|
+
return finish(
|
|
114216
|
+
rows2.map((r6) => ({
|
|
114217
|
+
key: {
|
|
114218
|
+
type: "customer",
|
|
114219
|
+
id: r6.customerId,
|
|
114220
|
+
name: r6.customerId ? r6.customerName : "(no customer)"
|
|
114221
|
+
},
|
|
114222
|
+
entryCount: Number(r6.entryCount),
|
|
114223
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114224
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114225
|
+
}))
|
|
114226
|
+
);
|
|
114227
|
+
}
|
|
114228
|
+
if (groupBy === "user") {
|
|
114229
|
+
const rows2 = await db.select({
|
|
114230
|
+
userId: te.userId,
|
|
114231
|
+
userName: schema_exports.users.fullName,
|
|
114232
|
+
totalSeconds: totalSecondsSel,
|
|
114233
|
+
billableSeconds: billableSecondsSel,
|
|
114234
|
+
entryCount: countSel
|
|
114235
|
+
}).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);
|
|
114236
|
+
return finish(
|
|
114237
|
+
rows2.map((r6) => ({
|
|
114238
|
+
key: { type: "user", id: r6.userId, name: r6.userName },
|
|
114239
|
+
entryCount: Number(r6.entryCount),
|
|
114240
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114241
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114242
|
+
}))
|
|
114243
|
+
);
|
|
114244
|
+
}
|
|
114245
|
+
const tet = schema_exports.timesheetEventTickets;
|
|
114246
|
+
const rows = await db.select({
|
|
114247
|
+
ticketId: schema_exports.tickets.id,
|
|
114248
|
+
ticketNumber: schema_exports.tickets.ticketNumber,
|
|
114249
|
+
totalSeconds: totalSecondsSel,
|
|
114250
|
+
billableSeconds: billableSecondsSel,
|
|
114251
|
+
entryCount: countSel
|
|
114252
|
+
}).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);
|
|
114253
|
+
return finish(
|
|
114254
|
+
rows.map((r6) => ({
|
|
114255
|
+
key: {
|
|
114256
|
+
type: "ticket",
|
|
114257
|
+
id: r6.ticketId,
|
|
114258
|
+
ticketNumber: r6.ticketId ? r6.ticketNumber : "(no ticket)"
|
|
114259
|
+
},
|
|
114260
|
+
entryCount: Number(r6.entryCount),
|
|
114261
|
+
totalHours: hoursFrom(r6.totalSeconds),
|
|
114262
|
+
billableHours: hoursFrom(r6.billableSeconds)
|
|
114263
|
+
}))
|
|
114264
|
+
);
|
|
114265
|
+
}
|
|
114266
|
+
async function handleGetTimeEntries(input) {
|
|
114267
|
+
const ctx = getAuthContext();
|
|
114268
|
+
const te = schema_exports.timesheetEvents;
|
|
114269
|
+
const status = input.status ?? "all";
|
|
114270
|
+
if (!TIME_ENTRY_STATUSES.includes(status)) {
|
|
114271
|
+
return textResponse3(
|
|
114272
|
+
`Error: invalid status "${input.status}". Allowed: ${TIME_ENTRY_STATUSES.join(", ")}.`
|
|
114273
|
+
);
|
|
114274
|
+
}
|
|
114275
|
+
const groupBy = input.groupBy ?? "none";
|
|
114276
|
+
if (!TIME_ENTRY_GROUP_BY.includes(groupBy)) {
|
|
114277
|
+
return textResponse3(
|
|
114278
|
+
`Error: invalid groupBy "${input.groupBy}". Allowed: ${TIME_ENTRY_GROUP_BY.join(", ")}.`
|
|
114279
|
+
);
|
|
114280
|
+
}
|
|
114281
|
+
if (input.type && !TIME_ENTRY_TYPES.includes(input.type)) {
|
|
114282
|
+
return textResponse3(
|
|
114283
|
+
`Error: invalid type "${input.type}". Allowed: ${TIME_ENTRY_TYPES.join(", ")}. (Deadlines/all-day markers are agenda items \u2014 use get-calendar-items.)`
|
|
114284
|
+
);
|
|
114285
|
+
}
|
|
114286
|
+
const scope = await resolveTeamScope(input.teamId);
|
|
114287
|
+
if (!scope.ok) return scope.response;
|
|
114288
|
+
if (scope.teamIds.length === 0) {
|
|
114289
|
+
return textResponse3("No accessible teams found.");
|
|
114290
|
+
}
|
|
114291
|
+
if (input.projectId && !scope.projectIds.includes(input.projectId)) {
|
|
114292
|
+
return textResponse3(
|
|
114293
|
+
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
114294
|
+
);
|
|
114295
|
+
}
|
|
114296
|
+
if (input.customerId && !scope.customerIds.includes(input.customerId)) {
|
|
114297
|
+
return textResponse3(
|
|
114298
|
+
`Customer not found or no access: ${input.customerId}. Call get-customers first.`
|
|
114299
|
+
);
|
|
114300
|
+
}
|
|
114301
|
+
const rawUserId = input.userId?.trim();
|
|
114302
|
+
const userId = !rawUserId || rawUserId === "me" ? ctx.userId : rawUserId;
|
|
114303
|
+
const conditions = [
|
|
114304
|
+
inArray(te.teamId, scope.teamIds),
|
|
114305
|
+
eq(te.isDeleted, false),
|
|
114306
|
+
// Worked time only: drop all-day markers / deadlines (~24h agenda blocks
|
|
114307
|
+
// that would otherwise massively inflate the totals).
|
|
114308
|
+
eq(te.allDay, false),
|
|
114309
|
+
sql`${te.type}::text <> 'deadline'`
|
|
114310
|
+
];
|
|
114311
|
+
if (userId !== "all") conditions.push(eq(te.userId, userId));
|
|
114312
|
+
if (input.projectId) conditions.push(eq(te.projectId, input.projectId));
|
|
114313
|
+
if (input.customerId) {
|
|
114314
|
+
conditions.push(
|
|
114315
|
+
sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId}) = ${input.customerId}`
|
|
114316
|
+
);
|
|
114317
|
+
}
|
|
114318
|
+
if (input.dateFrom) {
|
|
114319
|
+
conditions.push(sql`${localDateExpr()} >= ${input.dateFrom}::date`);
|
|
114320
|
+
}
|
|
114321
|
+
if (input.dateTo) {
|
|
114322
|
+
conditions.push(sql`${localDateExpr()} <= ${input.dateTo}::date`);
|
|
114323
|
+
}
|
|
114324
|
+
if (input.type) conditions.push(sql`${te.type}::text = ${input.type}`);
|
|
114325
|
+
if (input.ticketId) {
|
|
114326
|
+
conditions.push(
|
|
114327
|
+
sql`EXISTS (SELECT 1 FROM ${schema_exports.timesheetEventTickets} WHERE ${schema_exports.timesheetEventTickets.timesheetEventId} = ${te.id} AND ${schema_exports.timesheetEventTickets.ticketId} = ${input.ticketId})`
|
|
114328
|
+
);
|
|
114329
|
+
}
|
|
114330
|
+
if (status === "draft") {
|
|
114331
|
+
conditions.push(sql`(${te.status}::text = 'draft' AND ${te.invoiceId} IS NULL)`);
|
|
114332
|
+
} else if (status === "approved" || status === "confirmed") {
|
|
114333
|
+
conditions.push(
|
|
114334
|
+
sql`(${te.status}::text = 'confirmed' AND ${te.invoiceId} IS NULL)`
|
|
114335
|
+
);
|
|
114336
|
+
} else if (status === "invoiced") {
|
|
114337
|
+
conditions.push(sql`${te.invoiceId} IS NOT NULL`);
|
|
114338
|
+
}
|
|
114339
|
+
const whereExpr = and(...conditions);
|
|
114340
|
+
const [totalsRow] = await db.select({
|
|
114341
|
+
count: sql`count(*)::int`,
|
|
114342
|
+
totalSeconds: sql`COALESCE(SUM(${durationSecondsExpr()}), 0)`,
|
|
114343
|
+
billableSeconds: sql`COALESCE(SUM(CASE WHEN ${te.billingStatus}::text = 'unbillable' THEN 0 ELSE ${durationSecondsExpr()} END), 0)`,
|
|
114344
|
+
invoicedSeconds: sql`COALESCE(SUM(CASE WHEN ${te.invoiceId} IS NOT NULL THEN ${durationSecondsExpr()} ELSE 0 END), 0)`
|
|
114345
|
+
}).from(te).leftJoin(schema_exports.projects, eq(te.projectId, schema_exports.projects.id)).where(whereExpr);
|
|
114346
|
+
const totalSeconds = toNumber(totalsRow?.totalSeconds);
|
|
114347
|
+
const billableSeconds = toNumber(totalsRow?.billableSeconds);
|
|
114348
|
+
const invoicedSeconds = toNumber(totalsRow?.invoicedSeconds);
|
|
114349
|
+
const entryCount = Number(totalsRow?.count ?? 0);
|
|
114350
|
+
const pageSize = Math.min(input.pageSize ?? 50, 200);
|
|
114351
|
+
const entryRows = await db.select({
|
|
114352
|
+
id: te.id,
|
|
114353
|
+
title: te.title,
|
|
114354
|
+
description: te.description,
|
|
114355
|
+
type: te.type,
|
|
114356
|
+
rawStatus: te.status,
|
|
114357
|
+
billingStatus: te.billingStatus,
|
|
114358
|
+
invoiceId: te.invoiceId,
|
|
114359
|
+
startTime: te.startTime,
|
|
114360
|
+
endTime: te.endTime,
|
|
114361
|
+
date: sql`${localDateTextExpr()}`,
|
|
114362
|
+
durationSeconds: sql`${durationSecondsExpr()}`,
|
|
114363
|
+
userId: te.userId,
|
|
114364
|
+
userName: schema_exports.users.fullName,
|
|
114365
|
+
projectId: te.projectId,
|
|
114366
|
+
projectName: schema_exports.projects.name,
|
|
114367
|
+
customerId: sql`COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`,
|
|
114368
|
+
customerName: schema_exports.customers.name
|
|
114369
|
+
}).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(
|
|
114370
|
+
schema_exports.customers,
|
|
114371
|
+
sql`${schema_exports.customers.id} = COALESCE(${te.customerId}, ${schema_exports.projects.customerId})`
|
|
114372
|
+
).where(whereExpr).orderBy(desc(te.startTime)).limit(pageSize);
|
|
114373
|
+
const entryIds = entryRows.map((r6) => r6.id);
|
|
114374
|
+
const ticketsByEvent = /* @__PURE__ */ new Map();
|
|
114375
|
+
if (entryIds.length > 0) {
|
|
114376
|
+
const links = await db.select({
|
|
114377
|
+
eventId: schema_exports.timesheetEventTickets.timesheetEventId,
|
|
114378
|
+
ticketId: schema_exports.tickets.id,
|
|
114379
|
+
ticketNumber: schema_exports.tickets.ticketNumber
|
|
114380
|
+
}).from(schema_exports.timesheetEventTickets).innerJoin(
|
|
114381
|
+
schema_exports.tickets,
|
|
114382
|
+
eq(schema_exports.timesheetEventTickets.ticketId, schema_exports.tickets.id)
|
|
114383
|
+
).where(inArray(schema_exports.timesheetEventTickets.timesheetEventId, entryIds));
|
|
114384
|
+
for (const link of links) {
|
|
114385
|
+
const list = ticketsByEvent.get(link.eventId) ?? [];
|
|
114386
|
+
list.push({ id: link.ticketId, ticketNumber: link.ticketNumber });
|
|
114387
|
+
ticketsByEvent.set(link.eventId, list);
|
|
114388
|
+
}
|
|
114389
|
+
}
|
|
114390
|
+
const entries = entryRows.map((r6) => ({
|
|
114391
|
+
id: r6.id,
|
|
114392
|
+
date: r6.date,
|
|
114393
|
+
startTime: r6.startTime,
|
|
114394
|
+
endTime: r6.endTime,
|
|
114395
|
+
hours: hoursFrom(r6.durationSeconds),
|
|
114396
|
+
title: r6.title,
|
|
114397
|
+
description: r6.description,
|
|
114398
|
+
type: r6.type,
|
|
114399
|
+
status: deriveEntryStatus(r6.invoiceId, r6.rawStatus),
|
|
114400
|
+
billingStatus: r6.billingStatus,
|
|
114401
|
+
invoiced: r6.invoiceId != null,
|
|
114402
|
+
invoiceId: r6.invoiceId,
|
|
114403
|
+
user: r6.userId ? { id: r6.userId, name: r6.userName } : null,
|
|
114404
|
+
project: r6.projectId ? { id: r6.projectId, name: r6.projectName } : null,
|
|
114405
|
+
customer: r6.customerId ? { id: r6.customerId, name: r6.customerName } : null,
|
|
114406
|
+
tickets: ticketsByEvent.get(r6.id) ?? []
|
|
114407
|
+
}));
|
|
114408
|
+
const groups = groupBy === "none" ? void 0 : await buildTimeEntryGroups(groupBy, whereExpr);
|
|
114409
|
+
return jsonResponse({
|
|
114410
|
+
filters: {
|
|
114411
|
+
userId: userId === "all" ? "all" : userId,
|
|
114412
|
+
teamIds: scope.teamIds,
|
|
114413
|
+
projectId: input.projectId ?? null,
|
|
114414
|
+
customerId: input.customerId ?? null,
|
|
114415
|
+
ticketId: input.ticketId ?? null,
|
|
114416
|
+
dateFrom: input.dateFrom ?? null,
|
|
114417
|
+
dateTo: input.dateTo ?? null,
|
|
114418
|
+
status,
|
|
114419
|
+
type: input.type ?? null,
|
|
114420
|
+
timezone: TIMEZONE
|
|
114421
|
+
},
|
|
114422
|
+
totals: {
|
|
114423
|
+
totalHours: hoursFrom(totalSeconds),
|
|
114424
|
+
billableHours: hoursFrom(billableSeconds),
|
|
114425
|
+
nonBillableHours: hoursFrom(totalSeconds - billableSeconds),
|
|
114426
|
+
invoicedHours: hoursFrom(invoicedSeconds),
|
|
114427
|
+
entryCount
|
|
114428
|
+
},
|
|
114429
|
+
groupBy,
|
|
114430
|
+
...groups ? { groups } : {},
|
|
114431
|
+
entries,
|
|
114432
|
+
entriesTruncated: entryCount > entries.length
|
|
114433
|
+
});
|
|
114434
|
+
}
|
|
114027
114435
|
|
|
114028
114436
|
// ../invoice/src/utils/included-items.ts
|
|
114029
114437
|
function parseIncludedItems(value) {
|
|
@@ -114397,7 +114805,7 @@ var INVOICE_STATUSES = [
|
|
|
114397
114805
|
"refunded"
|
|
114398
114806
|
];
|
|
114399
114807
|
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
|
|
114808
|
+
function textResponse4(text3) {
|
|
114401
114809
|
return { content: [{ type: "text", text: text3 }] };
|
|
114402
114810
|
}
|
|
114403
114811
|
function tiptapNote(text3) {
|
|
@@ -114515,7 +114923,7 @@ async function resolveInvoiceLineItems(inputs, defaults, teamId) {
|
|
|
114515
114923
|
return { items };
|
|
114516
114924
|
}
|
|
114517
114925
|
function notDraftResponse(invoice) {
|
|
114518
|
-
return
|
|
114926
|
+
return textResponse4(
|
|
114519
114927
|
`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
114928
|
);
|
|
114521
114929
|
}
|
|
@@ -114532,14 +114940,14 @@ Issue: ${invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString() :
|
|
|
114532
114940
|
async function handleGetInvoices(input) {
|
|
114533
114941
|
const { customerId, status, q: q3, pageSize = 20 } = input;
|
|
114534
114942
|
if (status && !INVOICE_STATUSES.includes(status)) {
|
|
114535
|
-
return
|
|
114943
|
+
return textResponse4(
|
|
114536
114944
|
`Error: invalid status "${status}". Allowed: ${INVOICE_STATUSES.join(", ")}.`
|
|
114537
114945
|
);
|
|
114538
114946
|
}
|
|
114539
114947
|
const scope = await resolveTeamScope(input.teamId);
|
|
114540
114948
|
if (!scope.ok) return scope.response;
|
|
114541
114949
|
if (scope.teamIds.length === 0) {
|
|
114542
|
-
return
|
|
114950
|
+
return textResponse4("No accessible teams found.");
|
|
114543
114951
|
}
|
|
114544
114952
|
const filters = [inArray(schema_exports.invoices.teamId, scope.teamIds)];
|
|
114545
114953
|
if (customerId) filters.push(eq(schema_exports.invoices.customerId, customerId));
|
|
@@ -114566,7 +114974,7 @@ async function handleGetInvoices(input) {
|
|
|
114566
114974
|
createdAt: schema_exports.invoices.createdAt
|
|
114567
114975
|
}).from(schema_exports.invoices).where(and(...filters)).orderBy(desc(schema_exports.invoices.createdAt)).limit(Math.min(pageSize, 100));
|
|
114568
114976
|
if (rows.length === 0) {
|
|
114569
|
-
return
|
|
114977
|
+
return textResponse4("No invoices found.");
|
|
114570
114978
|
}
|
|
114571
114979
|
const list = rows.map(
|
|
114572
114980
|
(inv) => `**${inv.invoiceNumber ?? "(draft, no number)"}**
|
|
@@ -114576,7 +114984,7 @@ Customer: ${inv.customerName ?? inv.customerId ?? "(none)"}
|
|
|
114576
114984
|
Issue date: ${inv.issueDate ? new Date(inv.issueDate).toLocaleDateString() : "-"} | Due: ${inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : "-"}
|
|
114577
114985
|
`
|
|
114578
114986
|
).join("\n");
|
|
114579
|
-
return
|
|
114987
|
+
return textResponse4(
|
|
114580
114988
|
`Found ${rows.length} invoices:
|
|
114581
114989
|
|
|
114582
114990
|
${list}
|
|
@@ -114585,15 +114993,15 @@ Use \`get-invoice-by-id\` for line items and linked documents. Use \`link-docume
|
|
|
114585
114993
|
}
|
|
114586
114994
|
async function handleGetInvoiceById(input) {
|
|
114587
114995
|
const { invoiceId } = input;
|
|
114588
|
-
if (!invoiceId) return
|
|
114996
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114589
114997
|
const scope = await resolveTeamScope(input.teamId);
|
|
114590
114998
|
if (!scope.ok) return scope.response;
|
|
114591
114999
|
if (scope.teamIds.length === 0) {
|
|
114592
|
-
return
|
|
115000
|
+
return textResponse4("No accessible teams found.");
|
|
114593
115001
|
}
|
|
114594
115002
|
const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
|
|
114595
115003
|
if (!invoice) {
|
|
114596
|
-
return
|
|
115004
|
+
return textResponse4(
|
|
114597
115005
|
`Invoice ${invoiceId} not found or you don't have access to it.`
|
|
114598
115006
|
);
|
|
114599
115007
|
}
|
|
@@ -114610,7 +115018,7 @@ async function handleGetInvoiceById(input) {
|
|
|
114610
115018
|
);
|
|
114611
115019
|
const linesText = lineItems.length > 0 ? lineItems.map((line2, i6) => formatLineItemDetail(line2, i6)).join("\n\n") : "(no line items)";
|
|
114612
115020
|
const docsText = linkedDocs.length > 0 ? linkedDocs.map((d6) => `- ${d6.title} (${d6.type ?? "document"}) \u2014 ${d6.id}`).join("\n") : "(none)";
|
|
114613
|
-
return
|
|
115021
|
+
return textResponse4(
|
|
114614
115022
|
`**Invoice ${invoice.invoiceNumber ?? invoice.id}**
|
|
114615
115023
|
|
|
114616
115024
|
ID: ${invoice.id}
|
|
@@ -114632,12 +115040,12 @@ ${docsText}
|
|
|
114632
115040
|
}
|
|
114633
115041
|
async function handleUpdateInvoice(input) {
|
|
114634
115042
|
const { invoiceId } = input;
|
|
114635
|
-
if (!invoiceId) return
|
|
115043
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114636
115044
|
const resolved = await resolveTeamId(input.teamId);
|
|
114637
115045
|
if (!resolved.ok) return resolved.response;
|
|
114638
115046
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114639
115047
|
if (!invoice) {
|
|
114640
|
-
return
|
|
115048
|
+
return textResponse4(
|
|
114641
115049
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114642
115050
|
);
|
|
114643
115051
|
}
|
|
@@ -114664,7 +115072,7 @@ async function handleUpdateInvoice(input) {
|
|
|
114664
115072
|
defaults,
|
|
114665
115073
|
invoice.teamId
|
|
114666
115074
|
);
|
|
114667
|
-
if (error49) return
|
|
115075
|
+
if (error49) return textResponse4(`Error: ${error49}`);
|
|
114668
115076
|
const totals = computeInvoiceTotals(
|
|
114669
115077
|
items,
|
|
114670
115078
|
defaults,
|
|
@@ -114677,28 +115085,28 @@ async function handleUpdateInvoice(input) {
|
|
|
114677
115085
|
updates.amount = totals.amount;
|
|
114678
115086
|
}
|
|
114679
115087
|
if (Object.keys(updates).length === 0) {
|
|
114680
|
-
return
|
|
115088
|
+
return textResponse4(
|
|
114681
115089
|
"No fields to update. Provide at least one of: title, note, internalNote, dueDate, issueDate, lineItems."
|
|
114682
115090
|
);
|
|
114683
115091
|
}
|
|
114684
115092
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
114685
115093
|
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
|
|
115094
|
+
if (!updated) return textResponse4(`Failed to update invoice ${invoiceId}.`);
|
|
115095
|
+
return textResponse4(`\u2705 **Draft invoice updated**
|
|
114688
115096
|
|
|
114689
115097
|
${formatInvoiceSummary(updated)}`);
|
|
114690
115098
|
}
|
|
114691
115099
|
async function handleUpdateInvoiceLines(input) {
|
|
114692
115100
|
const { invoiceId, lineItems: patches } = input;
|
|
114693
|
-
if (!invoiceId) return
|
|
115101
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
114694
115102
|
if (!patches || patches.length === 0) {
|
|
114695
|
-
return
|
|
115103
|
+
return textResponse4("Error: `lineItems` must be a non-empty array.");
|
|
114696
115104
|
}
|
|
114697
115105
|
const resolved = await resolveTeamId(input.teamId);
|
|
114698
115106
|
if (!resolved.ok) return resolved.response;
|
|
114699
115107
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114700
115108
|
if (!invoice) {
|
|
114701
|
-
return
|
|
115109
|
+
return textResponse4(
|
|
114702
115110
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114703
115111
|
);
|
|
114704
115112
|
}
|
|
@@ -114712,7 +115120,7 @@ async function handleUpdateInvoiceLines(input) {
|
|
|
114712
115120
|
for (const patch of patches) {
|
|
114713
115121
|
const index2 = patch.index;
|
|
114714
115122
|
if (index2 < 0 || index2 >= items.length) {
|
|
114715
|
-
return
|
|
115123
|
+
return textResponse4(
|
|
114716
115124
|
`Error: line index ${index2} is out of range (invoice has ${items.length} line(s)).`
|
|
114717
115125
|
);
|
|
114718
115126
|
}
|
|
@@ -114739,13 +115147,13 @@ async function handleUpdateInvoiceLines(input) {
|
|
|
114739
115147
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114740
115148
|
}).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
|
|
114741
115149
|
if (!updated) {
|
|
114742
|
-
return
|
|
115150
|
+
return textResponse4(`Failed to update invoice lines for ${invoiceId}.`);
|
|
114743
115151
|
}
|
|
114744
115152
|
const changedLines = patches.map((p3) => {
|
|
114745
115153
|
const line2 = items[p3.index];
|
|
114746
115154
|
return `[${p3.index}] ${plainTextFromLineItemName(line2.name)}`;
|
|
114747
115155
|
}).join("\n");
|
|
114748
|
-
return
|
|
115156
|
+
return textResponse4(
|
|
114749
115157
|
`\u2705 **Updated ${updatedCount} line item(s) on draft invoice ${updated.invoiceNumber ?? updated.id}**
|
|
114750
115158
|
|
|
114751
115159
|
${changedLines}
|
|
@@ -114755,13 +115163,13 @@ New total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal},
|
|
|
114755
115163
|
}
|
|
114756
115164
|
async function handleAddProductToInvoice(input) {
|
|
114757
115165
|
const { invoiceId, productId } = input;
|
|
114758
|
-
if (!invoiceId) return
|
|
114759
|
-
if (!productId) return
|
|
115166
|
+
if (!invoiceId) return textResponse4("Error: `invoiceId` is required.");
|
|
115167
|
+
if (!productId) return textResponse4("Error: `productId` is required.");
|
|
114760
115168
|
const resolved = await resolveTeamId(input.teamId);
|
|
114761
115169
|
if (!resolved.ok) return resolved.response;
|
|
114762
115170
|
const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
|
|
114763
115171
|
if (!invoice) {
|
|
114764
|
-
return
|
|
115172
|
+
return textResponse4(
|
|
114765
115173
|
`Invoice ${invoiceId} not found or not owned by this team.`
|
|
114766
115174
|
);
|
|
114767
115175
|
}
|
|
@@ -114769,7 +115177,7 @@ async function handleAddProductToInvoice(input) {
|
|
|
114769
115177
|
const products = await loadProductsInTeam([productId], invoice.teamId);
|
|
114770
115178
|
const product = products.get(productId);
|
|
114771
115179
|
if (!product) {
|
|
114772
|
-
return
|
|
115180
|
+
return textResponse4(
|
|
114773
115181
|
`Product ${productId} not found or not owned by this team.`
|
|
114774
115182
|
);
|
|
114775
115183
|
}
|
|
@@ -114801,9 +115209,9 @@ async function handleAddProductToInvoice(input) {
|
|
|
114801
115209
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114802
115210
|
}).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
|
|
114803
115211
|
if (!updated) {
|
|
114804
|
-
return
|
|
115212
|
+
return textResponse4(`Failed to add product to invoice ${invoiceId}.`);
|
|
114805
115213
|
}
|
|
114806
|
-
return
|
|
115214
|
+
return textResponse4(
|
|
114807
115215
|
`\u2705 **Product added to draft invoice ${updated.invoiceNumber ?? updated.id}**
|
|
114808
115216
|
|
|
114809
115217
|
` + formatLineItemDetail(newItem, items.length - 1) + `
|
|
@@ -114815,12 +115223,12 @@ Clause and pricing variant are snapshotted on the line \u2014 later catalog edit
|
|
|
114815
115223
|
async function handleLinkDocumentToInvoice(input) {
|
|
114816
115224
|
const { documentId, invoiceId } = input;
|
|
114817
115225
|
if (!documentId) {
|
|
114818
|
-
return
|
|
115226
|
+
return textResponse4("Error: `documentId` is required.");
|
|
114819
115227
|
}
|
|
114820
115228
|
const scope = await resolveTeamScope(input.teamId);
|
|
114821
115229
|
if (!scope.ok) return scope.response;
|
|
114822
115230
|
if (scope.teamIds.length === 0) {
|
|
114823
|
-
return
|
|
115231
|
+
return textResponse4("No accessible teams found.");
|
|
114824
115232
|
}
|
|
114825
115233
|
const [doc] = await db.select({
|
|
114826
115234
|
id: schema_exports.documents.id,
|
|
@@ -114835,24 +115243,24 @@ async function handleLinkDocumentToInvoice(input) {
|
|
|
114835
115243
|
)
|
|
114836
115244
|
).limit(1);
|
|
114837
115245
|
if (!doc) {
|
|
114838
|
-
return
|
|
115246
|
+
return textResponse4(
|
|
114839
115247
|
`Document ${documentId} not found or you don't have access to it.`
|
|
114840
115248
|
);
|
|
114841
115249
|
}
|
|
114842
115250
|
if (!invoiceId) {
|
|
114843
115251
|
await db.update(schema_exports.documents).set({ invoiceId: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
|
|
114844
|
-
return
|
|
115252
|
+
return textResponse4(
|
|
114845
115253
|
`\u2705 Document "${doc.title}" (${doc.id}) is unlinked from its invoice.`
|
|
114846
115254
|
);
|
|
114847
115255
|
}
|
|
114848
115256
|
const invoice = await findAccessibleInvoice(invoiceId, [doc.teamId]);
|
|
114849
115257
|
if (!invoice) {
|
|
114850
|
-
return
|
|
115258
|
+
return textResponse4(
|
|
114851
115259
|
`Error: invoice ${invoiceId} not found in team ${doc.teamId}. Use get-invoices to find a valid invoice id.`
|
|
114852
115260
|
);
|
|
114853
115261
|
}
|
|
114854
115262
|
await db.update(schema_exports.documents).set({ invoiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
|
|
114855
|
-
return
|
|
115263
|
+
return textResponse4(
|
|
114856
115264
|
`\u2705 **Document linked to invoice!**
|
|
114857
115265
|
|
|
114858
115266
|
Document: ${doc.title} (${doc.id})
|
|
@@ -114985,7 +115393,7 @@ ${description ? `Description: ${description}
|
|
|
114985
115393
|
]
|
|
114986
115394
|
};
|
|
114987
115395
|
}
|
|
114988
|
-
function
|
|
115396
|
+
function textResponse5(text3) {
|
|
114989
115397
|
return { content: [{ type: "text", text: text3 }] };
|
|
114990
115398
|
}
|
|
114991
115399
|
function memberLabel(m4) {
|
|
@@ -114999,7 +115407,7 @@ async function requireTeamOwner2(teamId, userId) {
|
|
|
114999
115407
|
eq(schema_exports.usersOnTeam.teamId, teamId)
|
|
115000
115408
|
)
|
|
115001
115409
|
).limit(1);
|
|
115002
|
-
return membership?.role === "owner" ? null :
|
|
115410
|
+
return membership?.role === "owner" ? null : textResponse5(OWNER_REQUIRED);
|
|
115003
115411
|
}
|
|
115004
115412
|
async function setProjectMemberAccess(params) {
|
|
115005
115413
|
const { projectId, teamId, memberIds, createdBy } = params;
|
|
@@ -115103,7 +115511,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115103
115511
|
if (!match) {
|
|
115104
115512
|
return {
|
|
115105
115513
|
ok: false,
|
|
115106
|
-
response:
|
|
115514
|
+
response: textResponse5(
|
|
115107
115515
|
`User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
|
|
115108
115516
|
)
|
|
115109
115517
|
};
|
|
@@ -115116,7 +115524,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115116
115524
|
if (matches.length === 0) {
|
|
115117
115525
|
return {
|
|
115118
115526
|
ok: false,
|
|
115119
|
-
response:
|
|
115527
|
+
response: textResponse5(
|
|
115120
115528
|
`No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
|
|
115121
115529
|
)
|
|
115122
115530
|
};
|
|
@@ -115124,7 +115532,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115124
115532
|
if (matches.length > 1) {
|
|
115125
115533
|
return {
|
|
115126
115534
|
ok: false,
|
|
115127
|
-
response:
|
|
115535
|
+
response: textResponse5(
|
|
115128
115536
|
`Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
|
|
115129
115537
|
)
|
|
115130
115538
|
};
|
|
@@ -115133,7 +115541,7 @@ async function resolveTeamMember(teamId, opts) {
|
|
|
115133
115541
|
}
|
|
115134
115542
|
return {
|
|
115135
115543
|
ok: false,
|
|
115136
|
-
response:
|
|
115544
|
+
response: textResponse5(
|
|
115137
115545
|
"Provide either a userId or an email to identify the member."
|
|
115138
115546
|
)
|
|
115139
115547
|
};
|
|
@@ -115182,7 +115590,7 @@ async function handleUpdateProject(input) {
|
|
|
115182
115590
|
if (!resolved.ok) return resolved.response;
|
|
115183
115591
|
const existing = await loadProjectInTeam(id, resolved.teamId);
|
|
115184
115592
|
if (!existing) {
|
|
115185
|
-
return
|
|
115593
|
+
return textResponse5(
|
|
115186
115594
|
`Project ${id} not found, or it is not owned by this team.`
|
|
115187
115595
|
);
|
|
115188
115596
|
}
|
|
@@ -115197,7 +115605,7 @@ async function handleUpdateProject(input) {
|
|
|
115197
115605
|
)
|
|
115198
115606
|
).limit(1);
|
|
115199
115607
|
if (dupe) {
|
|
115200
|
-
return
|
|
115608
|
+
return textResponse5(
|
|
115201
115609
|
`A project named "${input.name}" already exists in this team. Choose a different name.`
|
|
115202
115610
|
);
|
|
115203
115611
|
}
|
|
@@ -115262,7 +115670,7 @@ async function handleUpdateProject(input) {
|
|
|
115262
115670
|
customerName: schema_exports.customers.name
|
|
115263
115671
|
}).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
115672
|
if (!updated) {
|
|
115265
|
-
return
|
|
115673
|
+
return textResponse5(`Failed to update project ${id}.`);
|
|
115266
115674
|
}
|
|
115267
115675
|
const lines = [
|
|
115268
115676
|
"\u2705 **Project Updated**",
|
|
@@ -115280,7 +115688,7 @@ async function handleUpdateProject(input) {
|
|
|
115280
115688
|
if (willRename) {
|
|
115281
115689
|
lines.push("", "Note: tickets for this project were renumbered.");
|
|
115282
115690
|
}
|
|
115283
|
-
return
|
|
115691
|
+
return textResponse5(lines.join("\n"));
|
|
115284
115692
|
}
|
|
115285
115693
|
async function handleGetProjectMembers(input) {
|
|
115286
115694
|
const { projectId } = input;
|
|
@@ -115288,7 +115696,7 @@ async function handleGetProjectMembers(input) {
|
|
|
115288
115696
|
if (!resolved.ok) return resolved.response;
|
|
115289
115697
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115290
115698
|
if (!project) {
|
|
115291
|
-
return
|
|
115699
|
+
return textResponse5(
|
|
115292
115700
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115293
115701
|
);
|
|
115294
115702
|
}
|
|
@@ -115317,7 +115725,7 @@ async function handleGetProjectMembers(input) {
|
|
|
115317
115725
|
return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
|
|
115318
115726
|
}).join("\n");
|
|
115319
115727
|
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
|
|
115728
|
+
return textResponse5(
|
|
115321
115729
|
`**Project members for "${project.name}"** (ID: ${project.id})
|
|
115322
115730
|
|
|
115323
115731
|
${note}
|
|
@@ -115338,7 +115746,7 @@ async function handleSetProjectMembers(input) {
|
|
|
115338
115746
|
if (ownerError) return ownerError;
|
|
115339
115747
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115340
115748
|
if (!project) {
|
|
115341
|
-
return
|
|
115749
|
+
return textResponse5(
|
|
115342
115750
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115343
115751
|
);
|
|
115344
115752
|
}
|
|
@@ -115376,7 +115784,7 @@ async function handleSetProjectMembers(input) {
|
|
|
115376
115784
|
|
|
115377
115785
|
\u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
|
|
115378
115786
|
}
|
|
115379
|
-
return
|
|
115787
|
+
return textResponse5(
|
|
115380
115788
|
`\u2705 **Project members updated**
|
|
115381
115789
|
|
|
115382
115790
|
Members with explicit access to this project:
|
|
@@ -115392,7 +115800,7 @@ async function handleAddProjectMember(input) {
|
|
|
115392
115800
|
if (ownerError) return ownerError;
|
|
115393
115801
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115394
115802
|
if (!project) {
|
|
115395
|
-
return
|
|
115803
|
+
return textResponse5(
|
|
115396
115804
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115397
115805
|
);
|
|
115398
115806
|
}
|
|
@@ -115403,7 +115811,7 @@ async function handleAddProjectMember(input) {
|
|
|
115403
115811
|
if (!member2.ok) return member2.response;
|
|
115404
115812
|
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
115405
115813
|
if (state2.projectMemberIds.has(member2.member.userId)) {
|
|
115406
|
-
return
|
|
115814
|
+
return textResponse5(
|
|
115407
115815
|
`${memberLabel(member2.member)} already has explicit access to this project.`
|
|
115408
115816
|
);
|
|
115409
115817
|
}
|
|
@@ -115418,7 +115826,7 @@ async function handleAddProjectMember(input) {
|
|
|
115418
115826
|
if (wasUnrestricted) {
|
|
115419
115827
|
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
115828
|
}
|
|
115421
|
-
return
|
|
115829
|
+
return textResponse5(text3);
|
|
115422
115830
|
}
|
|
115423
115831
|
async function handleRemoveProjectMember(input) {
|
|
115424
115832
|
const ctx = getAuthContext();
|
|
@@ -115429,7 +115837,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115429
115837
|
if (ownerError) return ownerError;
|
|
115430
115838
|
const project = await loadProjectInTeam(projectId, resolved.teamId);
|
|
115431
115839
|
if (!project) {
|
|
115432
|
-
return
|
|
115840
|
+
return textResponse5(
|
|
115433
115841
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115434
115842
|
);
|
|
115435
115843
|
}
|
|
@@ -115440,7 +115848,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115440
115848
|
if (!member2.ok) return member2.response;
|
|
115441
115849
|
const state2 = await getProjectAccessState(resolved.teamId, projectId);
|
|
115442
115850
|
if (!state2.projectMemberIds.has(member2.member.userId)) {
|
|
115443
|
-
return
|
|
115851
|
+
return textResponse5(
|
|
115444
115852
|
`${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
|
|
115445
115853
|
);
|
|
115446
115854
|
}
|
|
@@ -115456,7 +115864,7 @@ async function handleRemoveProjectMember(input) {
|
|
|
115456
115864
|
if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
|
|
115457
115865
|
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
115866
|
}
|
|
115459
|
-
return
|
|
115867
|
+
return textResponse5(text3);
|
|
115460
115868
|
}
|
|
115461
115869
|
async function loadProjectForCleanup(projectId, teamId) {
|
|
115462
115870
|
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
@@ -115484,25 +115892,25 @@ async function countProjectDependencies(projectId) {
|
|
|
115484
115892
|
}
|
|
115485
115893
|
async function handleArchiveProject(input) {
|
|
115486
115894
|
const { projectId, reason } = input;
|
|
115487
|
-
if (!projectId) return
|
|
115895
|
+
if (!projectId) return textResponse5("Error: `projectId` is required.");
|
|
115488
115896
|
const resolved = await resolveTeamId(input.teamId);
|
|
115489
115897
|
if (!resolved.ok) return resolved.response;
|
|
115490
115898
|
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
115491
115899
|
if (!project) {
|
|
115492
|
-
return
|
|
115900
|
+
return textResponse5(
|
|
115493
115901
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115494
115902
|
);
|
|
115495
115903
|
}
|
|
115496
115904
|
const state2 = getProjectArchiveState(project.settings);
|
|
115497
115905
|
if (state2.archived) {
|
|
115498
|
-
return
|
|
115906
|
+
return textResponse5(
|
|
115499
115907
|
`Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
|
|
115500
115908
|
);
|
|
115501
115909
|
}
|
|
115502
115910
|
const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
115503
115911
|
const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
|
|
115504
115912
|
await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
|
|
115505
|
-
return
|
|
115913
|
+
return textResponse5(
|
|
115506
115914
|
`\u2705 **Project archived**
|
|
115507
115915
|
|
|
115508
115916
|
Project: ${project.name}
|
|
@@ -115520,21 +115928,21 @@ Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashbo
|
|
|
115520
115928
|
async function handleDeleteProject(input) {
|
|
115521
115929
|
const ctx = getAuthContext();
|
|
115522
115930
|
const { projectId, confirmEmptyOnly } = input;
|
|
115523
|
-
if (!projectId) return
|
|
115931
|
+
if (!projectId) return textResponse5("Error: `projectId` is required.");
|
|
115524
115932
|
const resolved = await resolveTeamId(input.teamId);
|
|
115525
115933
|
if (!resolved.ok) return resolved.response;
|
|
115526
115934
|
const ownerError = await requireTeamOwner2(resolved.teamId, ctx.userId);
|
|
115527
115935
|
if (ownerError) return ownerError;
|
|
115528
115936
|
const project = await loadProjectForCleanup(projectId, resolved.teamId);
|
|
115529
115937
|
if (!project) {
|
|
115530
|
-
return
|
|
115938
|
+
return textResponse5(
|
|
115531
115939
|
`Project ${projectId} not found, or it is not owned by this team.`
|
|
115532
115940
|
);
|
|
115533
115941
|
}
|
|
115534
115942
|
const deps = await countProjectDependencies(project.id);
|
|
115535
115943
|
const summary = formatProjectDependencies(deps);
|
|
115536
115944
|
if (!isProjectEmpty(deps)) {
|
|
115537
|
-
return
|
|
115945
|
+
return textResponse5(
|
|
115538
115946
|
`\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
|
|
115539
115947
|
|
|
115540
115948
|
Dependencies: ${summary}.
|
|
@@ -115543,12 +115951,12 @@ A hard delete would orphan these records, so it is not allowed. Use archive-proj
|
|
|
115543
115951
|
);
|
|
115544
115952
|
}
|
|
115545
115953
|
if (confirmEmptyOnly !== true) {
|
|
115546
|
-
return
|
|
115954
|
+
return textResponse5(
|
|
115547
115955
|
`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
115956
|
);
|
|
115549
115957
|
}
|
|
115550
115958
|
await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
|
|
115551
|
-
return
|
|
115959
|
+
return textResponse5(
|
|
115552
115960
|
`\u2705 **Project deleted**
|
|
115553
115961
|
|
|
115554
115962
|
Project: ${project.name}
|
|
@@ -115601,7 +116009,7 @@ var PRODUCT_COLUMNS2 = {
|
|
|
115601
116009
|
createdAt: schema_exports.invoiceProducts.createdAt,
|
|
115602
116010
|
updatedAt: schema_exports.invoiceProducts.updatedAt
|
|
115603
116011
|
};
|
|
115604
|
-
function
|
|
116012
|
+
function textResponse6(text3) {
|
|
115605
116013
|
return { content: [{ type: "text", text: text3 }] };
|
|
115606
116014
|
}
|
|
115607
116015
|
function formatPrice(p3) {
|
|
@@ -115647,14 +116055,14 @@ async function handleGetProducts(input) {
|
|
|
115647
116055
|
const { q: q3, currency, pageSize = 20 } = input;
|
|
115648
116056
|
const status = input.status ?? "active";
|
|
115649
116057
|
if (!PRODUCT_STATUSES.includes(status)) {
|
|
115650
|
-
return
|
|
116058
|
+
return textResponse6(
|
|
115651
116059
|
`Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
|
|
115652
116060
|
);
|
|
115653
116061
|
}
|
|
115654
116062
|
const scope = await resolveTeamScope(input.teamId);
|
|
115655
116063
|
if (!scope.ok) return scope.response;
|
|
115656
116064
|
if (scope.teamIds.length === 0) {
|
|
115657
|
-
return
|
|
116065
|
+
return textResponse6("No accessible teams found.");
|
|
115658
116066
|
}
|
|
115659
116067
|
const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
|
|
115660
116068
|
if (status === "active") {
|
|
@@ -115677,11 +116085,11 @@ async function handleGetProducts(input) {
|
|
|
115677
116085
|
asc(schema_exports.invoiceProducts.name)
|
|
115678
116086
|
).limit(Math.min(pageSize, 100));
|
|
115679
116087
|
if (rows.length === 0) {
|
|
115680
|
-
return
|
|
116088
|
+
return textResponse6(
|
|
115681
116089
|
`No products found${status !== "all" ? ` (status: ${status})` : ""}.`
|
|
115682
116090
|
);
|
|
115683
116091
|
}
|
|
115684
|
-
return
|
|
116092
|
+
return textResponse6(
|
|
115685
116093
|
`Found ${rows.length} product(s):
|
|
115686
116094
|
|
|
115687
116095
|
${rows.map(formatProduct).join("\n")}`
|
|
@@ -115689,11 +116097,11 @@ ${rows.map(formatProduct).join("\n")}`
|
|
|
115689
116097
|
}
|
|
115690
116098
|
async function handleGetProductById(input) {
|
|
115691
116099
|
const { productId } = input;
|
|
115692
|
-
if (!productId) return
|
|
116100
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115693
116101
|
const scope = await resolveTeamScope(input.teamId);
|
|
115694
116102
|
if (!scope.ok) return scope.response;
|
|
115695
116103
|
if (scope.teamIds.length === 0) {
|
|
115696
|
-
return
|
|
116104
|
+
return textResponse6("No accessible teams found.");
|
|
115697
116105
|
}
|
|
115698
116106
|
const [row] = await db.select(PRODUCT_COLUMNS2).from(schema_exports.invoiceProducts).where(
|
|
115699
116107
|
and(
|
|
@@ -115702,11 +116110,11 @@ async function handleGetProductById(input) {
|
|
|
115702
116110
|
)
|
|
115703
116111
|
).limit(1);
|
|
115704
116112
|
if (!row) {
|
|
115705
|
-
return
|
|
116113
|
+
return textResponse6(
|
|
115706
116114
|
`Product ${productId} not found or you don't have access to it.`
|
|
115707
116115
|
);
|
|
115708
116116
|
}
|
|
115709
|
-
return
|
|
116117
|
+
return textResponse6(formatProduct(row));
|
|
115710
116118
|
}
|
|
115711
116119
|
async function loadProductInTeam(productId, teamId) {
|
|
115712
116120
|
const accessibleTeamIds = await getAccessibleTeamIds(teamId);
|
|
@@ -115721,10 +116129,10 @@ async function loadProductInTeam(productId, teamId) {
|
|
|
115721
116129
|
async function handleCreateProduct(input) {
|
|
115722
116130
|
const { name: name21, description, price, currency, unit } = input;
|
|
115723
116131
|
if (!name21 || name21.trim().length === 0) {
|
|
115724
|
-
return
|
|
116132
|
+
return textResponse6("Error: `name` is required.");
|
|
115725
116133
|
}
|
|
115726
116134
|
const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
|
|
115727
|
-
if (enumError) return
|
|
116135
|
+
if (enumError) return textResponse6(enumError);
|
|
115728
116136
|
const resolved = await resolveTeamId(input.teamId);
|
|
115729
116137
|
if (!resolved.ok) return resolved.response;
|
|
115730
116138
|
const [created] = await db.insert(schema_exports.invoiceProducts).values({
|
|
@@ -115744,8 +116152,8 @@ async function handleCreateProduct(input) {
|
|
|
115744
116152
|
isActive: true,
|
|
115745
116153
|
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
115746
116154
|
}).returning(PRODUCT_COLUMNS2);
|
|
115747
|
-
if (!created) return
|
|
115748
|
-
return
|
|
116155
|
+
if (!created) return textResponse6("Failed to create product.");
|
|
116156
|
+
return textResponse6(
|
|
115749
116157
|
`\u2705 **Product created**
|
|
115750
116158
|
|
|
115751
116159
|
${formatProduct(created)}`
|
|
@@ -115753,21 +116161,21 @@ ${formatProduct(created)}`
|
|
|
115753
116161
|
}
|
|
115754
116162
|
async function handleUpdateProduct(input) {
|
|
115755
116163
|
const { productId } = input;
|
|
115756
|
-
if (!productId) return
|
|
116164
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115757
116165
|
const resolved = await resolveTeamId(input.teamId);
|
|
115758
116166
|
if (!resolved.ok) return resolved.response;
|
|
115759
116167
|
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
115760
116168
|
if (!existing) {
|
|
115761
|
-
return
|
|
116169
|
+
return textResponse6(
|
|
115762
116170
|
`Product ${productId} not found, or it is not owned by this team.`
|
|
115763
116171
|
);
|
|
115764
116172
|
}
|
|
115765
116173
|
const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
|
|
115766
|
-
if (enumError) return
|
|
116174
|
+
if (enumError) return textResponse6(enumError);
|
|
115767
116175
|
const updates = {};
|
|
115768
116176
|
if (input.name !== void 0) {
|
|
115769
116177
|
if (!input.name || input.name.trim().length === 0) {
|
|
115770
|
-
return
|
|
116178
|
+
return textResponse6("Error: `name` cannot be empty.");
|
|
115771
116179
|
}
|
|
115772
116180
|
updates.name = input.name.trim();
|
|
115773
116181
|
}
|
|
@@ -115788,14 +116196,14 @@ async function handleUpdateProduct(input) {
|
|
|
115788
116196
|
updates.clause = serializeProductClause(input.clause);
|
|
115789
116197
|
}
|
|
115790
116198
|
if (Object.keys(updates).length === 0) {
|
|
115791
|
-
return
|
|
116199
|
+
return textResponse6(
|
|
115792
116200
|
"No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder, clause."
|
|
115793
116201
|
);
|
|
115794
116202
|
}
|
|
115795
116203
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
115796
116204
|
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
|
|
116205
|
+
if (!updated) return textResponse6(`Failed to update product ${productId}.`);
|
|
116206
|
+
return textResponse6(
|
|
115799
116207
|
`\u2705 **Product updated**
|
|
115800
116208
|
|
|
115801
116209
|
${formatProduct(updated)}
|
|
@@ -115804,23 +116212,23 @@ Note: this only affects future invoices/quotes. Existing documents keep their li
|
|
|
115804
116212
|
}
|
|
115805
116213
|
async function handleArchiveProduct(input) {
|
|
115806
116214
|
const { productId, reason } = input;
|
|
115807
|
-
if (!productId) return
|
|
116215
|
+
if (!productId) return textResponse6("Error: `productId` is required.");
|
|
115808
116216
|
const resolved = await resolveTeamId(input.teamId);
|
|
115809
116217
|
if (!resolved.ok) return resolved.response;
|
|
115810
116218
|
const existing = await loadProductInTeam(productId, resolved.teamId);
|
|
115811
116219
|
if (!existing) {
|
|
115812
|
-
return
|
|
116220
|
+
return textResponse6(
|
|
115813
116221
|
`Product ${productId} not found, or it is not owned by this team.`
|
|
115814
116222
|
);
|
|
115815
116223
|
}
|
|
115816
116224
|
if (!existing.isActive) {
|
|
115817
|
-
return
|
|
116225
|
+
return textResponse6(
|
|
115818
116226
|
`Product "${existing.name}" (${existing.id}) is already archived.`
|
|
115819
116227
|
);
|
|
115820
116228
|
}
|
|
115821
116229
|
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
|
|
116230
|
+
if (!archived) return textResponse6(`Failed to archive product ${productId}.`);
|
|
116231
|
+
return textResponse6(
|
|
115824
116232
|
`\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
|
|
115825
116233
|
|
|
115826
116234
|
${formatProduct(archived)}${reason ? `Reason: ${reason}
|
|
@@ -116870,7 +117278,7 @@ var QUOTE_STATUSES = [
|
|
|
116870
117278
|
"expired"
|
|
116871
117279
|
];
|
|
116872
117280
|
var SAFE_DRAFT_STATUSES = /* @__PURE__ */ new Set(["draft"]);
|
|
116873
|
-
function
|
|
117281
|
+
function textResponse7(text3) {
|
|
116874
117282
|
return { content: [{ type: "text", text: text3 }] };
|
|
116875
117283
|
}
|
|
116876
117284
|
async function loadTemplateDefaults(teamId) {
|
|
@@ -117040,14 +117448,14 @@ function tiptapNote2(text3) {
|
|
|
117040
117448
|
async function handleGetQuotes(input) {
|
|
117041
117449
|
const { customerId, status, q: q3, pageSize = 20 } = input;
|
|
117042
117450
|
if (status && !QUOTE_STATUSES.includes(status)) {
|
|
117043
|
-
return
|
|
117451
|
+
return textResponse7(
|
|
117044
117452
|
`Error: invalid status "${status}". Allowed: ${QUOTE_STATUSES.join(", ")}.`
|
|
117045
117453
|
);
|
|
117046
117454
|
}
|
|
117047
117455
|
const scope = await resolveTeamScope(input.teamId);
|
|
117048
117456
|
if (!scope.ok) return scope.response;
|
|
117049
117457
|
if (scope.teamIds.length === 0) {
|
|
117050
|
-
return
|
|
117458
|
+
return textResponse7("No accessible teams found.");
|
|
117051
117459
|
}
|
|
117052
117460
|
const filters = [inArray(schema_exports.quotations.teamId, scope.teamIds)];
|
|
117053
117461
|
if (customerId) filters.push(eq(schema_exports.quotations.customerId, customerId));
|
|
@@ -117062,10 +117470,10 @@ async function handleGetQuotes(input) {
|
|
|
117062
117470
|
}
|
|
117063
117471
|
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
117472
|
if (rows.length === 0) {
|
|
117065
|
-
return
|
|
117473
|
+
return textResponse7("No quotes found.");
|
|
117066
117474
|
}
|
|
117067
117475
|
const note = input.projectId ? "\nNote: `projectId` was ignored \u2014 quotations are not linked to projects." : "";
|
|
117068
|
-
return
|
|
117476
|
+
return textResponse7(
|
|
117069
117477
|
`Found ${rows.length} quote(s):
|
|
117070
117478
|
|
|
117071
117479
|
${rows.map(formatQuote).join("\n")}${note}`
|
|
@@ -117073,10 +117481,10 @@ ${rows.map(formatQuote).join("\n")}${note}`
|
|
|
117073
117481
|
}
|
|
117074
117482
|
async function handleCreateQuote(input) {
|
|
117075
117483
|
const { customerId } = input;
|
|
117076
|
-
if (!customerId) return
|
|
117484
|
+
if (!customerId) return textResponse7("Error: `customerId` is required.");
|
|
117077
117485
|
const status = input.status ?? "draft";
|
|
117078
117486
|
if (!SAFE_DRAFT_STATUSES.has(status)) {
|
|
117079
|
-
return
|
|
117487
|
+
return textResponse7(
|
|
117080
117488
|
`Error: this tool only creates draft quotes. Requested status "${status}" is not allowed. Sending/accepting a quote is a manual dashboard action.`
|
|
117081
117489
|
);
|
|
117082
117490
|
}
|
|
@@ -117099,7 +117507,7 @@ async function handleCreateQuote(input) {
|
|
|
117099
117507
|
)
|
|
117100
117508
|
).limit(1);
|
|
117101
117509
|
if (!customer) {
|
|
117102
|
-
return
|
|
117510
|
+
return textResponse7(
|
|
117103
117511
|
`Customer ${customerId} not found or not owned by this team.`
|
|
117104
117512
|
);
|
|
117105
117513
|
}
|
|
@@ -117109,7 +117517,7 @@ async function handleCreateQuote(input) {
|
|
|
117109
117517
|
defaults,
|
|
117110
117518
|
teamId
|
|
117111
117519
|
);
|
|
117112
|
-
if (error49) return
|
|
117520
|
+
if (error49) return textResponse7(`Error: ${error49}`);
|
|
117113
117521
|
const totals = computeTotals(items, defaults);
|
|
117114
117522
|
const quotationNumber = await nextQuotationNumber(teamId);
|
|
117115
117523
|
const template = buildQuoteTemplate(defaults, input.title);
|
|
@@ -117184,8 +117592,8 @@ async function handleCreateQuote(input) {
|
|
|
117184
117592
|
tax: totals.tax,
|
|
117185
117593
|
amount: totals.amount
|
|
117186
117594
|
}).returning(QUOTE_COLUMNS);
|
|
117187
|
-
if (!created) return
|
|
117188
|
-
return
|
|
117595
|
+
if (!created) return textResponse7("Failed to create quote.");
|
|
117596
|
+
return textResponse7(
|
|
117189
117597
|
`\u2705 **Draft quote created**
|
|
117190
117598
|
|
|
117191
117599
|
${formatQuote(created)}
|
|
@@ -117203,15 +117611,15 @@ async function loadQuoteInTeam(id, teamId) {
|
|
|
117203
117611
|
return row ?? null;
|
|
117204
117612
|
}
|
|
117205
117613
|
function notDraftResponse2(quote) {
|
|
117206
|
-
return
|
|
117614
|
+
return textResponse7(
|
|
117207
117615
|
`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
117616
|
);
|
|
117209
117617
|
}
|
|
117210
117618
|
async function handleUpdateQuote(input) {
|
|
117211
117619
|
const { id } = input;
|
|
117212
|
-
if (!id) return
|
|
117620
|
+
if (!id) return textResponse7("Error: `id` is required.");
|
|
117213
117621
|
if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
|
|
117214
|
-
return
|
|
117622
|
+
return textResponse7(
|
|
117215
117623
|
`Error: status can only stay within {${[...SAFE_DRAFT_STATUSES].join(", ")}}. "${input.status}" (send/accept/reject/expire) must be done manually from the dashboard.`
|
|
117216
117624
|
);
|
|
117217
117625
|
}
|
|
@@ -117219,7 +117627,7 @@ async function handleUpdateQuote(input) {
|
|
|
117219
117627
|
if (!resolved.ok) return resolved.response;
|
|
117220
117628
|
const quote = await loadQuoteInTeam(id, resolved.teamId);
|
|
117221
117629
|
if (!quote) {
|
|
117222
|
-
return
|
|
117630
|
+
return textResponse7(`Quote ${id} not found or not owned by this team.`);
|
|
117223
117631
|
}
|
|
117224
117632
|
if (quote.status !== "draft") return notDraftResponse2(quote);
|
|
117225
117633
|
const defaults = templateDefaultsFromStored(quote.template, quote.currency);
|
|
@@ -117239,7 +117647,7 @@ async function handleUpdateQuote(input) {
|
|
|
117239
117647
|
defaults,
|
|
117240
117648
|
quote.teamId
|
|
117241
117649
|
);
|
|
117242
|
-
if (error49) return
|
|
117650
|
+
if (error49) return textResponse7(`Error: ${error49}`);
|
|
117243
117651
|
const totals = computeTotals(items, defaults);
|
|
117244
117652
|
updates.lineItems = items;
|
|
117245
117653
|
updates.subtotal = totals.subtotal;
|
|
@@ -117251,32 +117659,32 @@ async function handleUpdateQuote(input) {
|
|
|
117251
117659
|
});
|
|
117252
117660
|
}
|
|
117253
117661
|
if (Object.keys(updates).length === 0) {
|
|
117254
|
-
return
|
|
117662
|
+
return textResponse7(
|
|
117255
117663
|
"No fields to update. Provide at least one of: title, description, validUntil, lineItems."
|
|
117256
117664
|
);
|
|
117257
117665
|
}
|
|
117258
117666
|
updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
117259
117667
|
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
|
|
117668
|
+
if (!updated) return textResponse7(`Failed to update quote ${id}.`);
|
|
117669
|
+
return textResponse7(`\u2705 **Draft quote updated**
|
|
117262
117670
|
|
|
117263
117671
|
${formatQuote(updated)}`);
|
|
117264
117672
|
}
|
|
117265
117673
|
async function handleAddProductToQuote(input) {
|
|
117266
117674
|
const { quoteId, productId } = input;
|
|
117267
|
-
if (!quoteId) return
|
|
117268
|
-
if (!productId) return
|
|
117675
|
+
if (!quoteId) return textResponse7("Error: `quoteId` is required.");
|
|
117676
|
+
if (!productId) return textResponse7("Error: `productId` is required.");
|
|
117269
117677
|
const resolved = await resolveTeamId(input.teamId);
|
|
117270
117678
|
if (!resolved.ok) return resolved.response;
|
|
117271
117679
|
const quote = await loadQuoteInTeam(quoteId, resolved.teamId);
|
|
117272
117680
|
if (!quote) {
|
|
117273
|
-
return
|
|
117681
|
+
return textResponse7(`Quote ${quoteId} not found or not owned by this team.`);
|
|
117274
117682
|
}
|
|
117275
117683
|
if (quote.status !== "draft") return notDraftResponse2(quote);
|
|
117276
117684
|
const products = await loadProductsInTeam2([productId], quote.teamId);
|
|
117277
117685
|
const product = products.get(productId);
|
|
117278
117686
|
if (!product) {
|
|
117279
|
-
return
|
|
117687
|
+
return textResponse7(
|
|
117280
117688
|
`Product ${productId} not found or not owned by this team.`
|
|
117281
117689
|
);
|
|
117282
117690
|
}
|
|
@@ -117306,7 +117714,7 @@ async function handleAddProductToQuote(input) {
|
|
|
117306
117714
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
117307
117715
|
}).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
|
|
117308
117716
|
if (!updated) {
|
|
117309
|
-
return
|
|
117717
|
+
return textResponse7(`Failed to add product to quote ${quoteId}.`);
|
|
117310
117718
|
}
|
|
117311
117719
|
await db.update(schema_exports.invoiceProducts).set({
|
|
117312
117720
|
usageCount: sql`${schema_exports.invoiceProducts.usageCount} + 1`,
|
|
@@ -117322,7 +117730,7 @@ async function handleAddProductToQuote(input) {
|
|
|
117322
117730
|
if (meta5.includedItems && meta5.includedItems.length > 0) {
|
|
117323
117731
|
metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
|
|
117324
117732
|
}
|
|
117325
|
-
return
|
|
117733
|
+
return textResponse7(
|
|
117326
117734
|
`\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
|
|
117327
117735
|
|
|
117328
117736
|
Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
|
|
@@ -123057,7 +123465,7 @@ function formatDeleteAttachmentRefusal(reason, context2) {
|
|
|
123057
123465
|
}
|
|
123058
123466
|
|
|
123059
123467
|
// src/tools/ticket-attachments.ts
|
|
123060
|
-
function
|
|
123468
|
+
function textResponse8(text3) {
|
|
123061
123469
|
return { content: [{ type: "text", text: text3 }] };
|
|
123062
123470
|
}
|
|
123063
123471
|
async function findAttachment(attachmentId) {
|
|
@@ -123130,7 +123538,7 @@ ${url3}`
|
|
|
123130
123538
|
async function handleUploadTicketAttachment(input) {
|
|
123131
123539
|
const ctx = getAuthContext() ?? authContext;
|
|
123132
123540
|
if (!ctx) {
|
|
123133
|
-
return
|
|
123541
|
+
return textResponse8("Error: Not authenticated.");
|
|
123134
123542
|
}
|
|
123135
123543
|
const access = await loadAccessibleTicket(input.teamId, input.ticketId);
|
|
123136
123544
|
if (!access.ok) return access.response;
|
|
@@ -123146,12 +123554,12 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123146
123554
|
userId: ctx.userId
|
|
123147
123555
|
});
|
|
123148
123556
|
if (!resolved.ok) {
|
|
123149
|
-
return
|
|
123557
|
+
return textResponse8(resolved.message);
|
|
123150
123558
|
}
|
|
123151
123559
|
const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
|
|
123152
123560
|
const validationError = validateAttachmentBuffer(buffer2, mimeType);
|
|
123153
123561
|
if (validationError) {
|
|
123154
|
-
return
|
|
123562
|
+
return textResponse8(validationError.message);
|
|
123155
123563
|
}
|
|
123156
123564
|
const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
|
|
123157
123565
|
try {
|
|
@@ -123162,7 +123570,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123162
123570
|
options: { contentType: mimeType, upsert: true }
|
|
123163
123571
|
});
|
|
123164
123572
|
} catch (error49) {
|
|
123165
|
-
return
|
|
123573
|
+
return textResponse8(
|
|
123166
123574
|
`Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
|
|
123167
123575
|
);
|
|
123168
123576
|
}
|
|
@@ -123191,7 +123599,7 @@ async function handleUploadTicketAttachment(input) {
|
|
|
123191
123599
|
url3 = signed.url;
|
|
123192
123600
|
} catch {
|
|
123193
123601
|
}
|
|
123194
|
-
return
|
|
123602
|
+
return textResponse8(
|
|
123195
123603
|
`\u{1F4CE} **Attached to ${ticket.ticketNumber}**
|
|
123196
123604
|
File: ${fileName}
|
|
123197
123605
|
Type: ${mimeType}
|
|
@@ -123205,18 +123613,18 @@ ${url3}` : "")
|
|
|
123205
123613
|
async function handleDeleteTicketAttachment(input) {
|
|
123206
123614
|
const ctx = getAuthContext() ?? authContext;
|
|
123207
123615
|
if (!ctx) {
|
|
123208
|
-
return
|
|
123616
|
+
return textResponse8("Error: Not authenticated.");
|
|
123209
123617
|
}
|
|
123210
123618
|
const inputError = validateDeleteAttachmentInput(input.attachmentId);
|
|
123211
123619
|
if (inputError) {
|
|
123212
|
-
return
|
|
123620
|
+
return textResponse8(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
|
|
123213
123621
|
}
|
|
123214
123622
|
const access = await loadAccessibleTicket(input.teamId, input.ticketId);
|
|
123215
123623
|
if (!access.ok) return access.response;
|
|
123216
123624
|
const ticket = access.ticket;
|
|
123217
123625
|
const attachment = await findAttachment(input.attachmentId);
|
|
123218
123626
|
if (!attachment) {
|
|
123219
|
-
return
|
|
123627
|
+
return textResponse8(
|
|
123220
123628
|
formatDeleteAttachmentRefusal("attachment_not_found", {
|
|
123221
123629
|
attachmentId: input.attachmentId,
|
|
123222
123630
|
ticketNumber: ticket.ticketNumber
|
|
@@ -123224,7 +123632,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123224
123632
|
);
|
|
123225
123633
|
}
|
|
123226
123634
|
if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
|
|
123227
|
-
return
|
|
123635
|
+
return textResponse8(
|
|
123228
123636
|
formatDeleteAttachmentRefusal("wrong_ticket", {
|
|
123229
123637
|
attachmentId: input.attachmentId,
|
|
123230
123638
|
ticketNumber: ticket.ticketNumber,
|
|
@@ -123236,7 +123644,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123236
123644
|
const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
|
|
123237
123645
|
const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
|
|
123238
123646
|
if (!deletedRow) {
|
|
123239
|
-
return
|
|
123647
|
+
return textResponse8(
|
|
123240
123648
|
`Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
|
|
123241
123649
|
);
|
|
123242
123650
|
}
|
|
@@ -123266,7 +123674,7 @@ async function handleDeleteTicketAttachment(input) {
|
|
|
123266
123674
|
fileName: attachment.fileName,
|
|
123267
123675
|
source: attachment.source
|
|
123268
123676
|
});
|
|
123269
|
-
return
|
|
123677
|
+
return textResponse8(JSON.stringify(result, null, 2));
|
|
123270
123678
|
}
|
|
123271
123679
|
|
|
123272
123680
|
// src/tools/tiptap-text.ts
|
|
@@ -123683,7 +124091,7 @@ function formatTagUsage(usage) {
|
|
|
123683
124091
|
}
|
|
123684
124092
|
|
|
123685
124093
|
// src/tools/tag-management.ts
|
|
123686
|
-
function
|
|
124094
|
+
function textResponse9(text3) {
|
|
123687
124095
|
return { content: [{ type: "text", text: text3 }] };
|
|
123688
124096
|
}
|
|
123689
124097
|
var TAG_COLUMNS = {
|
|
@@ -123724,24 +124132,24 @@ function scopeFilter(projectId) {
|
|
|
123724
124132
|
return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
|
|
123725
124133
|
}
|
|
123726
124134
|
async function handleUpdateTag(input) {
|
|
123727
|
-
if (!input.tagId) return
|
|
124135
|
+
if (!input.tagId) return textResponse9("Error: `tagId` is required.");
|
|
123728
124136
|
const resolved = await resolveTeamId(input.teamId);
|
|
123729
124137
|
if (!resolved.ok) return resolved.response;
|
|
123730
124138
|
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
123731
124139
|
if (!existing) {
|
|
123732
|
-
return
|
|
124140
|
+
return textResponse9(
|
|
123733
124141
|
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
123734
124142
|
);
|
|
123735
124143
|
}
|
|
123736
124144
|
const renaming = input.name !== void 0;
|
|
123737
124145
|
const rescoping = input.projectId !== void 0;
|
|
123738
124146
|
if (!renaming && !rescoping) {
|
|
123739
|
-
return
|
|
124147
|
+
return textResponse9(
|
|
123740
124148
|
"No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
|
|
123741
124149
|
);
|
|
123742
124150
|
}
|
|
123743
124151
|
if (renaming && !isValidTagName(input.name)) {
|
|
123744
|
-
return
|
|
124152
|
+
return textResponse9("Error: `name` cannot be empty.");
|
|
123745
124153
|
}
|
|
123746
124154
|
const nextName = renaming ? input.name.trim() : existing.name;
|
|
123747
124155
|
const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
|
|
@@ -123754,13 +124162,13 @@ async function handleUpdateTag(input) {
|
|
|
123754
124162
|
)
|
|
123755
124163
|
).limit(1);
|
|
123756
124164
|
if (collision) {
|
|
123757
|
-
return
|
|
124165
|
+
return textResponse9(
|
|
123758
124166
|
`\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
124167
|
);
|
|
123760
124168
|
}
|
|
123761
124169
|
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
|
|
124170
|
+
if (!updated) return textResponse9(`Failed to update tag ${input.tagId}.`);
|
|
124171
|
+
return textResponse9(
|
|
123764
124172
|
`\u2705 **Tag updated**
|
|
123765
124173
|
|
|
123766
124174
|
${describeTag(updated)}
|
|
@@ -123769,34 +124177,34 @@ Existing ticket/customer/project/transaction tag relations are preserved.`
|
|
|
123769
124177
|
);
|
|
123770
124178
|
}
|
|
123771
124179
|
async function handleDeleteTag(input) {
|
|
123772
|
-
if (!input.tagId) return
|
|
124180
|
+
if (!input.tagId) return textResponse9("Error: `tagId` is required.");
|
|
123773
124181
|
const mode = input.mode ?? "delete_if_unused";
|
|
123774
124182
|
const resolved = await resolveTeamId(input.teamId);
|
|
123775
124183
|
if (!resolved.ok) return resolved.response;
|
|
123776
124184
|
const existing = await loadTagInTeam(input.tagId, resolved.teamId);
|
|
123777
124185
|
if (!existing) {
|
|
123778
|
-
return
|
|
124186
|
+
return textResponse9(
|
|
123779
124187
|
`Tag ${input.tagId} not found, or it is not owned by this team.`
|
|
123780
124188
|
);
|
|
123781
124189
|
}
|
|
123782
124190
|
const usage = await getTagUsage(existing.id);
|
|
123783
124191
|
const total = totalTagUsage(usage);
|
|
123784
124192
|
if (mode === "archive") {
|
|
123785
|
-
return
|
|
124193
|
+
return textResponse9(
|
|
123786
124194
|
`\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
|
|
123787
124195
|
|
|
123788
124196
|
Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
|
|
123789
124197
|
);
|
|
123790
124198
|
}
|
|
123791
124199
|
if (total > 0) {
|
|
123792
|
-
return
|
|
124200
|
+
return textResponse9(
|
|
123793
124201
|
`\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
|
|
123794
124202
|
|
|
123795
124203
|
Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
|
|
123796
124204
|
);
|
|
123797
124205
|
}
|
|
123798
124206
|
await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
|
|
123799
|
-
return
|
|
124207
|
+
return textResponse9(
|
|
123800
124208
|
`\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
|
|
123801
124209
|
);
|
|
123802
124210
|
}
|
|
@@ -123806,7 +124214,7 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123806
124214
|
if (!tag2) {
|
|
123807
124215
|
return {
|
|
123808
124216
|
ok: false,
|
|
123809
|
-
response:
|
|
124217
|
+
response: textResponse9(
|
|
123810
124218
|
`Target tag ${input.targetTagId} not found, or it is not owned by this team.`
|
|
123811
124219
|
)
|
|
123812
124220
|
};
|
|
@@ -123816,7 +124224,7 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123816
124224
|
if (!isValidTagName(input.targetName)) {
|
|
123817
124225
|
return {
|
|
123818
124226
|
ok: false,
|
|
123819
|
-
response:
|
|
124227
|
+
response: textResponse9(
|
|
123820
124228
|
"Error: provide either `targetTagId` or a non-empty `targetName`."
|
|
123821
124229
|
)
|
|
123822
124230
|
};
|
|
@@ -123834,14 +124242,14 @@ async function resolveMergeTarget(teamId, input) {
|
|
|
123834
124242
|
}
|
|
123835
124243
|
const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
|
|
123836
124244
|
if (!created) {
|
|
123837
|
-
return { ok: false, response:
|
|
124245
|
+
return { ok: false, response: textResponse9("Failed to create target tag.") };
|
|
123838
124246
|
}
|
|
123839
124247
|
return { ok: true, tag: created, created: true };
|
|
123840
124248
|
}
|
|
123841
124249
|
async function handleMergeTags(input) {
|
|
123842
124250
|
const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
|
|
123843
124251
|
if (rawSourceIds.length === 0) {
|
|
123844
|
-
return
|
|
124252
|
+
return textResponse9("Error: `sourceTagIds` must contain at least one tag id.");
|
|
123845
124253
|
}
|
|
123846
124254
|
const resolved = await resolveTeamId(input.teamId);
|
|
123847
124255
|
if (!resolved.ok) return resolved.response;
|
|
@@ -123855,7 +124263,7 @@ async function handleMergeTags(input) {
|
|
|
123855
124263
|
const foundIds = new Set(sourceTags.map((t8) => t8.id));
|
|
123856
124264
|
const missing = rawSourceIds.filter((id) => !foundIds.has(id));
|
|
123857
124265
|
if (missing.length > 0) {
|
|
123858
|
-
return
|
|
124266
|
+
return textResponse9(
|
|
123859
124267
|
`Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
|
|
123860
124268
|
);
|
|
123861
124269
|
}
|
|
@@ -123863,7 +124271,7 @@ async function handleMergeTags(input) {
|
|
|
123863
124271
|
if (!target.ok) return target.response;
|
|
123864
124272
|
const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
|
|
123865
124273
|
if (sourcesToMerge.length === 0) {
|
|
123866
|
-
return
|
|
124274
|
+
return textResponse9(
|
|
123867
124275
|
"Error: nothing to merge \u2014 the only source tag is the same as the target tag."
|
|
123868
124276
|
);
|
|
123869
124277
|
}
|
|
@@ -123960,7 +124368,7 @@ async function handleMergeTags(input) {
|
|
|
123960
124368
|
const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
|
|
123961
124369
|
const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
|
|
123962
124370
|
const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
|
|
123963
|
-
return
|
|
124371
|
+
return textResponse9(
|
|
123964
124372
|
`\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
|
|
123965
124373
|
|
|
123966
124374
|
Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
|
|
@@ -124346,15 +124754,15 @@ function attemptedLockedFields(update) {
|
|
|
124346
124754
|
// src/tools/trips.ts
|
|
124347
124755
|
var TRIP_TYPES = ["private", "business"];
|
|
124348
124756
|
var BILLING_TYPES2 = TRIP_BILLING_TYPES;
|
|
124349
|
-
function
|
|
124757
|
+
function textResponse10(text3) {
|
|
124350
124758
|
return { content: [{ type: "text", text: text3 }] };
|
|
124351
124759
|
}
|
|
124352
|
-
function
|
|
124760
|
+
function jsonResponse2(payload) {
|
|
124353
124761
|
return {
|
|
124354
124762
|
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
124355
124763
|
};
|
|
124356
124764
|
}
|
|
124357
|
-
function
|
|
124765
|
+
function toNumber3(value) {
|
|
124358
124766
|
if (value == null) return 0;
|
|
124359
124767
|
if (typeof value === "number") return value;
|
|
124360
124768
|
const parsed = Number.parseFloat(String(value));
|
|
@@ -124367,9 +124775,9 @@ function formatTrip(t8) {
|
|
|
124367
124775
|
startLocation: t8.startLocation,
|
|
124368
124776
|
endLocation: t8.endLocation,
|
|
124369
124777
|
tripType: t8.tripType,
|
|
124370
|
-
distance: t8.distance != null ?
|
|
124371
|
-
odometerStart: t8.odometerStart != null ?
|
|
124372
|
-
odometerEnd: t8.odometerEnd != null ?
|
|
124778
|
+
distance: t8.distance != null ? toNumber3(t8.distance) : null,
|
|
124779
|
+
odometerStart: t8.odometerStart != null ? toNumber3(t8.odometerStart) : null,
|
|
124780
|
+
odometerEnd: t8.odometerEnd != null ? toNumber3(t8.odometerEnd) : null,
|
|
124373
124781
|
billingType: t8.billingType,
|
|
124374
124782
|
rate: t8.rate,
|
|
124375
124783
|
amount: t8.amount,
|
|
@@ -124402,19 +124810,19 @@ var TRIP_RELATIONS = {
|
|
|
124402
124810
|
};
|
|
124403
124811
|
async function handleGetTrips(input) {
|
|
124404
124812
|
if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
|
|
124405
|
-
return
|
|
124813
|
+
return textResponse10(
|
|
124406
124814
|
`Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
|
|
124407
124815
|
);
|
|
124408
124816
|
}
|
|
124409
124817
|
if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
|
|
124410
|
-
return
|
|
124818
|
+
return textResponse10(
|
|
124411
124819
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124412
124820
|
);
|
|
124413
124821
|
}
|
|
124414
124822
|
const scope = await resolveTeamScope(input.teamId);
|
|
124415
124823
|
if (!scope.ok) return scope.response;
|
|
124416
124824
|
if (scope.teamIds.length === 0) {
|
|
124417
|
-
return
|
|
124825
|
+
return textResponse10("No accessible teams found.");
|
|
124418
124826
|
}
|
|
124419
124827
|
const filters = [inArray(schema_exports.trips.teamId, scope.teamIds)];
|
|
124420
124828
|
if (input.dateFrom) filters.push(gte(schema_exports.trips.date, input.dateFrom));
|
|
@@ -124446,7 +124854,7 @@ async function handleGetTrips(input) {
|
|
|
124446
124854
|
});
|
|
124447
124855
|
const totals = rows.reduce(
|
|
124448
124856
|
(acc, t8) => {
|
|
124449
|
-
const distance =
|
|
124857
|
+
const distance = toNumber3(t8.distance);
|
|
124450
124858
|
const amount = t8.amount ?? 0;
|
|
124451
124859
|
if (t8.tripType === "business") acc.businessKm += distance;
|
|
124452
124860
|
else acc.privateKm += distance;
|
|
@@ -124456,7 +124864,7 @@ async function handleGetTrips(input) {
|
|
|
124456
124864
|
},
|
|
124457
124865
|
{ businessKm: 0, privateKm: 0, totalKm: 0, totalAmount: 0 }
|
|
124458
124866
|
);
|
|
124459
|
-
return
|
|
124867
|
+
return jsonResponse2({
|
|
124460
124868
|
count: rows.length,
|
|
124461
124869
|
totals: {
|
|
124462
124870
|
businessKm: round23(totals.businessKm),
|
|
@@ -124516,20 +124924,20 @@ async function validateInvoice(invoiceId, teamId) {
|
|
|
124516
124924
|
}
|
|
124517
124925
|
async function handleCreateTrip(input) {
|
|
124518
124926
|
const ctx = getAuthContext();
|
|
124519
|
-
if (!input.date) return
|
|
124927
|
+
if (!input.date) return textResponse10("Error: `date` (YYYY-MM-DD) is required.");
|
|
124520
124928
|
if (!input.startLocation || !input.endLocation) {
|
|
124521
|
-
return
|
|
124929
|
+
return textResponse10(
|
|
124522
124930
|
"Error: `startLocation` and `endLocation` are required."
|
|
124523
124931
|
);
|
|
124524
124932
|
}
|
|
124525
124933
|
if (!input.tripType || !TRIP_TYPES.includes(input.tripType)) {
|
|
124526
|
-
return
|
|
124934
|
+
return textResponse10(
|
|
124527
124935
|
`Error: \`tripType\` is required and must be one of: ${TRIP_TYPES.join(", ")}.`
|
|
124528
124936
|
);
|
|
124529
124937
|
}
|
|
124530
124938
|
const billingType = input.billingType ?? "not_billable";
|
|
124531
124939
|
if (!BILLING_TYPES2.includes(billingType)) {
|
|
124532
|
-
return
|
|
124940
|
+
return textResponse10(
|
|
124533
124941
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124534
124942
|
);
|
|
124535
124943
|
}
|
|
@@ -124541,7 +124949,7 @@ async function handleCreateTrip(input) {
|
|
|
124541
124949
|
customerId: input.customerId,
|
|
124542
124950
|
vehicleId: input.vehicleId
|
|
124543
124951
|
});
|
|
124544
|
-
if (linkError) return
|
|
124952
|
+
if (linkError) return textResponse10(`Error: ${linkError}`);
|
|
124545
124953
|
if (!input.allowDuplicate) {
|
|
124546
124954
|
const dupFilters = [
|
|
124547
124955
|
eq(schema_exports.trips.teamId, teamId),
|
|
@@ -124558,8 +124966,8 @@ async function handleCreateTrip(input) {
|
|
|
124558
124966
|
}
|
|
124559
124967
|
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
124968
|
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 ? ` (${
|
|
124969
|
+
return textResponse10(
|
|
124970
|
+
`\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
124971
|
);
|
|
124564
124972
|
}
|
|
124565
124973
|
}
|
|
@@ -124588,7 +124996,7 @@ async function handleCreateTrip(input) {
|
|
|
124588
124996
|
vehicleId: input.vehicleId ?? null,
|
|
124589
124997
|
snapshotId: input.snapshotId ?? null
|
|
124590
124998
|
}).returning({ id: schema_exports.trips.id });
|
|
124591
|
-
if (!created) return
|
|
124999
|
+
if (!created) return textResponse10("Failed to create trip.");
|
|
124592
125000
|
const trip = await loadTripInTeams(created.id, [teamId]);
|
|
124593
125001
|
return {
|
|
124594
125002
|
content: [
|
|
@@ -124603,14 +125011,14 @@ ${JSON.stringify(formatTrip(trip), null, 2)}`
|
|
|
124603
125011
|
}
|
|
124604
125012
|
async function handleUpdateTrip(input) {
|
|
124605
125013
|
const ctx = getAuthContext();
|
|
124606
|
-
if (!input.id) return
|
|
125014
|
+
if (!input.id) return textResponse10("Error: `id` is required.");
|
|
124607
125015
|
if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
|
|
124608
|
-
return
|
|
125016
|
+
return textResponse10(
|
|
124609
125017
|
`Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
|
|
124610
125018
|
);
|
|
124611
125019
|
}
|
|
124612
125020
|
if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
|
|
124613
|
-
return
|
|
125021
|
+
return textResponse10(
|
|
124614
125022
|
`Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
|
|
124615
125023
|
);
|
|
124616
125024
|
}
|
|
@@ -124619,7 +125027,7 @@ async function handleUpdateTrip(input) {
|
|
|
124619
125027
|
const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
|
|
124620
125028
|
const existing = await loadTripInTeams(input.id, accessibleTeamIds);
|
|
124621
125029
|
if (!existing) {
|
|
124622
|
-
return
|
|
125030
|
+
return textResponse10(
|
|
124623
125031
|
`Trip ${input.id} not found or you don't have access to it. Call get-trips to find a valid id.`
|
|
124624
125032
|
);
|
|
124625
125033
|
}
|
|
@@ -124630,7 +125038,7 @@ async function handleUpdateTrip(input) {
|
|
|
124630
125038
|
input
|
|
124631
125039
|
);
|
|
124632
125040
|
if (attempted.length > 0) {
|
|
124633
|
-
return
|
|
125041
|
+
return textResponse10(
|
|
124634
125042
|
`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
125043
|
);
|
|
124636
125044
|
}
|
|
@@ -124640,10 +125048,10 @@ async function handleUpdateTrip(input) {
|
|
|
124640
125048
|
customerId: input.customerId ?? void 0,
|
|
124641
125049
|
vehicleId: input.vehicleId ?? void 0
|
|
124642
125050
|
});
|
|
124643
|
-
if (linkError) return
|
|
125051
|
+
if (linkError) return textResponse10(`Error: ${linkError}`);
|
|
124644
125052
|
if (input.invoiceId) {
|
|
124645
125053
|
const invoiceError = await validateInvoice(input.invoiceId, teamId);
|
|
124646
|
-
if (invoiceError) return
|
|
125054
|
+
if (invoiceError) return textResponse10(`Error: ${invoiceError}`);
|
|
124647
125055
|
}
|
|
124648
125056
|
const updates = {};
|
|
124649
125057
|
if (input.date !== void 0) updates.date = input.date;
|
|
@@ -124680,7 +125088,7 @@ async function handleUpdateTrip(input) {
|
|
|
124680
125088
|
if (input.isInvoiced !== void 0) updates.isInvoiced = input.isInvoiced;
|
|
124681
125089
|
if (input.amount === void 0 && (input.distance !== void 0 || input.rate !== void 0 || input.billingType !== void 0)) {
|
|
124682
125090
|
const nextBilling = input.billingType ?? existing.billingType;
|
|
124683
|
-
const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ?
|
|
125091
|
+
const nextDistance = input.distance !== void 0 ? input.distance : existing.distance != null ? toNumber3(existing.distance) : null;
|
|
124684
125092
|
const nextRate = input.rate !== void 0 ? input.rate : existing.rate;
|
|
124685
125093
|
const derived = deriveTripAmount({
|
|
124686
125094
|
billingType: nextBilling,
|
|
@@ -124691,7 +125099,7 @@ async function handleUpdateTrip(input) {
|
|
|
124691
125099
|
if (derived != null) updates.amount = derived;
|
|
124692
125100
|
}
|
|
124693
125101
|
if (Object.keys(updates).length === 0) {
|
|
124694
|
-
return
|
|
125102
|
+
return textResponse10(
|
|
124695
125103
|
"No fields to update. Provide at least one editable field."
|
|
124696
125104
|
);
|
|
124697
125105
|
}
|
|
@@ -124718,7 +125126,7 @@ async function handleGetVehicles(input) {
|
|
|
124718
125126
|
const scope = await resolveTeamScope(input.teamId);
|
|
124719
125127
|
if (!scope.ok) return scope.response;
|
|
124720
125128
|
if (scope.teamIds.length === 0) {
|
|
124721
|
-
return
|
|
125129
|
+
return textResponse10("No accessible teams found.");
|
|
124722
125130
|
}
|
|
124723
125131
|
const filters = [inArray(schema_exports.vehicles.teamId, scope.teamIds)];
|
|
124724
125132
|
if (input.q) filters.push(ilike(schema_exports.vehicles.name, `%${input.q}%`));
|
|
@@ -124729,13 +125137,13 @@ async function handleGetVehicles(input) {
|
|
|
124729
125137
|
currentOdometer: schema_exports.vehicles.currentOdometer,
|
|
124730
125138
|
teamId: schema_exports.vehicles.teamId
|
|
124731
125139
|
}).from(schema_exports.vehicles).where(and(...filters)).orderBy(asc(schema_exports.vehicles.name)).limit(Math.min(input.pageSize ?? 50, 200));
|
|
124732
|
-
return
|
|
125140
|
+
return jsonResponse2({
|
|
124733
125141
|
count: rows.length,
|
|
124734
125142
|
vehicles: rows.map((v2) => ({
|
|
124735
125143
|
id: v2.id,
|
|
124736
125144
|
name: v2.name,
|
|
124737
125145
|
licensePlate: v2.licensePlate,
|
|
124738
|
-
currentOdometer: v2.currentOdometer != null ?
|
|
125146
|
+
currentOdometer: v2.currentOdometer != null ? toNumber3(v2.currentOdometer) : null
|
|
124739
125147
|
}))
|
|
124740
125148
|
});
|
|
124741
125149
|
}
|
|
@@ -124744,7 +125152,7 @@ async function handleGetTripTemplates(input) {
|
|
|
124744
125152
|
const scope = await resolveTeamScope(input.teamId);
|
|
124745
125153
|
if (!scope.ok) return scope.response;
|
|
124746
125154
|
if (scope.teamIds.length === 0) {
|
|
124747
|
-
return
|
|
125155
|
+
return textResponse10("No accessible teams found.");
|
|
124748
125156
|
}
|
|
124749
125157
|
const filters = [inArray(schema_exports.tripTemplates.teamId, scope.teamIds)];
|
|
124750
125158
|
const userId = input.userId ?? ctx.userId;
|
|
@@ -124768,26 +125176,26 @@ async function handleGetTripTemplates(input) {
|
|
|
124768
125176
|
vehicleId: schema_exports.tripTemplates.vehicleId,
|
|
124769
125177
|
notes: schema_exports.tripTemplates.notes
|
|
124770
125178
|
}).from(schema_exports.tripTemplates).where(and(...filters)).orderBy(asc(schema_exports.tripTemplates.name)).limit(Math.min(input.pageSize ?? 50, 200));
|
|
124771
|
-
return
|
|
125179
|
+
return jsonResponse2({
|
|
124772
125180
|
count: rows.length,
|
|
124773
125181
|
templates: rows.map((t8) => ({
|
|
124774
125182
|
...t8,
|
|
124775
|
-
distance: t8.distance != null ?
|
|
124776
|
-
returnDistance: t8.returnDistance != null ?
|
|
125183
|
+
distance: t8.distance != null ? toNumber3(t8.distance) : null,
|
|
125184
|
+
returnDistance: t8.returnDistance != null ? toNumber3(t8.returnDistance) : null
|
|
124777
125185
|
}))
|
|
124778
125186
|
});
|
|
124779
125187
|
}
|
|
124780
125188
|
async function handleGetFrequentTripsForProject(input) {
|
|
124781
125189
|
const ctx = getAuthContext();
|
|
124782
125190
|
if (!input.projectId) {
|
|
124783
|
-
return
|
|
125191
|
+
return textResponse10("Error: `projectId` is required.");
|
|
124784
125192
|
}
|
|
124785
125193
|
const resolved = await resolveTeamId(input.teamId);
|
|
124786
125194
|
if (!resolved.ok) return resolved.response;
|
|
124787
125195
|
const teamId = resolved.teamId;
|
|
124788
125196
|
const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
|
|
124789
125197
|
if (!projectIds.includes(input.projectId)) {
|
|
124790
|
-
return
|
|
125198
|
+
return textResponse10(
|
|
124791
125199
|
`Project not found or no access: ${input.projectId}. Call get-projects first.`
|
|
124792
125200
|
);
|
|
124793
125201
|
}
|
|
@@ -124814,7 +125222,7 @@ async function handleGetFrequentTripsForProject(input) {
|
|
|
124814
125222
|
schema_exports.trips.endLocation,
|
|
124815
125223
|
schema_exports.trips.tripType
|
|
124816
125224
|
).orderBy(desc(sql`count(*)`), desc(sql`max(${schema_exports.trips.date})`)).limit(limitN);
|
|
124817
|
-
return
|
|
125225
|
+
return jsonResponse2({
|
|
124818
125226
|
count: groups.length,
|
|
124819
125227
|
daysBack,
|
|
124820
125228
|
frequentTrips: groups.map((g6) => ({
|
|
@@ -124822,7 +125230,7 @@ async function handleGetFrequentTripsForProject(input) {
|
|
|
124822
125230
|
endLocation: g6.endLocation,
|
|
124823
125231
|
tripType: g6.tripType,
|
|
124824
125232
|
count: g6.count,
|
|
124825
|
-
avgDistance: g6.avgDistance != null ? round23(
|
|
125233
|
+
avgDistance: g6.avgDistance != null ? round23(toNumber3(g6.avgDistance)) : null,
|
|
124826
125234
|
lastUsedDate: g6.lastUsedDate
|
|
124827
125235
|
}))
|
|
124828
125236
|
});
|
|
@@ -125490,6 +125898,10 @@ function createMcpServer() {
|
|
|
125490
125898
|
);
|
|
125491
125899
|
case "log-hours":
|
|
125492
125900
|
return await handleLogHours(asToolArgs(toolArgs));
|
|
125901
|
+
case "get-time-entries":
|
|
125902
|
+
return await handleGetTimeEntries(
|
|
125903
|
+
asToolArgs(toolArgs)
|
|
125904
|
+
);
|
|
125493
125905
|
case "get-github-file":
|
|
125494
125906
|
return await handleGetGithubFile(asToolArgs(toolArgs));
|
|
125495
125907
|
case "list-github-directory":
|