Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
115 lines
4.5 KiB
Go
115 lines
4.5 KiB
Go
// Dashboard aggregation store. MVP: a single struct of live COUNT/GROUP BY
|
|
// queries per tenant, no caching layer and no materialized views. All queries
|
|
// are tenant-scoped (WHERE tenant_id = $1) exactly like the rest of the store.
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// DashboardStats is the aggregated key-figure snapshot for one tenant, served
|
|
// by GET /api/dashboard.
|
|
type DashboardStats struct {
|
|
TotalDocuments int64 `json:"total_documents"`
|
|
DocumentsThisMonth int64 `json:"documents_this_month"`
|
|
TrashCount int64 `json:"trash_count"`
|
|
PendingDeleteRequests int64 `json:"pending_delete_requests"`
|
|
RemindersDue int64 `json:"reminders_due"`
|
|
RemindersUpcoming7d int64 `json:"reminders_upcoming_7d"`
|
|
RetentionExpiring30d int64 `json:"retention_expiring_30d"`
|
|
DocumentsByType []DocumentTypeCount `json:"documents_by_type"`
|
|
}
|
|
|
|
// DocumentTypeCount is one entry of the documents_by_type breakdown.
|
|
type DocumentTypeCount struct {
|
|
DocumentTypeName string `json:"document_type_name"`
|
|
Count int64 `json:"count"`
|
|
}
|
|
|
|
// GetDashboardStats computes the aggregated dashboard key figures for a tenant.
|
|
// Reminders are additionally scoped to the requesting user (reminders are
|
|
// per-user like in ListReminders); the document/trash/retention figures are
|
|
// tenant-wide.
|
|
func (s *Store) GetDashboardStats(ctx context.Context, tenantID, userID int64) (*DashboardStats, error) {
|
|
var stats DashboardStats
|
|
|
|
// Documents: active count + this-calendar-month count in one scan.
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL),
|
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND created_at >= date_trunc('month', now())),
|
|
COUNT(*) FILTER (WHERE deleted_at IS NOT NULL)
|
|
FROM documents WHERE tenant_id = $1
|
|
`, tenantID).Scan(&stats.TotalDocuments, &stats.DocumentsThisMonth, &stats.TrashCount)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard document counts: %w", err)
|
|
}
|
|
|
|
// Open delete requests (awaiting confirmation or blocked by retention).
|
|
err = s.db.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM document_delete_requests
|
|
WHERE tenant_id = $1 AND status IN ('pending', 'blocked_retention')
|
|
`, tenantID).Scan(&stats.PendingDeleteRequests)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard pending delete requests: %w", err)
|
|
}
|
|
|
|
// Reminders (per user): due/overdue and upcoming within 7 days. "Not done"
|
|
// means still status='open' (see reminders.go status semantics).
|
|
err = s.db.QueryRow(ctx, `
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE due_date <= now()),
|
|
COUNT(*) FILTER (WHERE due_date > now() AND due_date <= now() + interval '7 days')
|
|
FROM reminders WHERE tenant_id = $1 AND user_id = $2 AND status = 'open'
|
|
`, tenantID, userID).Scan(&stats.RemindersDue, &stats.RemindersUpcoming7d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard reminders: %w", err)
|
|
}
|
|
|
|
// Retention expiring within the next 30 days (active documents only).
|
|
err = s.db.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM documents
|
|
WHERE tenant_id = $1 AND deleted_at IS NULL
|
|
AND retain_until IS NOT NULL
|
|
AND retain_until >= current_date
|
|
AND retain_until <= current_date + 30
|
|
`, tenantID).Scan(&stats.RetentionExpiring30d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard retention expiring: %w", err)
|
|
}
|
|
|
|
// Documents by type — top 5 by count. Uses the structured document_types
|
|
// entity via doc_type_id, falling back to the deprecated free-text doc_type
|
|
// for Bestandsschutz, and "(ohne Typ)" when neither is set.
|
|
rows, err := s.db.Query(ctx, `
|
|
SELECT COALESCE(dt.name, NULLIF(d.doc_type, ''), '(ohne Typ)') AS type_name, COUNT(*) AS cnt
|
|
FROM documents d
|
|
LEFT JOIN document_types dt ON dt.id = d.doc_type_id
|
|
WHERE d.tenant_id = $1 AND d.deleted_at IS NULL
|
|
GROUP BY type_name
|
|
ORDER BY cnt DESC, type_name ASC
|
|
LIMIT 5
|
|
`, tenantID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard documents by type: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var c DocumentTypeCount
|
|
if err := rows.Scan(&c.DocumentTypeName, &c.Count); err != nil {
|
|
return nil, fmt.Errorf("storage: scan documents by type: %w", err)
|
|
}
|
|
stats.DocumentsByType = append(stats.DocumentsByType, c)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("storage: dashboard documents by type rows: %w", err)
|
|
}
|
|
if stats.DocumentsByType == nil {
|
|
stats.DocumentsByType = []DocumentTypeCount{}
|
|
}
|
|
|
|
return &stats, nil
|
|
}
|