This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

780 lines
21 KiB
Go

package handlers
import (
"bytes"
"fmt"
"net/http"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/dsgvo"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// DSGVOHandlers handles DSGVO-related API endpoints
type DSGVOHandlers struct {
store *dsgvo.Store
}
// NewDSGVOHandlers creates new DSGVO handlers
func NewDSGVOHandlers(store *dsgvo.Store) *DSGVOHandlers {
return &DSGVOHandlers{store: store}
}
// ============================================================================
// VVT - Verarbeitungsverzeichnis (Processing Activities)
// ============================================================================
// ListProcessingActivities returns all processing activities for a tenant
func (h *DSGVOHandlers) ListProcessingActivities(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var namespaceID *uuid.UUID
if nsID := c.Query("namespace_id"); nsID != "" {
if id, err := uuid.Parse(nsID); err == nil {
namespaceID = &id
}
}
activities, err := h.store.ListProcessingActivities(c.Request.Context(), tenantID, namespaceID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"processing_activities": activities})
}
// GetProcessingActivity returns a processing activity by ID
func (h *DSGVOHandlers) GetProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
activity, err := h.store.GetProcessingActivity(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if activity == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, activity)
}
// CreateProcessingActivity creates a new processing activity
func (h *DSGVOHandlers) CreateProcessingActivity(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var activity dsgvo.ProcessingActivity
if err := c.ShouldBindJSON(&activity); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
activity.TenantID = tenantID
activity.CreatedBy = userID
if activity.Status == "" {
activity.Status = "draft"
}
if err := h.store.CreateProcessingActivity(c.Request.Context(), &activity); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, activity)
}
// UpdateProcessingActivity updates a processing activity
func (h *DSGVOHandlers) UpdateProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var activity dsgvo.ProcessingActivity
if err := c.ShouldBindJSON(&activity); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
activity.ID = id
if err := h.store.UpdateProcessingActivity(c.Request.Context(), &activity); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, activity)
}
// DeleteProcessingActivity deletes a processing activity
func (h *DSGVOHandlers) DeleteProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteProcessingActivity(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// TOM - Technische und Organisatorische Maßnahmen
// ============================================================================
// ListTOMs returns all TOMs for a tenant
func (h *DSGVOHandlers) ListTOMs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
category := c.Query("category")
toms, err := h.store.ListTOMs(c.Request.Context(), tenantID, category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"toms": toms, "categories": dsgvo.TOMCategories})
}
// GetTOM returns a TOM by ID
func (h *DSGVOHandlers) GetTOM(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
tom, err := h.store.GetTOM(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tom == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, tom)
}
// CreateTOM creates a new TOM
func (h *DSGVOHandlers) CreateTOM(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var tom dsgvo.TOM
if err := c.ShouldBindJSON(&tom); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tom.TenantID = tenantID
tom.CreatedBy = userID
if tom.ImplementationStatus == "" {
tom.ImplementationStatus = "planned"
}
if err := h.store.CreateTOM(c.Request.Context(), &tom); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, tom)
}
// ============================================================================
// DSR - Data Subject Requests
// ============================================================================
// ListDSRs returns all DSRs for a tenant
func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
status := c.Query("status")
requestType := c.Query("type")
dsrs, err := h.store.ListDSRs(c.Request.Context(), tenantID, status, requestType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"dsrs": dsrs, "types": dsgvo.DSRTypes})
}
// GetDSR returns a DSR by ID
func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsr, err := h.store.GetDSR(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsr == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, dsr)
}
// CreateDSR creates a new DSR
func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var dsr dsgvo.DSR
if err := c.ShouldBindJSON(&dsr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsr.TenantID = tenantID
dsr.CreatedBy = userID
if dsr.Status == "" {
dsr.Status = "received"
}
if err := h.store.CreateDSR(c.Request.Context(), &dsr); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, dsr)
}
// UpdateDSR updates a DSR
func (h *DSGVOHandlers) UpdateDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var dsr dsgvo.DSR
if err := c.ShouldBindJSON(&dsr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsr.ID = id
if err := h.store.UpdateDSR(c.Request.Context(), &dsr); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dsr)
}
// ============================================================================
// Retention Policies
// ============================================================================
// ListRetentionPolicies returns all retention policies for a tenant
func (h *DSGVOHandlers) ListRetentionPolicies(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
policies, err := h.store.ListRetentionPolicies(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"policies": policies,
"common_periods": dsgvo.CommonRetentionPeriods,
})
}
// CreateRetentionPolicy creates a new retention policy
func (h *DSGVOHandlers) CreateRetentionPolicy(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var policy dsgvo.RetentionPolicy
if err := c.ShouldBindJSON(&policy); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
policy.TenantID = tenantID
policy.CreatedBy = userID
if policy.Status == "" {
policy.Status = "draft"
}
if err := h.store.CreateRetentionPolicy(c.Request.Context(), &policy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, policy)
}
// ============================================================================
// Statistics
// ============================================================================
// GetStats returns DSGVO statistics for a tenant
func (h *DSGVOHandlers) GetStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
stats, err := h.store.GetStats(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ============================================================================
// DSFA - Datenschutz-Folgenabschätzung
// ============================================================================
// ListDSFAs returns all DSFAs for a tenant
func (h *DSGVOHandlers) ListDSFAs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
status := c.Query("status")
dsfas, err := h.store.ListDSFAs(c.Request.Context(), tenantID, status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"dsfas": dsfas})
}
// GetDSFA returns a DSFA by ID
func (h *DSGVOHandlers) GetDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsfa, err := h.store.GetDSFA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsfa == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, dsfa)
}
// CreateDSFA creates a new DSFA
func (h *DSGVOHandlers) CreateDSFA(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var dsfa dsgvo.DSFA
if err := c.ShouldBindJSON(&dsfa); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsfa.TenantID = tenantID
dsfa.CreatedBy = userID
if dsfa.Status == "" {
dsfa.Status = "draft"
}
if err := h.store.CreateDSFA(c.Request.Context(), &dsfa); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, dsfa)
}
// UpdateDSFA updates a DSFA
func (h *DSGVOHandlers) UpdateDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var dsfa dsgvo.DSFA
if err := c.ShouldBindJSON(&dsfa); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsfa.ID = id
if err := h.store.UpdateDSFA(c.Request.Context(), &dsfa); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dsfa)
}
// DeleteDSFA deletes a DSFA
func (h *DSGVOHandlers) DeleteDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteDSFA(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// PDF Export
// ============================================================================
// ExportVVT exports the Verarbeitungsverzeichnis as CSV/JSON
func (h *DSGVOHandlers) ExportVVT(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
activities, err := h.store.ListProcessingActivities(c.Request.Context(), tenantID, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=vvt_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"processing_activities": activities,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Name;Zweck;Rechtsgrundlage;Datenkategorien;Betroffene;Empfänger;Drittland;Aufbewahrung;Verantwortlich;Status;Erstellt\n")
for _, pa := range activities {
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%t;%s;%s;%s;%s\n",
pa.ID.String(),
escapeCSV(pa.Name),
escapeCSV(pa.Purpose),
pa.LegalBasis,
joinStrings(pa.DataCategories),
joinStrings(pa.DataSubjectCategories),
joinStrings(pa.Recipients),
pa.ThirdCountryTransfer,
pa.RetentionPeriod,
escapeCSV(pa.ResponsiblePerson),
pa.Status,
pa.CreatedAt.Format("2006-01-02"),
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=vvt_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportTOM exports the TOM catalog as CSV/JSON
func (h *DSGVOHandlers) ExportTOM(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
toms, err := h.store.ListTOMs(c.Request.Context(), tenantID, "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=tom_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"toms": toms,
"categories": dsgvo.TOMCategories,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Kategorie;Name;Beschreibung;Typ;Status;Implementiert am;Verantwortlich;Wirksamkeit;Erstellt\n")
for _, tom := range toms {
implementedAt := ""
if tom.ImplementedAt != nil {
implementedAt = tom.ImplementedAt.Format("2006-01-02")
}
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
tom.ID.String(),
tom.Category,
escapeCSV(tom.Name),
escapeCSV(tom.Description),
tom.Type,
tom.ImplementationStatus,
implementedAt,
escapeCSV(tom.ResponsiblePerson),
tom.EffectivenessRating,
tom.CreatedAt.Format("2006-01-02"),
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=tom_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportDSR exports DSR overview as CSV/JSON
func (h *DSGVOHandlers) ExportDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
dsrs, err := h.store.ListDSRs(c.Request.Context(), tenantID, "", "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=dsr_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"dsrs": dsrs,
"types": dsgvo.DSRTypes,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Typ;Name;E-Mail;Status;Eingegangen;Frist;Abgeschlossen;Kanal;Zugewiesen\n")
for _, dsr := range dsrs {
completedAt := ""
if dsr.CompletedAt != nil {
completedAt = dsr.CompletedAt.Format("2006-01-02")
}
assignedTo := ""
if dsr.AssignedTo != nil {
assignedTo = dsr.AssignedTo.String()
}
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
dsr.ID.String(),
dsr.RequestType,
escapeCSV(dsr.SubjectName),
dsr.SubjectEmail,
dsr.Status,
dsr.ReceivedAt.Format("2006-01-02"),
dsr.DeadlineAt.Format("2006-01-02"),
completedAt,
dsr.RequestChannel,
assignedTo,
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=dsr_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportDSFA exports a DSFA as JSON
func (h *DSGVOHandlers) ExportDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsfa, err := h.store.GetDSFA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsfa == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=dsfa_%s.json", id.String()[:8]))
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"dsfa": dsfa,
})
}
// ExportRetentionPolicies exports retention policies as CSV/JSON
func (h *DSGVOHandlers) ExportRetentionPolicies(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
policies, err := h.store.ListRetentionPolicies(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=retention_policies_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"policies": policies,
"common_periods": dsgvo.CommonRetentionPeriods,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Name;Datenkategorie;Aufbewahrungsdauer (Tage);Dauer (Text);Rechtsgrundlage;Referenz;Löschmethode;Status\n")
for _, rp := range policies {
buf.WriteString(fmt.Sprintf("%s;%s;%s;%d;%s;%s;%s;%s;%s\n",
rp.ID.String(),
escapeCSV(rp.Name),
rp.DataCategory,
rp.RetentionPeriodDays,
escapeCSV(rp.RetentionPeriodText),
escapeCSV(rp.LegalBasis),
escapeCSV(rp.LegalReference),
rp.DeletionMethod,
rp.Status,
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=retention_policies_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// Helper functions
func escapeCSV(s string) string {
// Simple CSV escaping - wrap in quotes if contains semicolon, quote, or newline
if s == "" {
return ""
}
needsQuotes := false
for _, c := range s {
if c == ';' || c == '"' || c == '\n' || c == '\r' {
needsQuotes = true
break
}
}
if needsQuotes {
// Double any quotes and wrap in quotes
escaped := ""
for _, c := range s {
if c == '"' {
escaped += "\"\""
} else if c == '\n' || c == '\r' {
escaped += " "
} else {
escaped += string(c)
}
}
return "\"" + escaped + "\""
}
return s
}
func joinStrings(strs []string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += ", " + strs[i]
}
return result
}