Refine project and todo workflow
This commit is contained in:
1 parent
3da436423f
commit
239be9f218
9 files changed
+487
-301
No files matched your search
+51
-19
@@ -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) {
|
||||
|
||||
@@ -113,6 +113,7 @@ export async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
ticket_number TEXT NOT NULL UNIQUE,
|
||||
title TEXT,
|
||||
organization_id BIGINT REFERENCES organizations(zammad_id),
|
||||
customer_name TEXT,
|
||||
work_type TEXT,
|
||||
@@ -123,6 +124,7 @@ export async function migrate() {
|
||||
`);
|
||||
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS customer_name TEXT;");
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS title TEXT;");
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS organization_id BIGINT REFERENCES organizations(zammad_id);");
|
||||
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS work_type TEXT;");
|
||||
await query(`
|
||||
|
||||
Reference in new issue
Block a user