diff --git a/backend/src/index.ts b/backend/src/index.ts index 733f4d9..5c68168 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -702,7 +702,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { td.tracked_minutes, COALESCE(tdb.billed_minutes, 0)::int AS crm_billed_minutes, tdb.billed_minutes IS NOT NULL AS has_crm_value, - ack.acknowledged_at IS NOT NULL AS missing_crm_acknowledged + ack.acknowledged_at IS NOT NULL AS missing_crm_acknowledged, + ack.acknowledged_at AS missing_crm_acknowledged_at FROM ticket_day td LEFT JOIN ticket_day_billings tdb ON tdb.ticket_id = td.ticket_id @@ -753,6 +754,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { ticketResult, openResult, missingCrmResult, + acknowledgedMissingCrmResult, mismatchResult, closureResult ] = await Promise.all([ @@ -917,7 +919,9 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { MIN(sb.organization_name) AS organization_name, tdb.day::text AS day, tdb.tracked_minutes, - tdb.crm_billed_minutes + tdb.crm_billed_minutes, + false AS acknowledged, + NULL::text AS acknowledged_at 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 @@ -927,6 +931,27 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { `, [period.startIso, period.endIso, userId] ), + query( + ` + ${baseCte} + SELECT + tdb.ticket_id::text AS ticket_id, + tdb.ticket_number, + MIN(sb.organization_name) AS organization_name, + tdb.day::text AS day, + tdb.tracked_minutes, + tdb.crm_billed_minutes, + true AS acknowledged, + MAX(tdb.missing_crm_acknowledged_at)::text AS acknowledged_at + 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 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; + `, + [period.startIso, period.endIso, userId] + ), query( ` ${baseCte} @@ -1009,6 +1034,7 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) { attention: { openSessions: openResult.rows, missingCrmDays: missingCrmResult.rows, + acknowledgedMissingCrmDays: acknowledgedMissingCrmResult.rows, crmMismatches: mismatchResult.rows } }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 3b85cfb..e1ad10b 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -258,6 +258,8 @@ export type StatisticsCrmDay = { tracked_minutes: number; crm_billed_minutes: number; delta_minutes?: number; + acknowledged?: boolean; + acknowledged_at?: string | null; }; export type StatisticsOverview = { @@ -291,6 +293,7 @@ export type StatisticsOverview = { attention: { openSessions: StatisticsOpenSession[]; missingCrmDays: StatisticsCrmDay[]; + acknowledgedMissingCrmDays: StatisticsCrmDay[]; crmMismatches: StatisticsCrmDay[]; }; }; diff --git a/frontend/src/views/MonthlyClosePage.tsx b/frontend/src/views/MonthlyClosePage.tsx index 7c0bcef..fd1e7fe 100644 --- a/frontend/src/views/MonthlyClosePage.tsx +++ b/frontend/src/views/MonthlyClosePage.tsx @@ -116,32 +116,31 @@ function OpenTicketItem({ group, month, onNavigate }: { group: OpenTicketGroup; function CrmDayItem({ item, - acknowledging, - onAcknowledge, + busy, + onSetAcknowledgement, onNavigate }: { item: StatisticsCrmDay; - acknowledging: boolean; - onAcknowledge: (item: StatisticsCrmDay) => void; + busy: boolean; + onSetAcknowledgement: (item: StatisticsCrmDay, acknowledged: boolean) => void; onNavigate: (to: string) => void; }) { + const isAcknowledged = Boolean(item.acknowledged); + return ( -
+
{ - if (checked) { - onAcknowledge(item); - } - }} + onCheckedChange={(checked) => onSetAcknowledgement(item, checked === true)} /> {item.ticket_number} {formatDate(`${item.day}T00:00:00`)} + {isAcknowledged ? "zur Kenntnis" : "offen"}

{item.organization_name}

@@ -151,8 +150,8 @@ function CrmDayItem({

-
@@ -365,20 +370,35 @@ export function MonthlyClosePage({ onNavigate }: MonthlyClosePageProps) {
-
- - Tage ohne Teamspace-Wert +
+
+ + Tage ohne Teamspace-Wert +
+
- {missingCrmDays.map((item) => ( + {visibleMissingCrmDays.map((item) => ( ))} - {missingCrmDays.length === 0 ?

Alle getrackten Tage haben einen Teamspace-Wert.

: null} + {visibleMissingCrmDays.length === 0 ? ( +

+ {acknowledgedMissingCrmDays.length > 0 ? "Alle offenen Punkte sind zur Kenntnis genommen." : "Alle getrackten Tage haben einen Teamspace-Wert."} +

+ ) : null}