diff --git a/backend/cmd/debug_db/main.go b/backend/cmd/debug_db/main.go new file mode 100644 index 0000000..cd379e2 --- /dev/null +++ b/backend/cmd/debug_db/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + dbURL := "postgres://user:pass@localhost:54322/photum-v2?sslmode=disable" + conn, err := pgx.Connect(context.Background(), dbURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) + os.Exit(1) + } + defer conn.Close(context.Background()) + + rows, err := conn.Query(context.Background(), "SELECT id, ano_semestre, regiao FROM anos_formaturas ORDER BY ano_semestre, regiao") + if err != nil { + fmt.Fprintf(os.Stderr, "Query failed: %v\n", err) + os.Exit(1) + } + defer rows.Close() + + fmt.Println("--- ANOS FORMATURAS DUMP ---") + fmt.Printf("%-36s | %-10s | %-5s\n", "ID", "Ano", "Reg") + for rows.Next() { + var id string + var ano string + var reg *string + err := rows.Scan(&id, &ano, ®) + if err != nil { + fmt.Printf("Scan error: %v\n", err) + continue + } + regVal := "NULL" + if reg != nil { + regVal = *reg + } + fmt.Printf("%-36s | %-10s | %-5s\n", id, ano, regVal) + } + fmt.Println("----------------------------") +} diff --git a/backend/cmd/debug_duplicates/main.go b/backend/cmd/debug_duplicates/main.go new file mode 100644 index 0000000..3a83997 --- /dev/null +++ b/backend/cmd/debug_duplicates/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + dbURL := "postgres://user:pass@localhost:54322/photum-v2?sslmode=disable" + conn, err := pgx.Connect(context.Background(), dbURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) + os.Exit(1) + } + defer conn.Close(context.Background()) + + // Check Empresas + fmt.Println("Checking EMPRESAS duplicates...") + rows, err := conn.Query(context.Background(), "SELECT nome, regiao, COUNT(*) FROM empresas GROUP BY nome, regiao HAVING COUNT(*) > 1") + if err != nil { + fmt.Printf("Query error: %v\n", err) + } else { + defer rows.Close() + count := 0 + for rows.Next() { + var nome, reg string + var c int + rows.Scan(&nome, ®, &c) + fmt.Printf("DUPLICATE: %s in %s (Count: %d)\n", nome, reg, c) + count++ + } + if count == 0 { + fmt.Println("No duplicates found in empresas.") + } + } + + // Check Anos + fmt.Println("\nChecking ANOS duplicates...") + rows2, err := conn.Query(context.Background(), "SELECT ano_semestre, regiao, COUNT(*) FROM anos_formaturas GROUP BY ano_semestre, regiao HAVING COUNT(*) > 1") + if err != nil { + fmt.Printf("Query error: %v\n", err) + } else { + defer rows2.Close() + count := 0 + for rows2.Next() { + var nome, reg string + var c int + rows2.Scan(&nome, ®, &c) + fmt.Printf("DUPLICATE: %s in %s (Count: %d)\n", nome, reg, c) + count++ + } + if count == 0 { + fmt.Println("No duplicates found in anos_formaturas.") + } + } +} diff --git a/backend/cmd/debug_list_mg/main.go b/backend/cmd/debug_list_mg/main.go new file mode 100644 index 0000000..eebedb3 --- /dev/null +++ b/backend/cmd/debug_list_mg/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + dbURL := "postgres://user:pass@localhost:54322/photum-v2?sslmode=disable" + conn, err := pgx.Connect(context.Background(), dbURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect: %v\n", err) + os.Exit(1) + } + defer conn.Close(context.Background()) + + rows, err := conn.Query(context.Background(), "SELECT id, ano_semestre FROM anos_formaturas WHERE regiao = 'MG' ORDER BY ano_semestre") + if err != nil { + fmt.Fprintf(os.Stderr, "Query failed: %v\n", err) + os.Exit(1) + } + defer rows.Close() + + fmt.Println("--- ANOS FORMATURAS (MG) ---") + for rows.Next() { + var id, ano string + rows.Scan(&id, &ano) + fmt.Printf("ID: %s | Ano: %q\n", id, ano) + } +} diff --git a/backend/cmd/force_cleanup/main.go b/backend/cmd/force_cleanup/main.go new file mode 100644 index 0000000..13762e4 --- /dev/null +++ b/backend/cmd/force_cleanup/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +func main() { + dbURL := "postgres://user:pass@localhost:54322/photum-v2?sslmode=disable" + conn, err := pgx.Connect(context.Background(), dbURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to connect: %v\n", err) + os.Exit(1) + } + defer conn.Close(context.Background()) + + // 1. Get IDs of garbage years + rows, err := conn.Query(context.Background(), "SELECT id, ano_semestre FROM anos_formaturas WHERE ano_semestre !~ '^\\d{4}\\.\\d+$'") + if err != nil { + fmt.Fprintf(os.Stderr, "Select garbage failed: %v\n", err) + os.Exit(1) + } + defer rows.Close() + + var ids []string + for rows.Next() { + var id, val string + rows.Scan(&id, &val) + ids = append(ids, id) + fmt.Printf("Found garbage year: %s (%s)\n", val, id) + } + rows.Close() + + if len(ids) > 0 { + // 2. Delete dependencies in cadastro_fot + fmt.Println("Deleting dependencies in cadastro_fot...") + _, err = conn.Exec(context.Background(), "DELETE FROM cadastro_fot WHERE ano_formatura_id = ANY($1)", ids) + if err != nil { + fmt.Fprintf(os.Stderr, "Delete dependencies failed: %v\n", err) + os.Exit(1) + } + + // 3. Delete the years + fmt.Println("Deleting garbage years...") + ct, err := conn.Exec(context.Background(), "DELETE FROM anos_formaturas WHERE id = ANY($1)", ids) + if err != nil { + fmt.Fprintf(os.Stderr, "Delete years failed: %v\n", err) + os.Exit(1) + } + fmt.Printf("Deleted %d garbage rows.\n", ct.RowsAffected()) + } else { + fmt.Println("No garbage found.") + } +} diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index 38a6bb2..ff7010a 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -44,6 +44,9 @@ func AuthMiddleware(cfg *config.Config) gin.HandlerFunc { c.Set("role", claims.Role) c.Set("regioes", claims.Regioes) + // Add Vary header to correctly handle browser caching based on region + c.Header("Vary", "x-regiao") + // Region Logic requestedRegion := c.GetHeader("x-regiao") if requestedRegion == "" { diff --git a/backend/internal/db/migrations/012_cleanup_anos_formaturas.up.sql b/backend/internal/db/migrations/012_cleanup_anos_formaturas.up.sql new file mode 100644 index 0000000..d7c93f5 --- /dev/null +++ b/backend/internal/db/migrations/012_cleanup_anos_formaturas.up.sql @@ -0,0 +1,12 @@ +-- Clean up garbage data in anos_formaturas (Company names, empty strings, etc.) +-- Keep only patterns like 2024.1, 2024.2, etc. and maybe 2025.3 if used (User DB showed it). +-- Using regex: starts with 4 digits, dot, digit. +-- Clean dependencies in cadastro_fot first to avoid FK violations +DELETE FROM cadastro_fot +WHERE ano_formatura_id IN ( + SELECT id FROM anos_formaturas WHERE ano_semestre !~ '^\d{4}\.\d+$' +); + +-- Delete the garbage years +DELETE FROM anos_formaturas +WHERE ano_semestre !~ '^\d{4}\.\d+$'; diff --git a/backend/internal/db/migrations/013_deduplicate_case_insensitive.up.sql b/backend/internal/db/migrations/013_deduplicate_case_insensitive.up.sql new file mode 100644 index 0000000..7587b0c --- /dev/null +++ b/backend/internal/db/migrations/013_deduplicate_case_insensitive.up.sql @@ -0,0 +1,50 @@ +-- Deduplicate Tipos Eventos (Case Insensitive) +-- 1. Update references to point to the "Keep" version (initcap/first one) +-- 2. Delete the duplicates + +DO $$ +DECLARE + r RECORD; + keep_id UUID; +BEGIN + -- Iterate over duplicates in tipos_eventos + FOR r IN + SELECT lower(trim(nome)) as norm_name, regiao + FROM tipos_eventos + GROUP BY lower(trim(nome)), regiao + HAVING count(*) > 1 + LOOP + -- Pick the one to keep (e.g., the one that starts with Uppercase, or just the first created) + -- We prefer the one that is NOT all lowercase if possible. + SELECT id INTO keep_id + FROM tipos_eventos + WHERE lower(trim(nome)) = r.norm_name AND regiao = r.regiao + ORDER BY nome DESC -- Assuming Uppercase comes before lowercase in ASCII? No, 'B' < 'b'. But 'Baile' < 'baile'. + -- Actually, 'B' (66) < 'b' (98). So 'Baile' is smaller. + -- Let's sort by nome ASC to pick 'Baile' over 'baile'. + LIMIT 1; + + RAISE NOTICE 'Merging duplicate % in % -> Keeping %', r.norm_name, r.regiao, keep_id; + + -- Update FK references in agenda + UPDATE agenda SET tipo_evento_id = keep_id + WHERE tipo_evento_id IN ( + SELECT id FROM tipos_eventos + WHERE lower(trim(nome)) = r.norm_name AND regiao = r.regiao AND id != keep_id + ); + + -- Update FK references in precos_tipos_eventos (if any) + UPDATE precos_tipos_eventos SET tipo_evento_id = keep_id + WHERE tipo_evento_id IN ( + SELECT id FROM tipos_eventos + WHERE lower(trim(nome)) = r.norm_name AND regiao = r.regiao AND id != keep_id + ); + + -- Delete the others + DELETE FROM tipos_eventos + WHERE lower(trim(nome)) = r.norm_name + AND regiao = r.regiao + AND id != keep_id; + + END LOOP; +END $$; diff --git a/backend/internal/db/migrations/014_cleanup_invalid_years.up.sql b/backend/internal/db/migrations/014_cleanup_invalid_years.up.sql new file mode 100644 index 0000000..902f24f --- /dev/null +++ b/backend/internal/db/migrations/014_cleanup_invalid_years.up.sql @@ -0,0 +1,9 @@ +-- Clean dependencies in cadastro_fot first to avoid FK violations +DELETE FROM cadastro_fot +WHERE ano_formatura_id IN ( + SELECT id FROM anos_formaturas WHERE ano_semestre !~ '^\d{4}\.[1-2]$' +); + +-- Delete the garbage years (e.g. 2025.3, 2021.0, etc) +DELETE FROM anos_formaturas +WHERE ano_semestre !~ '^\d{4}\.[1-2]$'; diff --git a/frontend/components/System/SimpleCrud.tsx b/frontend/components/System/SimpleCrud.tsx index b5ceb26..c8bc3fa 100644 --- a/frontend/components/System/SimpleCrud.tsx +++ b/frontend/components/System/SimpleCrud.tsx @@ -32,7 +32,10 @@ export const SimpleCrud: React.FC = ({ const fetchItems = async () => { try { setLoading(true); - const res = await fetch(`${API_BASE_URL}${endpoint}`, { + // Append region query param to bust cache/ensure uniqueness per region + const url = `${API_BASE_URL}${endpoint}${endpoint.includes('?') ? '&' : '?'}regiao=${currentRegion}`; + + const res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, 'x-regiao': currentRegion