feat(projects): t-paliad-232 Verfahrenstyp picker + Schriftsätze CTA
Two-part fix from m's 2026-05-21 finding that the Schriftsätze tab
told users "Bitte zuerst einen Verfahrenstyp setzen" while the
project form had no field to set it. The `proceeding_type_id`
column was already on `paliad.projects` and accepted by the API.
Part 1 — Verfahrenstyp picker on the case-fields block
* frontend/src/components/ProjectFormFields.tsx — new optional
<select id="project-proceeding-type-id"> rendered between
Aktenzeichen and Mandantenrolle inside the type=case block.
First option is "(nicht gesetzt)" / "(unset)".
* frontend/src/client/project-form.ts — shared
loadProceedingTypes() + populateProceedingTypeSelect()
helpers. Options sorted by `code` (de.* → dpma.* → epa.* →
upc.*). readPayload sends `proceeding_type_id` only when the
user picked a value; prefillForm restores the saved id via
dataset.preselect to survive the async populate race.
* frontend/src/client/projects-new.ts — kicks off populate on
DOMContentLoaded.
* frontend/src/client/projects-detail.ts — edit-modal preload
now awaits populate; the local loadProceedingTypes duplicate
(used by the counterclaim modal) is replaced by the shared
helper so both surfaces hit the same cache.
Part 2 — Actionable empty-state on the Schriftsätze tab
* frontend/src/projects-detail.tsx — the static <p> empty-state
becomes a div with a "Projekt bearbeiten" button.
* frontend/src/client/projects-detail.ts — openEditModal now
accepts an optional focusFieldID; the new
#project-submissions-edit-cta click handler calls it with
"project-proceeding-type-id" so the picker is scrolled into
view and focused right after the modal opens.
i18n: new keys projects.field.proceeding_type{,.unset,.hint} and
projects.detail.submissions.empty.no_proceeding.cta; reworded
no_proceeding copy to match the new "edit the project" CTA.
Backend already validates via validateProceedingTypeCategory
(mig 087/088 fristenrechner-category guard). Added
TestProjectService_CaseProceedingTypePicker exercising both the
happy and reject paths through a `case`-typed Create.
Manual test path: open any case project → Edit → the Verfahrenstyp
picker shows below Aktenzeichen → save → the Schriftsätze tab now
lists the submission codes. Clicking the empty-state CTA jumps
straight to the picker.
This commit is contained in:
@@ -253,3 +253,95 @@ func TestProjectService_InstanceLevel_Roundtrip(t *testing.T) {
|
||||
t.Errorf("want ErrInvalidInput, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectService_CaseProceedingTypePicker covers the t-paliad-232
|
||||
// data path for the new project-form Verfahrenstyp picker:
|
||||
//
|
||||
// 1. Creating a `case`-typed project with a fristenrechner-category
|
||||
// proceeding_type_id round-trips the column.
|
||||
// 2. The same code path rejects a non-fristenrechner-category id with
|
||||
// ErrInvalidProceedingTypeCategory (mirror of the guard test above,
|
||||
// this time exercised through a 'case' shape).
|
||||
//
|
||||
// Skipped when TEST_DATABASE_URL is unset.
|
||||
func TestProjectService_CaseProceedingTypePicker(t *testing.T) {
|
||||
url := os.Getenv("TEST_DATABASE_URL")
|
||||
if url == "" {
|
||||
t.Skip("TEST_DATABASE_URL not set — skipping live DB test")
|
||||
}
|
||||
if err := db.ApplyMigrations(url); err != nil {
|
||||
t.Fatalf("apply migrations: %v", err)
|
||||
}
|
||||
pool, err := sqlx.Connect("postgres", url)
|
||||
if err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var fristenrechnerID int
|
||||
if err := pool.GetContext(ctx, &fristenrechnerID,
|
||||
`SELECT id FROM paliad.proceeding_types
|
||||
WHERE category = 'fristenrechner' AND code = $1 AND is_active = true`,
|
||||
CodeUPCInfringement); err != nil {
|
||||
t.Fatalf("look up %s id: %v", CodeUPCInfringement, err)
|
||||
}
|
||||
var nonFristenrechnerID int
|
||||
if err := pool.GetContext(ctx, &nonFristenrechnerID,
|
||||
`SELECT id FROM paliad.proceeding_types
|
||||
WHERE category <> 'fristenrechner'
|
||||
ORDER BY id
|
||||
LIMIT 1`); err != nil {
|
||||
t.Fatalf("look up non-fristenrechner id: %v", err)
|
||||
}
|
||||
|
||||
users := NewUserService(pool)
|
||||
svc := NewProjectService(pool, users)
|
||||
|
||||
userID := uuid.New()
|
||||
cleanup := func() {
|
||||
pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE created_by = $1`, userID)
|
||||
pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id = $1`, userID)
|
||||
pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id = $1`, userID)
|
||||
}
|
||||
cleanup()
|
||||
defer cleanup()
|
||||
|
||||
if _, err := pool.ExecContext(ctx,
|
||||
`INSERT INTO auth.users (id, email) VALUES ($1, 't-paliad-232-test@hlc.com')`,
|
||||
userID); err != nil {
|
||||
t.Fatalf("seed auth.users: %v", err)
|
||||
}
|
||||
if _, err := pool.ExecContext(ctx,
|
||||
`INSERT INTO paliad.users (id, email, display_name, office, role, lang)
|
||||
VALUES ($1, 't-paliad-232-test@hlc.com', 'Picker Test', 'munich', 'associate', 'de')`,
|
||||
userID); err != nil {
|
||||
t.Fatalf("seed paliad.users: %v", err)
|
||||
}
|
||||
|
||||
// 1. Case-typed create with a fristenrechner id succeeds.
|
||||
created, err := svc.Create(ctx, userID, CreateProjectInput{
|
||||
Type: ProjectTypeCase,
|
||||
Title: "t-paliad-232 — case with proceeding_type_id",
|
||||
ProceedingTypeID: &fristenrechnerID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create case with fristenrechner id: %v", err)
|
||||
}
|
||||
if created.ProceedingTypeID == nil || *created.ProceedingTypeID != fristenrechnerID {
|
||||
t.Errorf("created proceeding_type_id = %v, want %d", created.ProceedingTypeID, fristenrechnerID)
|
||||
}
|
||||
|
||||
// 2. Case-typed create with a non-fristenrechner id is rejected.
|
||||
_, err = svc.Create(ctx, userID, CreateProjectInput{
|
||||
Type: ProjectTypeCase,
|
||||
Title: "t-paliad-232 — case with non-fristenrechner id",
|
||||
ProceedingTypeID: &nonFristenrechnerID,
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("Create case with non-fristenrechner proceeding_type_id should fail, but succeeded")
|
||||
} else if !errors.Is(err, ErrInvalidProceedingTypeCategory) {
|
||||
t.Errorf("expected ErrInvalidProceedingTypeCategory, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user