Models: Akte → Projekt (tree type + parent_id + path + client/matter numbers + netDocuments URL + type-specific client/patent/case columns). AkteEvent → ProjektEvent. FristWithAkte → FristWithProjekt. TerminWithAkte → TerminWithProjekt. Notiz.AkteID → ProjektID. ChecklistInstance.AkteID → ProjektID. Partei.AkteID → ProjektID. User adds AdditionalOffices pq.StringArray. Services: - NEW projekt_service.go replaces akte_service.go. Adds tree ops: List/GetByID/ ListChildren/ListAncestors/GetTree. Create auto-adds creator to projekt_teams role=lead in same tx. ResolveClientNumber walks path for inheritance. Visibility helpers (visibilityPredicate / Positional / Placeholder) centralise team-based access check: admin OR any ancestor/direct projekt_teams row. - NEW team_service.go — AddMember/RemoveMember/ListDirectMembers/ ListEffectiveMembers (unions direct + inherited via path, dedup by user; direct wins)/IsEffectiveMember. Inherited=true set at read time only. - NEW dezernat_service.go — admin-gated CRUD + member add/remove + user membership lookup for settings page. - frist_service.go → projekt_id everywhere, uses visibilityPredicate. ListFilter. AkteID → ProjektID. - termin_service.go → projekt_id everywhere. CalDAV log reads projekt_events. - notiz_service.go → projekt_id polymorphic branch; eventProjektID() looks at projekt_events; akten_event_id column kept (FK now resolves to projekt_events). - parteien_service.go → projekt_id. - checklist_instance_service.go → projekt_id with ClearProjekt flag. - dashboard_service.go → rewrites all four queries against projekte + projekt_events + projekt_teams. Matter/Upcoming/Activity surfaces use ProjektID/ProjektTitle/ProjektRef. - reminder_service.go → joins paliad.projekte, aliases a.reference AS akte_aktenzeichen for template compat. Handlers/tests still reference old API — Phase 2 completion requires handler rewrite (next commit). Build currently broken in internal/handlers.
122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"mgit.msbls.de/m/patholo/internal/models"
|
|
)
|
|
|
|
// ParteienService reads and writes paliad.parteien. Visibility inherits from
|
|
// the parent Projekt.
|
|
type ParteienService struct {
|
|
db *sqlx.DB
|
|
projekte *ProjektService
|
|
}
|
|
|
|
// NewParteienService wires the service.
|
|
func NewParteienService(db *sqlx.DB, projekte *ProjektService) *ParteienService {
|
|
return &ParteienService{db: db, projekte: projekte}
|
|
}
|
|
|
|
const parteiColumns = `id, projekt_id, name, role, representative, contact_info,
|
|
created_at, updated_at`
|
|
|
|
// CreateParteiInput is the payload for Create.
|
|
type CreateParteiInput struct {
|
|
Name string `json:"name"`
|
|
Role *string `json:"role,omitempty"`
|
|
Representative *string `json:"representative,omitempty"`
|
|
ContactInfo json.RawMessage `json:"contact_info,omitempty"`
|
|
}
|
|
|
|
// ListForProjekt returns all Parteien for the Projekt, visibility-checked.
|
|
func (s *ParteienService) ListForProjekt(ctx context.Context, userID, projektID uuid.UUID) ([]models.Partei, error) {
|
|
if _, err := s.projekte.GetByID(ctx, userID, projektID); err != nil {
|
|
return nil, err
|
|
}
|
|
var rows []models.Partei
|
|
if err := s.db.SelectContext(ctx, &rows,
|
|
`SELECT `+parteiColumns+`
|
|
FROM paliad.parteien
|
|
WHERE projekt_id = $1
|
|
ORDER BY name`, projektID); err != nil {
|
|
return nil, fmt.Errorf("list parteien: %w", err)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
// Create inserts a Partei under a Projekt; visibility is checked on the parent.
|
|
func (s *ParteienService) Create(ctx context.Context, userID, projektID uuid.UUID, input CreateParteiInput) (*models.Partei, error) {
|
|
if strings.TrimSpace(input.Name) == "" {
|
|
return nil, fmt.Errorf("%w: name is required", ErrInvalidInput)
|
|
}
|
|
if _, err := s.projekte.GetByID(ctx, userID, projektID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contact := input.ContactInfo
|
|
if len(contact) == 0 {
|
|
contact = json.RawMessage(`{}`)
|
|
}
|
|
|
|
id := uuid.New()
|
|
now := time.Now().UTC()
|
|
if _, err := s.db.ExecContext(ctx,
|
|
`INSERT INTO paliad.parteien
|
|
(id, projekt_id, name, role, representative, contact_info,
|
|
created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $7)`,
|
|
id, projektID, input.Name, input.Role, input.Representative, contact, now,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("insert partei: %w", err)
|
|
}
|
|
|
|
var p models.Partei
|
|
if err := s.db.GetContext(ctx, &p,
|
|
`SELECT `+parteiColumns+` FROM paliad.parteien WHERE id = $1`, id); err != nil {
|
|
return nil, fmt.Errorf("fetch created partei: %w", err)
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
// Delete removes a Partei. Partner/admin only.
|
|
func (s *ParteienService) Delete(ctx context.Context, userID, parteiID uuid.UUID) error {
|
|
user, err := s.projekte.Users().GetByID(ctx, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if user == nil {
|
|
return ErrNotVisible
|
|
}
|
|
if user.Role != "partner" && user.Role != "admin" {
|
|
return fmt.Errorf("%w: only partners/admins can delete Parteien", ErrForbidden)
|
|
}
|
|
|
|
var projektID uuid.UUID
|
|
err = s.db.GetContext(ctx, &projektID,
|
|
`SELECT projekt_id FROM paliad.parteien WHERE id = $1`, parteiID)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ErrNotVisible
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("lookup partei parent: %w", err)
|
|
}
|
|
if _, err := s.projekte.GetByID(ctx, userID, projektID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := s.db.ExecContext(ctx,
|
|
`DELETE FROM paliad.parteien WHERE id = $1`, parteiID); err != nil {
|
|
return fmt.Errorf("delete partei: %w", err)
|
|
}
|
|
return nil
|
|
}
|