- Database: kanzlai.audit_log table with RLS, append-only policies (no UPDATE/DELETE), indexes for entity, user, and time queries - Backend: AuditService.Log() with context-based tenant/user/IP/UA extraction, wired into all 7 services (case, deadline, appointment, document, note, party, tenant) - API: GET /api/audit-log with entity_type, entity_id, user_id, from/to date, and pagination filters - Frontend: Protokoll tab on case detail page with chronological audit entries, diff preview, and pagination Required by § 50 BRAO and DSGVO Art. 5(2).
140 lines
3.9 KiB
Go
140 lines
3.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
|
|
)
|
|
|
|
type AppointmentService struct {
|
|
db *sqlx.DB
|
|
audit *AuditService
|
|
}
|
|
|
|
func NewAppointmentService(db *sqlx.DB, audit *AuditService) *AppointmentService {
|
|
return &AppointmentService{db: db, audit: audit}
|
|
}
|
|
|
|
type AppointmentFilter struct {
|
|
CaseID *uuid.UUID
|
|
Type *string
|
|
StartFrom *time.Time
|
|
StartTo *time.Time
|
|
}
|
|
|
|
func (s *AppointmentService) List(ctx context.Context, tenantID uuid.UUID, filter AppointmentFilter) ([]models.Appointment, error) {
|
|
query := "SELECT * FROM appointments WHERE tenant_id = $1"
|
|
args := []any{tenantID}
|
|
argN := 2
|
|
|
|
if filter.CaseID != nil {
|
|
query += fmt.Sprintf(" AND case_id = $%d", argN)
|
|
args = append(args, *filter.CaseID)
|
|
argN++
|
|
}
|
|
if filter.Type != nil {
|
|
query += fmt.Sprintf(" AND appointment_type = $%d", argN)
|
|
args = append(args, *filter.Type)
|
|
argN++
|
|
}
|
|
if filter.StartFrom != nil {
|
|
query += fmt.Sprintf(" AND start_at >= $%d", argN)
|
|
args = append(args, *filter.StartFrom)
|
|
argN++
|
|
}
|
|
if filter.StartTo != nil {
|
|
query += fmt.Sprintf(" AND start_at <= $%d", argN)
|
|
args = append(args, *filter.StartTo)
|
|
argN++
|
|
}
|
|
|
|
query += " ORDER BY start_at ASC"
|
|
|
|
var appointments []models.Appointment
|
|
if err := s.db.SelectContext(ctx, &appointments, query, args...); err != nil {
|
|
return nil, fmt.Errorf("listing appointments: %w", err)
|
|
}
|
|
if appointments == nil {
|
|
appointments = []models.Appointment{}
|
|
}
|
|
return appointments, nil
|
|
}
|
|
|
|
func (s *AppointmentService) GetByID(ctx context.Context, tenantID, id uuid.UUID) (*models.Appointment, error) {
|
|
var a models.Appointment
|
|
err := s.db.GetContext(ctx, &a, "SELECT * FROM appointments WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting appointment: %w", err)
|
|
}
|
|
return &a, nil
|
|
}
|
|
|
|
func (s *AppointmentService) Create(ctx context.Context, a *models.Appointment) error {
|
|
a.ID = uuid.New()
|
|
now := time.Now().UTC()
|
|
a.CreatedAt = now
|
|
a.UpdatedAt = now
|
|
|
|
_, err := s.db.NamedExecContext(ctx, `
|
|
INSERT INTO appointments (id, tenant_id, case_id, title, description, start_at, end_at, location, appointment_type, caldav_uid, caldav_etag, created_at, updated_at)
|
|
VALUES (:id, :tenant_id, :case_id, :title, :description, :start_at, :end_at, :location, :appointment_type, :caldav_uid, :caldav_etag, :created_at, :updated_at)
|
|
`, a)
|
|
if err != nil {
|
|
return fmt.Errorf("creating appointment: %w", err)
|
|
}
|
|
s.audit.Log(ctx, "create", "appointment", &a.ID, nil, a)
|
|
return nil
|
|
}
|
|
|
|
func (s *AppointmentService) Update(ctx context.Context, a *models.Appointment) error {
|
|
a.UpdatedAt = time.Now().UTC()
|
|
|
|
result, err := s.db.NamedExecContext(ctx, `
|
|
UPDATE appointments SET
|
|
case_id = :case_id,
|
|
title = :title,
|
|
description = :description,
|
|
start_at = :start_at,
|
|
end_at = :end_at,
|
|
location = :location,
|
|
appointment_type = :appointment_type,
|
|
caldav_uid = :caldav_uid,
|
|
caldav_etag = :caldav_etag,
|
|
updated_at = :updated_at
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
`, a)
|
|
if err != nil {
|
|
return fmt.Errorf("updating appointment: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("checking rows affected: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return fmt.Errorf("appointment not found")
|
|
}
|
|
s.audit.Log(ctx, "update", "appointment", &a.ID, nil, a)
|
|
return nil
|
|
}
|
|
|
|
func (s *AppointmentService) Delete(ctx context.Context, tenantID, id uuid.UUID) error {
|
|
result, err := s.db.ExecContext(ctx, "DELETE FROM appointments WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
|
if err != nil {
|
|
return fmt.Errorf("deleting appointment: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("checking rows affected: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return fmt.Errorf("appointment not found")
|
|
}
|
|
s.audit.Log(ctx, "delete", "appointment", &id, nil, nil)
|
|
return nil
|
|
}
|