Refine project and todo workflow

This commit is contained in:
michael committed 2026-09-02 11:23:34 +02:00
1 parent 3da436423f
commit 239be9f218
9 files changed
+487 -301

No files matched your search

+51 -19
View File
@@ -58,6 +58,19 @@ function optionalString(value: unknown) {
return trimmed.length > 0 ? trimmed : null;
}
function optionalNullableString(value: unknown) {
if (value === null || value === undefined) {
return null;
}
if (typeof value !== "string") {
throw badRequest("value must be a string or null");
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function optionalWorkType(value: unknown) {
if (value === undefined || value === null || value === "") {
return null;
@@ -148,6 +161,7 @@ type ProjectRow = {
id: string;
ticket_id: string;
ticket_number: string;
title: string | null;
organization_id: string | null;
organization_name: string | null;
customer_name: string | null;
@@ -637,6 +651,7 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
SELECT
t.id,
t.ticket_number,
t.title,
t.organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
@@ -662,6 +677,7 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
GROUP BY
t.id,
t.ticket_number,
t.title,
t.organization_id,
o.name,
t.customer_name,
@@ -1153,8 +1169,9 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
`
SELECT
t.id,
t.ticket_number,
t.organization_id,
t.ticket_number,
t.title,
t.organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
t.work_type,
@@ -1275,6 +1292,7 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
SELECT
t.id,
t.ticket_number,
t.title,
t.organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
@@ -1402,6 +1420,7 @@ async function getTicketDetails(ticketId: number, userId: string, filters: { mon
p.id::text AS id,
p.ticket_id::text AS ticket_id,
t.ticket_number,
t.title,
t.organization_id::text AS organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
@@ -3044,6 +3063,7 @@ app.get("/api/tickets/search", requireUser, async (req, res) => {
SELECT
t.id AS ticket_id,
t.ticket_number,
t.title,
t.organization_id,
COALESCE(o.name, t.customer_name, '') AS organization_name,
COALESCE(o.name, t.customer_name, '') AS customer_name,
@@ -3063,6 +3083,7 @@ app.get("/api/tickets/search", requireUser, async (req, res) => {
AND (
t.ticket_number ILIKE $2
OR replace(t.ticket_number, 'Ticket#', '') ILIKE $3
OR COALESCE(t.title, '') ILIKE $2
OR COALESCE(o.name, '') ILIKE $2
OR COALESCE(t.customer_name, '') ILIKE $2
OR (s.user_id = $1 AND COALESCE(s.customer_name, '') ILIKE $2)
@@ -3071,7 +3092,7 @@ app.get("/api/tickets/search", requireUser, async (req, res) => {
OR COALESCE(p.description, '') ILIKE $2
OR COALESCE(p.goals, '') ILIKE $2
)
GROUP BY t.id, t.ticket_number, t.organization_id, o.name, t.customer_name, t.work_type, p.id, p.teamspace_project
GROUP BY t.id, t.ticket_number, t.title, t.organization_id, o.name, t.customer_name, t.work_type, p.id, p.teamspace_project
ORDER BY
CASE
WHEN lower(t.ticket_number) = lower($4) THEN 0
@@ -3097,6 +3118,7 @@ app.get("/api/projects", requireUser, async (req, res) => {
p.id::text AS id,
p.ticket_id::text AS ticket_id,
t.ticket_number,
t.title,
t.organization_id::text AS organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
@@ -3150,6 +3172,7 @@ app.get("/api/projects/:projectId", requireUser, async (req, res) => {
p.id::text AS id,
p.ticket_id::text AS ticket_id,
t.ticket_number,
t.title,
t.organization_id::text AS organization_id,
o.name AS organization_name,
COALESCE(o.name, t.customer_name) AS customer_name,
@@ -3182,6 +3205,7 @@ app.get("/api/projects/:projectId", requireUser, async (req, res) => {
app.post("/api/projects", requireUser, async (req, res) => {
const userId = currentUser(req).id;
const ticketNumber = parseTicketNumber(req.body.ticketNumber);
const title = optionalNullableString(req.body.title);
const requestOrganizationId = req.body.organizationId === undefined || req.body.organizationId === "" ? null : parseOrganizationId(req.body.organizationId);
const teamspaceProject = optionalString(req.body.teamspaceProject);
const description = optionalString(req.body.description) ?? "";
@@ -3198,11 +3222,12 @@ app.post("/api/projects", requireUser, async (req, res) => {
const created = await withTransaction(async (client) => {
const existingTicket = await client.query<{
id: string;
title: string | null;
organization_id: string | null;
customer_name: string | null;
}>(
`
SELECT id, organization_id::text AS organization_id, customer_name
SELECT id, title, organization_id::text AS organization_id, customer_name
FROM tickets
WHERE ticket_number = $1
FOR UPDATE;
@@ -3224,16 +3249,17 @@ app.post("/api/projects", requireUser, async (req, res) => {
const ticketResult = await client.query<{ id: string }>(
`
INSERT INTO tickets (ticket_number, organization_id, customer_name, work_type)
VALUES ($1, $2, $3, 'consulting')
INSERT INTO tickets (ticket_number, title, organization_id, customer_name, work_type)
VALUES ($1, $2, $3, $4, 'consulting')
ON CONFLICT (ticket_number)
DO UPDATE SET
title = COALESCE(EXCLUDED.title, tickets.title),
organization_id = EXCLUDED.organization_id,
customer_name = EXCLUDED.customer_name,
work_type = 'consulting'
RETURNING id;
`,
[ticketNumber, organization.id, organization.name]
[ticketNumber, title, organization.id, organization.name]
);
const ticketId = ticketResult.rows[0].id;
@@ -3268,6 +3294,7 @@ app.post("/api/projects", requireUser, async (req, res) => {
id::text,
ticket_id::text,
$9::text AS ticket_number,
COALESCE($12::text, $13::text) AS title,
$10::text AS organization_id,
$11::text AS organization_name,
$11::text AS customer_name,
@@ -3281,7 +3308,7 @@ app.post("/api/projects", requireUser, async (req, res) => {
created_at,
updated_at;
`,
[userId, ticketId, teamspaceProject, description, goals, startsOn, endsOn, hourBudgetMinutes, ticketNumber, organization.id, organization.name]
[userId, ticketId, teamspaceProject, description, goals, startsOn, endsOn, hourBudgetMinutes, ticketNumber, organization.id, organization.name, title, existingTicket.rows[0]?.title ?? null]
);
return projectResult.rows[0];
@@ -3312,6 +3339,7 @@ app.patch("/api/projects/:projectId", requireUser, async (req, res) => {
const projectId = parsePositiveInteger(req.params.projectId, "projectId");
const userId = currentUser(req).id;
const ticketNumber = parseTicketNumber(req.body.ticketNumber);
const title = optionalNullableString(req.body.title);
const organizationId = parseOrganizationId(req.body.organizationId);
const teamspaceProject = optionalString(req.body.teamspaceProject);
const description = optionalString(req.body.description) ?? "";
@@ -3351,12 +3379,13 @@ app.patch("/api/projects/:projectId", requireUser, async (req, res) => {
`
UPDATE tickets
SET ticket_number = $1,
organization_id = $2,
customer_name = $3,
title = $2,
organization_id = $3,
customer_name = $4,
work_type = 'consulting'
WHERE id = $4;
WHERE id = $5;
`,
[ticketNumber, organization.id, organization.name, projectResult.rows[0].ticket_id]
[ticketNumber, title, organization.id, organization.name, projectResult.rows[0].ticket_id]
);
await client.query(
@@ -3387,6 +3416,7 @@ app.patch("/api/projects/:projectId", requireUser, async (req, res) => {
id::text,
ticket_id::text,
$9::text AS ticket_number,
$12::text AS title,
$10::text AS organization_id,
$11::text AS organization_name,
$11::text AS customer_name,
@@ -3400,7 +3430,7 @@ app.patch("/api/projects/:projectId", requireUser, async (req, res) => {
created_at,
updated_at;
`,
[teamspaceProject, description, goals, startsOn, endsOn, hourBudgetMinutes, projectId, userId, ticketNumber, organization.id, organization.name]
[teamspaceProject, description, goals, startsOn, endsOn, hourBudgetMinutes, projectId, userId, ticketNumber, organization.id, organization.name, title]
);
return result.rows[0];
@@ -3586,6 +3616,7 @@ app.get("/api/tickets/:ticketId/details", requireUser, async (req, res) => {
app.patch("/api/tickets/:ticketId", requireUser, async (req, res) => {
const ticketId = parsePositiveInteger(req.params.ticketId, "ticketId");
const ticketNumber = parseTrackableTicketNumber(req.body.ticketNumber);
const title = optionalNullableString(req.body.title);
const organizationId = parseOrganizationId(req.body.organizationId);
const workType = parseWorkType(req.body.workType);
@@ -3611,13 +3642,14 @@ app.patch("/api/tickets/:ticketId", requireUser, async (req, res) => {
`
UPDATE tickets
SET ticket_number = $1,
organization_id = $2,
customer_name = $3,
work_type = $4
WHERE id = $5
RETURNING id, ticket_number, organization_id, customer_name, work_type;
title = $2,
organization_id = $3,
customer_name = $4,
work_type = $5
WHERE id = $6
RETURNING id, ticket_number, title, organization_id, customer_name, work_type;
`,
[ticketNumber, organization.id, organization.name, effectiveWorkType, ticketId]
[ticketNumber, title, organization.id, organization.name, effectiveWorkType, ticketId]
);
if (ticketResult.rowCount === 0) {