Add NPC database table with server-side DataTable and per-NPC game notes

- Npc entity (village spreadsheet columns) + game_notes text field for
  the DM's live play notes, plus updated_at touched on every change
- Race / VillageStatus / BloodCartInvolvement / MysteryImportance as
  string-backed PHP enums, rendered as dropdowns on the edit form
- /npcs: server-side DataTables (paging, global search and column sort
  all run in the backend via NpcRepository::findForDataTable); name cell
  links to the edit page, game notes shown truncated
- /npcs/{id}: Bootstrap-themed edit form (all fields + a large game
  notes textarea); Symfony forms now use the bootstrap_5 theme
- app.js: generic server-side DataTable wiring driven by data-* on the
  <table>/<th> (data-datatable-src, data-row-url, data-format)
- app:npc:import console command - upserts from data/npcs.csv by
  (family, name), never overwrites game_notes; --purge / --dry-run
- data/npcs.csv: 180 villagers exported from the working spreadsheet

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-06 15:04:39 +02:00
co-authored by Claude Sonnet 5
parent cc4fbf425d
commit 1ccab6ed68
15 changed files with 1010 additions and 9 deletions
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Form;
use App\Entity\Npc;
use App\Enum\BloodCartInvolvement;
use App\Enum\MysteryImportance;
use App\Enum\Race;
use App\Enum\VillageStatus;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class NpcType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$enum = static fn (string $class) => [
'class' => $class,
'choice_label' => static fn ($case) => $case->value,
];
$builder
->add('family', TextType::class)
->add('name', TextType::class)
->add('age', IntegerType::class, ['required' => false])
->add('race', EnumType::class, $enum(Race::class))
->add('occupation', TextType::class, ['required' => false])
->add('relationships', TextareaType::class, [
'required' => false,
'label' => 'Relationships / Notes',
'attr' => ['rows' => 3],
])
->add('villageStatus', EnumType::class, $enum(VillageStatus::class))
->add('bloodCartInvolvement', EnumType::class, $enum(BloodCartInvolvement::class))
->add('mysteryImportance', EnumType::class, $enum(MysteryImportance::class))
->add('gameNotes', TextareaType::class, [
'required' => false,
'label' => 'Game notes',
'attr' => ['rows' => 10, 'placeholder' => 'What happened with this NPC during play…'],
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => Npc::class]);
}
}