feat: feedback statistics

addresses #869bgtz2d
This commit is contained in:
Björn Fromme
2025-12-23 14:54:27 +01:00
parent 2fb4fd750b
commit cdfd172a1e
16 changed files with 745 additions and 4 deletions
+200
View File
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Feedback;
use App\Repository\FeedbackRepository;
use App\Repository\FeedbackSetRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:feedback:populate-feedback-set',
description: 'One-time command to populate missing feedbackSet for existing feedbacks',
)]
class PopulateFeedbackSetCommand extends Command
{
public function __construct(
private readonly FeedbackRepository $feedbackRepository,
private readonly FeedbackSetRepository $feedbackSetRepository,
private readonly EntityManagerInterface $entityManager,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Run without persisting changes to the database'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$isDryRun = true === $input->getOption('dry-run');
if ($isDryRun) {
$io->note('Running in dry-run mode. No changes will be persisted.');
}
// Build lookup table: question texts -> FeedbackSet
$feedbackSets = $this->feedbackSetRepository->findAll();
$lookup = [];
foreach ($feedbackSets as $feedbackSet) {
$key = $this->buildQuestionKey($feedbackSet->getRatings());
$lookup[$key] = $feedbackSet;
$io->writeln(sprintf('Registered FeedbackSet #%d "%s" with %d questions', $feedbackSet->getId(), $feedbackSet->getName(), count($feedbackSet->getRatings())));
}
// Find feedbacks without feedbackSet
$qb = $this->feedbackRepository->createQueryBuilder('f');
$feedbacks = $qb
->where($qb->expr()->isNull('f.feedbackSet'))
->andWhere($qb->expr()->isNotNull('f.ratings'))
->getQuery()
->getResult()
;
$totalCount = count($feedbacks);
if (0 === $totalCount) {
$io->success('No feedbacks found that need feedbackSet assignment.');
return Command::SUCCESS;
}
$io->info(sprintf('Found %d feedback(s) without feedbackSet.', $totalCount));
$matchedCount = 0;
$fallbackCount = 0;
$unmatchedCount = 0;
/** @var Feedback $feedback */
foreach ($feedbacks as $feedback) {
$feedbackSet = $this->matchByQuestions($feedback, $lookup);
if (null !== $feedbackSet) {
$feedback->setFeedbackSet($feedbackSet);
++$matchedCount;
if ($output->isVerbose()) {
$io->writeln(sprintf(
'Matched feedback #%d to FeedbackSet "%s" by questions',
$feedback->getId(),
$feedbackSet->getName()
));
}
continue;
}
// Fallback: try via Assignment -> JobProfile -> FeedbackSet
$feedbackSet = $this->matchByAssignment($feedback);
if (null !== $feedbackSet) {
$feedback->setFeedbackSet($feedbackSet);
++$fallbackCount;
if ($output->isVerbose()) {
$io->writeln(sprintf(
'Matched feedback #%d to FeedbackSet "%s" via Assignment fallback',
$feedback->getId(),
$feedbackSet->getName()
));
}
continue;
}
++$unmatchedCount;
$io->warning(sprintf(
'Could not match feedback #%d (no matching FeedbackSet found)',
$feedback->getId()
));
}
if (false === $isDryRun && ($matchedCount > 0 || $fallbackCount > 0)) {
$this->entityManager->flush();
$io->success(sprintf(
'Successfully updated %d feedback(s): %d by questions, %d by assignment fallback. %d unmatched.',
$matchedCount + $fallbackCount,
$matchedCount,
$fallbackCount,
$unmatchedCount
));
} elseif ($isDryRun) {
$io->success(sprintf(
'Dry-run complete. Would update %d feedback(s): %d by questions, %d by assignment fallback. %d unmatched.',
$matchedCount + $fallbackCount,
$matchedCount,
$fallbackCount,
$unmatchedCount
));
} else {
$io->info(sprintf('No feedbacks updated. %d unmatched.', $unmatchedCount));
}
return Command::SUCCESS;
}
/**
* @param array<int, array{rating: string, mark: int}> $ratings
*/
private function buildQuestionKey(array $ratings): string
{
// For FeedbackSet, ratings is array of strings
if (isset($ratings[0]) && is_string($ratings[0])) {
return implode('|', $ratings);
}
// For Feedback, ratings is array of {rating, mark}
$questions = array_map(fn (array $r): string => $r['rating'], $ratings);
return implode('|', $questions);
}
/**
* @param array<string, \App\Entity\FeedbackSet> $lookup
*/
private function matchByQuestions(Feedback $feedback, array $lookup): ?\App\Entity\FeedbackSet
{
$ratings = $feedback->getRatings();
if (0 === count($ratings)) {
return null;
}
$key = $this->buildQuestionKey($ratings);
return $lookup[$key] ?? null;
}
private function matchByAssignment(Feedback $feedback): ?\App\Entity\FeedbackSet
{
$assignment = $feedback->getAssignment();
if (null === $assignment) {
return null;
}
$jobProfile = $assignment->getJobProfile();
if (null === $jobProfile) {
return null;
}
return $jobProfile->getFeedbackSet();
}
}