Refine monthly close cleanup

This commit is contained in:
2026-08-05 15:21:55 +02:00
parent ca5e015065
commit 20bc04778d
5 changed files with 212 additions and 57 deletions
+69 -1
View File
@@ -701,12 +701,17 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
td.day,
td.tracked_minutes,
COALESCE(tdb.billed_minutes, 0)::int AS crm_billed_minutes,
tdb.billed_minutes IS NOT NULL AS has_crm_value
tdb.billed_minutes IS NOT NULL AS has_crm_value,
ack.acknowledged_at IS NOT NULL AS missing_crm_acknowledged
FROM ticket_day td
LEFT JOIN ticket_day_billings tdb
ON tdb.ticket_id = td.ticket_id
AND tdb.user_id = $3
AND tdb.day = td.day
LEFT JOIN ticket_day_billing_acknowledgements ack
ON ack.ticket_id = td.ticket_id
AND ack.user_id = $3
AND ack.day = td.day
),
session_group AS (
SELECT
@@ -916,6 +921,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
FROM ticket_day_billing tdb
JOIN session_base sb ON sb.ticket_id = tdb.ticket_id AND sb.day = tdb.day
WHERE NOT tdb.has_crm_value
AND NOT tdb.missing_crm_acknowledged
GROUP BY tdb.ticket_id, tdb.ticket_number, tdb.day, tdb.tracked_minutes, tdb.crm_billed_minutes
ORDER BY tdb.day ASC, tdb.ticket_number ASC;
`,
@@ -2560,9 +2566,71 @@ app.patch("/api/tickets/:ticketId/day-billings/:day", async (req, res) => {
[ticketId, userId, day.start, billedMinutes]
);
await query(
`
DELETE FROM ticket_day_billing_acknowledgements
WHERE ticket_id = $1
AND user_id = $2
AND day = $3::date;
`,
[ticketId, userId, day.start]
);
res.json({ dayBilling: result.rows[0] });
});
app.patch("/api/tickets/:ticketId/day-billings/:day/acknowledgement", async (req, res) => {
const ticketId = parsePositiveInteger(req.params.ticketId, "ticketId");
const day = parseDay(req.params.day);
const acknowledged = req.body.acknowledged !== false;
const userId = currentUser(req).id;
const sessionResult = await query<{ id: string }>(
`
SELECT id
FROM sessions
WHERE ticket_id = $1
AND user_id = $2
AND (started_at AT TIME ZONE 'Europe/Berlin')::date = $3::date
LIMIT 1;
`,
[ticketId, userId, day.start]
);
if (sessionResult.rowCount === 0) {
res.status(404).json({ error: "No sessions found for this ticket and day" });
return;
}
if (!acknowledged) {
await query(
`
DELETE FROM ticket_day_billing_acknowledgements
WHERE ticket_id = $1
AND user_id = $2
AND day = $3::date;
`,
[ticketId, userId, day.start]
);
res.json({ acknowledged: false });
return;
}
const result = await query(
`
INSERT INTO ticket_day_billing_acknowledgements (ticket_id, user_id, day)
VALUES ($1, $2, $3::date)
ON CONFLICT (ticket_id, user_id, day)
DO UPDATE SET acknowledged_at = now()
RETURNING day::text AS day, acknowledged_at;
`,
[ticketId, userId, day.start]
);
res.json({ acknowledged: true, acknowledgement: result.rows[0] });
});
app.post("/api/periods/:periodType/:period/tickets/:ticketId/close", async (req, res) => {
parsePeriod(req.params.periodType, req.params.period);
parsePositiveInteger(req.params.ticketId, "ticketId");
+11
View File
@@ -295,6 +295,16 @@ export async function migrate() {
);
`);
await query(`
CREATE TABLE IF NOT EXISTS ticket_day_billing_acknowledgements (
ticket_id BIGINT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
day DATE NOT NULL,
acknowledged_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (ticket_id, user_id, day)
);
`);
await ensureClosureOwnership("ticket_month_closures", "month", ["ticket_id", "month"], adminId);
await ensureClosureOwnership("month_closures", "month", ["month"], adminId);
await ensureClosureOwnership("ticket_day_closures", "day", ["ticket_id", "day"], adminId);
@@ -309,6 +319,7 @@ export async function migrate() {
await query("CREATE INDEX IF NOT EXISTS idx_sessions_billing_status ON sessions(billing_status);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_closures_day ON ticket_day_closures(day);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billings_user_day ON ticket_day_billings(user_id, day);");
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billing_ack_user_day ON ticket_day_billing_acknowledgements(user_id, day);");
await query("CREATE INDEX IF NOT EXISTS idx_recurring_billings_user_active ON recurring_billings(user_id, active);");
await query("UPDATE recurring_billings SET active = true WHERE active = false;");
await query(`