feat(07.1-03): user-tz day boundaries on /api/(mobile/)dashboard(/overview)
- Switch opened_today / resolved_today / yesterday / 7d-avg buckets from CURRENT_DATE to ((value AT TIME ZONE 'UTC') AT TIME ZONE $1)::date. - Both routes destructure session from requireAuth() and resolve tz via getUserTimezone(); tz parameterized as $1 (no SQL interpolation). - Preserved unchanged: due_date_time < NOW() (rolling SLA, tz-independent), the INTERVAL '24h/5min/1h' rolling-window queries (failed backups, stalled workflows, analyzer/RMM 1h fail counts, backup-success 24h). - Added a code comment above the 24h failed-backups query explaining why it stays UTC-NOW relative.
This commit is contained in:
parent
ea5532c5c3
commit
8a9887faa1
2 changed files with 50 additions and 27 deletions
|
|
@ -13,10 +13,12 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { requireAuth } from '@/lib/auth-utils';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import { getUserTimezone } from '@/lib/services/user-timezone';
|
||||
|
||||
export async function GET() {
|
||||
const { error } = await requireAuth();
|
||||
const { session, error } = await requireAuth();
|
||||
if (error) return error;
|
||||
const tz = getUserTimezone(session);
|
||||
|
||||
type Counts = { count: string };
|
||||
|
||||
|
|
@ -35,47 +37,59 @@ export async function GET() {
|
|||
ciRes,
|
||||
xrefRes,
|
||||
] = await Promise.all([
|
||||
/* today snapshot — single row, all four KPIs */
|
||||
/* today snapshot — single row, all four KPIs.
|
||||
Day-boundary counts are anchored to the calling user's tz via $1.
|
||||
The `due_date_time < NOW()` clause stays as-is (rolling-now SLA check
|
||||
is timezone-independent). */
|
||||
postgresClient.query<{
|
||||
opened_today: string;
|
||||
resolved_today: string;
|
||||
open_total: string;
|
||||
sla_breaches: string;
|
||||
}>(`
|
||||
}>(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE create_date::date = CURRENT_DATE)::text AS opened_today,
|
||||
COUNT(*) FILTER (WHERE completed_date::date = CURRENT_DATE)::text AS resolved_today,
|
||||
COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total,
|
||||
COUNT(*) FILTER (WHERE ((create_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date = (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date)::text AS opened_today,
|
||||
COUNT(*) FILTER (WHERE ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date = (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date)::text AS resolved_today,
|
||||
COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total,
|
||||
COUNT(*) FILTER (
|
||||
WHERE completed_date IS NULL
|
||||
AND due_date_time IS NOT NULL
|
||||
AND due_date_time < NOW()
|
||||
)::text AS sla_breaches
|
||||
)::text AS sla_breaches
|
||||
FROM tickets
|
||||
WHERE (is_deleted = false OR is_deleted IS NULL)
|
||||
AND company_id NOT IN (SELECT company_id FROM company_scope WHERE in_scope = false)
|
||||
`),
|
||||
/* yesterday's opened count for the today-vs-yesterday delta */
|
||||
postgresClient.query<{ count: string }>(`
|
||||
`,
|
||||
[tz],
|
||||
),
|
||||
/* yesterday's opened count for the today-vs-yesterday delta (user-tz) */
|
||||
postgresClient.query<{ count: string }>(
|
||||
`
|
||||
SELECT COUNT(*)::text AS count
|
||||
FROM tickets
|
||||
WHERE create_date::date = CURRENT_DATE - INTERVAL '1 day'
|
||||
WHERE ((create_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date = (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date - INTERVAL '1 day'
|
||||
AND (is_deleted = false OR is_deleted IS NULL)
|
||||
AND company_id NOT IN (SELECT company_id FROM company_scope WHERE in_scope = false)
|
||||
`),
|
||||
/* 7-day average resolved (excluding today) for the resolved delta */
|
||||
postgresClient.query<{ avg_resolved: string }>(`
|
||||
`,
|
||||
[tz],
|
||||
),
|
||||
/* 7-day average resolved (excluding today) for the resolved delta (user-tz) */
|
||||
postgresClient.query<{ avg_resolved: string }>(
|
||||
`
|
||||
SELECT COALESCE(AVG(daily_count), 0)::text AS avg_resolved
|
||||
FROM (
|
||||
SELECT completed_date::date AS d, COUNT(*) AS daily_count
|
||||
SELECT ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date AS d, COUNT(*) AS daily_count
|
||||
FROM tickets
|
||||
WHERE completed_date >= CURRENT_DATE - INTERVAL '7 days'
|
||||
AND completed_date < CURRENT_DATE
|
||||
WHERE ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date >= (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date - INTERVAL '7 days'
|
||||
AND ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date < (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date
|
||||
AND (is_deleted = false OR is_deleted IS NULL)
|
||||
AND company_id NOT IN (SELECT company_id FROM company_scope WHERE in_scope = false)
|
||||
GROUP BY completed_date::date
|
||||
GROUP BY ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date
|
||||
) sub
|
||||
`),
|
||||
`,
|
||||
[tz],
|
||||
),
|
||||
postgresClient.query<Counts>(
|
||||
`SELECT COUNT(*)::text AS count FROM device_link_review WHERE resolved_at IS NULL`
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { requireAuth } from '@/lib/auth-utils';
|
||||
import postgresClient from '@/lib/services/postgres-client';
|
||||
import { getUserTimezone } from '@/lib/services/user-timezone';
|
||||
|
||||
// ─── Response shape ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -46,8 +47,9 @@ export interface MobileDashboardResponse {
|
|||
// ─── Handler ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function GET() {
|
||||
const { error } = await requireAuth();
|
||||
const { session, error } = await requireAuth();
|
||||
if (error) return error;
|
||||
const tz = getUserTimezone(session);
|
||||
|
||||
try {
|
||||
const [
|
||||
|
|
@ -58,27 +60,34 @@ export async function GET() {
|
|||
rmmRes,
|
||||
backupSuccessRes,
|
||||
] = await Promise.all([
|
||||
/* 1. KPI snapshot — four counts in one row, scoped companies excluded */
|
||||
/* 1. KPI snapshot — four counts in one row, scoped companies excluded.
|
||||
Day-boundary counts (opened_today, resolved_today) are anchored to
|
||||
the calling user's IANA timezone via $1; storage tz unchanged. */
|
||||
postgresClient.query<{
|
||||
open_total: string;
|
||||
opened_today: string;
|
||||
resolved_today: string;
|
||||
sla_breaches: string;
|
||||
}>(`
|
||||
}>(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total,
|
||||
COUNT(*) FILTER (WHERE create_date::date = CURRENT_DATE)::text AS opened_today,
|
||||
COUNT(*) FILTER (WHERE completed_date::date = CURRENT_DATE)::text AS resolved_today,
|
||||
COUNT(*) FILTER (WHERE completed_date IS NULL)::text AS open_total,
|
||||
COUNT(*) FILTER (WHERE ((create_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date = (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date)::text AS opened_today,
|
||||
COUNT(*) FILTER (WHERE ((completed_date AT TIME ZONE 'UTC') AT TIME ZONE $1)::date = (NOW() AT TIME ZONE 'UTC' AT TIME ZONE $1)::date)::text AS resolved_today,
|
||||
COUNT(*) FILTER (
|
||||
WHERE completed_date IS NULL
|
||||
AND due_date_time IS NOT NULL
|
||||
AND due_date_time < NOW()
|
||||
)::text AS sla_breaches
|
||||
)::text AS sla_breaches
|
||||
FROM tickets
|
||||
WHERE (is_deleted = false OR is_deleted IS NULL)
|
||||
AND company_id NOT IN (SELECT company_id FROM company_scope WHERE in_scope = false)
|
||||
`),
|
||||
`,
|
||||
[tz],
|
||||
),
|
||||
|
||||
// INTERVAL '24 hours' here is rolling — not a calendar-day boundary —
|
||||
// so timezone does not apply. Do not migrate to user-tz.
|
||||
/* 2. Failed backups in the last 24 hours (Needs Attention) */
|
||||
postgresClient.query<{ count: string }>(`
|
||||
SELECT COUNT(*)::text AS count FROM (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue