Skip to content

Training Report#1365

Open
bjerrecs wants to merge 1 commit intomainfrom
feat(report-templates)
Open

Training Report#1365
bjerrecs wants to merge 1 commit intomainfrom
feat(report-templates)

Conversation

@bjerrecs
Copy link
Contributor

@bjerrecs bjerrecs commented Jan 4, 2026

image image image

Implementation for issue #1203

Summary by Sourcery

Introduce reusable training report templates and integrate them into the training report workflow.

New Features:

  • Allow moderators to create, edit, list, and delete training report templates, including per-area assignment and draft/published status.
  • Enable instructors to select a training report template when creating a training report, pre-filling both report and improvement content fields.

Enhancements:

  • Add authorization policy and sidebar navigation updates to manage access and visibility of training report template administration pages.

Chores:

  • Add database schema and Eloquent models to store training report templates and their area assignments.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 4, 2026

Reviewer's Guide

Implements an admin-manageable Training Report Template system (with per-area scoping and draft/published states) and integrates it into the training report creation flow so instructors can prefill report fields from selected templates.

Sequence diagram for training report creation with template prefill

sequenceDiagram
    actor Instructor
    participant Browser
    participant TrainingReportController
    participant TrainingReportTemplate
    participant Training

    Instructor->>Browser: Open training report create URL
    Browser->>TrainingReportController: GET create(training_id)
    TrainingReportController->>Training: find(training_id)
    TrainingReportController->>TrainingReportTemplate: query published templates scoped to training.area_id
    TrainingReportTemplate-->>TrainingReportController: templates collection
    TrainingReportController-->>Browser: Render training.report.create with templates

    Instructor->>Browser: Choose template from templateSelect
    Browser->>Browser: JS reads selectedOption.dataset.content and contentimprove
    alt current content empty or user confirms replacement
        Browser->>Browser: simplemde1.value(template.content)
        Browser->>Browser: simplemde2.value(template.contentimprove)
    else user cancels
        Browser-->>Browser: Keep existing editor content
    end
Loading

ER diagram for training report templates and area mapping

erDiagram
    AREAS {
        int id PK
        string name
        string other_columns
    }

    TRAINING_REPORT_TEMPLATES {
        int id PK
        string name
        text content
        text contentimprove
        boolean draft
        timestamp created_at
        timestamp updated_at
    }

    TRAINING_REPORT_TEMPLATE_AREA {
        int id PK
        int training_report_template_id FK
        int area_id FK
        timestamp created_at
        timestamp updated_at
        string unique_training_report_template_id_area_id
    }

    TRAININGS {
        int id PK
        int area_id FK
        string other_columns
    }

    AREAS ||--o{ TRAININGS : has
    AREAS ||--o{ TRAINING_REPORT_TEMPLATE_AREA : maps
    TRAINING_REPORT_TEMPLATES ||--o{ TRAINING_REPORT_TEMPLATE_AREA : maps
Loading

Class diagram for TrainingReportTemplate management and area scoping

classDiagram
    class Area {
        +int id
        +string name
        +positions()
        +trainingReportTemplates()
    }

    class TrainingReportTemplate {
        +int id
        +string name
        +text content
        +text contentimprove
        +bool draft
        +timestamps
        +areas()
    }

    class TrainingReportController {
        +create(Request request, Training training)
    }

    class TrainingReportTemplateController {
        +index()
        +create()
        +store(Request request)
        +edit(TrainingReportTemplate template)
        +update(Request request, TrainingReportTemplate template)
        +destroy(TrainingReportTemplate template)
    }

    class TrainingReportTemplatePolicy {
        +viewAny(User user) bool
        +view(User user, TrainingReportTemplate template) bool
        +create(User user) bool
        +update(User user, TrainingReportTemplate template) bool
        +delete(User user, TrainingReportTemplate template) bool
    }

    class User {
        +bool isModeratorOrAbove()
    }

    Area "1" --> "*" Position
    Area "*" -- "*" TrainingReportTemplate : training_report_template_area

    TrainingReportController ..> TrainingReportTemplate : uses
    TrainingReportController ..> Training : uses

    TrainingReportTemplateController ..> TrainingReportTemplate : manages
    TrainingReportTemplateController ..> Area : reads

    TrainingReportTemplatePolicy ..> TrainingReportTemplate : authorizes
    TrainingReportTemplatePolicy ..> User : checks_role
Loading

File-Level Changes

Change Details Files
Add template selection to training report creation and wire it to Markdown editors.
  • Render a Template (Optional) select box on the training report form, populated with available templates or disabled with an admin link if none exist.
  • Store template id validation errors and display them below the select.
  • Attach a JavaScript change handler that, on template selection, optionally overwrites the two EasyMDE editor contents after user confirmation, using data attributes on the selected option.
resources/views/training/report/create.blade.php
Load area-scoped, non-draft templates when creating a training report.
  • Inject TrainingReportTemplate model into TrainingReportController.
  • Query non-draft templates that are either global (no areas) or assigned to the training's area, ordered by name.
  • Pass the resulting templates collection into the training report creation view.
app/Http/Controllers/TrainingReportController.php
Expose CRUD administration for training report templates with authorization and navigation hooks.
  • Register admin/reporttemplates routes for index, create, store, edit, update, and destroy actions, using TrainingReportTemplateController.
  • Add Report templates entry under Administration and update active state matching to include new routes.
  • Register TrainingReportTemplatePolicy in AuthServiceProvider and implement basic moderator-only permissions for view/create/update/delete.
routes/web.php
resources/views/layouts/sidebar.blade.php
app/Providers/AuthServiceProvider.php
app/Policies/TrainingReportTemplatePolicy.php
app/Http/Controllers/TrainingReportTemplateController.php
Provide admin UI for listing, creating, and editing training report templates with area assignment and draft flag.
  • Add reporttemplates index view that lists templates with their areas, draft/published status, creation date, and edit/delete actions guarded by policies.
  • Add create and edit views with EasyMDE editors for report and areas-to-improve content, multi-select assignment to areas, draft checkbox, and validation error display.
  • Wire forms to the appropriate store/update routes and redirect back to the index with success flash messages.
resources/views/admin/reporttemplates.blade.php
resources/views/admin/reporttemplates/create.blade.php
resources/views/admin/reporttemplates/edit.blade.php
Model training report templates and their many-to-many relationship with areas in the database and Eloquent models.
  • Create training_report_templates table with name, content, contentimprove (nullable), draft flag, and timestamps.
  • Create pivot table training_report_template_area with FKs to training_report_templates and areas, CASCADE rules, and uniqueness on template/area pairs, dropping any existing table first.
  • Add TrainingReportTemplate Eloquent model with guarded attributes, boolean casting for draft, and areas() belongsToMany relation.
  • Extend Area model with trainingReportTemplates() belongsToMany relation on the same pivot table.
database/migrations/2026_01_04_223423_create_training_report_templates_table.php
database/migrations/2026_01_04_223425_add_contentimprove_to_training_report_templates.php
database/migrations/2026_01_04_223424_create_training_report_template_area_table.php
app/Models/TrainingReportTemplate.php
app/Models/Area.php

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The new trainingReportTemplates() relation on Area references TrainingReportTemplate without a use import in Area.php, which will cause a class resolution error when the relation is used; consider adding use App\Models\TrainingReportTemplate; at the top of the model.
  • In the 2026_01_04_223424_create_training_report_template_area_table migration, calling Schema::dropIfExists in the up() method will silently drop any existing data on re-run; it would be safer to remove that call and instead handle cleanup in a dedicated rollback or manual migration if needed.
  • In admin/reporttemplates/edit.blade.php, the selected logic for the areas multi-select ($template->areas->contains($area->id) || in_array($area->id, old('areas', []))) means original assignments can override user changes after a validation error; you may want to prefer old('areas') when present (e.g. using in_array($area->id, old('areas', $template->areas->pluck('id')->toArray()))).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `trainingReportTemplates()` relation on `Area` references `TrainingReportTemplate` without a `use` import in `Area.php`, which will cause a class resolution error when the relation is used; consider adding `use App\Models\TrainingReportTemplate;` at the top of the model.
- In the `2026_01_04_223424_create_training_report_template_area_table` migration, calling `Schema::dropIfExists` in the `up()` method will silently drop any existing data on re-run; it would be safer to remove that call and instead handle cleanup in a dedicated rollback or manual migration if needed.
- In `admin/reporttemplates/edit.blade.php`, the selected logic for the areas multi-select (`$template->areas->contains($area->id) || in_array($area->id, old('areas', []))`) means original assignments can override user changes after a validation error; you may want to prefer `old('areas')` when present (e.g. using `in_array($area->id, old('areas', $template->areas->pluck('id')->toArray()))`).

## Individual Comments

### Comment 1
<location> `resources/views/training/report/create.blade.php:65` </location>
<code_context>
+                            <select class="form-select @error('template_id') is-invalid @enderror" name="template_id" id="templateSelect">
+                                <option value="">-- Select a template --</option>
+                                @foreach($templates as $template)
+                                    <option value="{{ $template->id }}" data-content="{{ htmlspecialchars($template->content, ENT_QUOTES, 'UTF-8') }}" data-contentimprove="{{ htmlspecialchars($template->contentimprove ?? '', ENT_QUOTES, 'UTF-8') }}">{{ $template->name }}</option>
+                                @endforeach
+                            </select>
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid double-escaping template content in data attributes, which will surface as HTML entities in the editor.

`{{ ... }}` already escapes output, so adding `htmlspecialchars` escapes it a second time. When you read `dataset.content` into the editor, users will see `&amp;` instead of `&`. Please adjust the attribute so the value is escaped exactly once, for example:

- `data-content="{!! e($template->content) !!}"`, or
- `data-content="{{ $template->content }}"` with `@php echo e(...); @endphp`

This keeps the content safe in the attribute without corrupting what appears in the editor.
</issue_to_address>

### Comment 2
<location> `database/migrations/2026_01_04_223424_create_training_report_template_area_table.php:14-15` </location>
<code_context>
+     */
+    public function up(): void
+    {
+        // Drop the table if it exists (from a previous failed migration)
+        Schema::dropIfExists('training_report_template_area');
+        
+        Schema::create('training_report_template_area', function (Blueprint $table) {
</code_context>

<issue_to_address>
**issue (bug_risk):** Dropping the pivot table in `up()` can cause unexpected data loss if migrations are re-run.

Using `Schema::dropIfExists` in `up()` means any time this migration is re-run (e.g. in non-fresh environments or some CI flows), existing pivot data will be dropped before the table is recreated. If this was only to recover from a one-off failed migration, it’s safer to remove the drop and instead do a manual cleanup or a separate corrective migration.
</issue_to_address>

### Comment 3
<location> `app/Http/Controllers/TrainingReportTemplateController.php:25-28` </location>
<code_context>
+    {
+        $this->authorize('viewAny', TrainingReportTemplate::class);
+
+        $templates = TrainingReportTemplate::with('areas')->orderBy('created_at', 'desc')->get();
+        $areas = Area::all();
+
+        return view('admin.reporttemplates', compact('templates', 'areas'));
</code_context>

<issue_to_address>
**suggestion (performance):** The `$areas` variable loaded in `index()` is not used in the corresponding view.

Since the view only references `$templates`, consider removing the `$areas = Area::all();` query and omitting `$areas` from the `view()` call to avoid an unnecessary database query on each request.

```suggestion
        $templates = TrainingReportTemplate::with('areas')->orderBy('created_at', 'desc')->get();

        return view('admin.reporttemplates', compact('templates'));
```
</issue_to_address>

### Comment 4
<location> `resources/views/admin/reporttemplates/edit.blade.php:49-54` </location>
<code_context>
+                        <label class="form-label" for="areas">Assign to Areas</label>
+                        <select class="form-select @error('areas') is-invalid @enderror" name="areas[]" id="areas" multiple size="5">
+                            @foreach($areas as $area)
+                                <option value="{{ $area->id }}" {{ $template->areas->contains($area->id) || in_array($area->id, old('areas', [])) ? 'selected' : '' }}>{{ $area->name }}</option>
+                            @endforeach
+                        </select>
</code_context>

<issue_to_address>
**suggestion:** Let `old('areas')` take precedence over the existing assignment so the form reflects failed-validation input correctly.

As written, a previously-associated area can still appear selected after a validation failure even if the user tried to unselect it, because `$template->areas->contains($area->id)` is always considered alongside `old('areas')`. To preserve the user’s last submission, branch on `old('areas')` first, e.g. `($old = old('areas')) !== null ? in_array($area->id, $old) : $template->areas->contains($area->id)`.

```suggestion
                        <label class="form-label" for="areas">Assign to Areas</label>
                        <select class="form-select @error('areas') is-invalid @enderror" name="areas[]" id="areas" multiple size="5">
                            @php($oldAreas = old('areas'))
                            @foreach($areas as $area)
                                <option value="{{ $area->id }}" {{
                                    !is_null($oldAreas)
                                        ? (in_array($area->id, $oldAreas ?? []) ? 'selected' : '')
                                        : ($template->areas->contains($area->id) ? 'selected' : '')
                                }}>{{ $area->name }}</option>
                            @endforeach
                        </select>
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

<select class="form-select @error('template_id') is-invalid @enderror" name="template_id" id="templateSelect">
<option value="">-- Select a template --</option>
@foreach($templates as $template)
<option value="{{ $template->id }}" data-content="{{ htmlspecialchars($template->content, ENT_QUOTES, 'UTF-8') }}" data-contentimprove="{{ htmlspecialchars($template->contentimprove ?? '', ENT_QUOTES, 'UTF-8') }}">{{ $template->name }}</option>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Avoid double-escaping template content in data attributes, which will surface as HTML entities in the editor.

{{ ... }} already escapes output, so adding htmlspecialchars escapes it a second time. When you read dataset.content into the editor, users will see &amp; instead of &. Please adjust the attribute so the value is escaped exactly once, for example:

  • data-content="{!! e($template->content) !!}", or
  • data-content="{{ $template->content }}" with @php echo e(...); @endphp

This keeps the content safe in the attribute without corrupting what appears in the editor.

Comment on lines +14 to +15
// Drop the table if it exists (from a previous failed migration)
Schema::dropIfExists('training_report_template_area');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Dropping the pivot table in up() can cause unexpected data loss if migrations are re-run.

Using Schema::dropIfExists in up() means any time this migration is re-run (e.g. in non-fresh environments or some CI flows), existing pivot data will be dropped before the table is recreated. If this was only to recover from a one-off failed migration, it’s safer to remove the drop and instead do a manual cleanup or a separate corrective migration.

Comment on lines +25 to +28
$templates = TrainingReportTemplate::with('areas')->orderBy('created_at', 'desc')->get();
$areas = Area::all();

return view('admin.reporttemplates', compact('templates', 'areas'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): The $areas variable loaded in index() is not used in the corresponding view.

Since the view only references $templates, consider removing the $areas = Area::all(); query and omitting $areas from the view() call to avoid an unnecessary database query on each request.

Suggested change
$templates = TrainingReportTemplate::with('areas')->orderBy('created_at', 'desc')->get();
$areas = Area::all();
return view('admin.reporttemplates', compact('templates', 'areas'));
$templates = TrainingReportTemplate::with('areas')->orderBy('created_at', 'desc')->get();
return view('admin.reporttemplates', compact('templates'));

Comment on lines +49 to +54
<label class="form-label" for="areas">Assign to Areas</label>
<select class="form-select @error('areas') is-invalid @enderror" name="areas[]" id="areas" multiple size="5">
@foreach($areas as $area)
<option value="{{ $area->id }}" {{ $template->areas->contains($area->id) || in_array($area->id, old('areas', [])) ? 'selected' : '' }}>{{ $area->name }}</option>
@endforeach
</select>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Let old('areas') take precedence over the existing assignment so the form reflects failed-validation input correctly.

As written, a previously-associated area can still appear selected after a validation failure even if the user tried to unselect it, because $template->areas->contains($area->id) is always considered alongside old('areas'). To preserve the user’s last submission, branch on old('areas') first, e.g. ($old = old('areas')) !== null ? in_array($area->id, $old) : $template->areas->contains($area->id).

Suggested change
<label class="form-label" for="areas">Assign to Areas</label>
<select class="form-select @error('areas') is-invalid @enderror" name="areas[]" id="areas" multiple size="5">
@foreach($areas as $area)
<option value="{{ $area->id }}" {{ $template->areas->contains($area->id) || in_array($area->id, old('areas', [])) ? 'selected' : '' }}>{{ $area->name }}</option>
@endforeach
</select>
<label class="form-label" for="areas">Assign to Areas</label>
<select class="form-select @error('areas') is-invalid @enderror" name="areas[]" id="areas" multiple size="5">
@php($oldAreas = old('areas'))
@foreach($areas as $area)
<option value="{{ $area->id }}" {{
!is_null($oldAreas)
? (in_array($area->id, $oldAreas ?? []) ? 'selected' : '')
: ($template->areas->contains($area->id) ? 'selected' : '')
}}>{{ $area->name }}</option>
@endforeach
</select>

@bjerrecs bjerrecs requested review from Dunkstormen and thor January 4, 2026 22:55
Copy link
Collaborator

@thor thor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoop!

I haven't looked particularly close at any one thing yet, but visually it looks interesting. Given the failing checks, I've only done a very cursory glance, and want to give it a closer review if it's fully ready.

Seeing as this is a complete feature PR, instead of bit by bit:

  • Tests! Simple, yet with good coverage. Broad and useful. Occasionally specific for the edge cases.
  • Style. Define return types with PHPs type annotations where applicable.
  • Documentation. How do you use it?

During use:

  • Would all report-related templates and, if they exist, be available for training department members (and above) only?

  • Should we keep putting things under Administration as one app or view the different parts of CC as multiple apps with their own settings?

@Atriusftw
Copy link
Contributor

Atriusftw commented Jan 7, 2026

Will training staff for each FIR be allowed to manage their own templates? Looking at the examples above it might look like this is something that would be available to Administrator only with assignment to different FIRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants