Add staff notes functionality

This commit is contained in:
Ryan Prather 2024-12-22 01:15:15 +00:00
parent 6b61d1a182
commit 5a531ae171
9 changed files with 624 additions and 3 deletions

View File

@ -2,21 +2,35 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\MemberCase;
use App\Entity\Messages;
use App\Entity\StaffNote;
use App\Entity\Supervision;
use App\Entity\User; use App\Entity\User;
use App\Entity\UserCase;
use App\Form\StaffNoteFormType;
use App\Libs\Breadcrumb; use App\Libs\Breadcrumb;
use App\Libs\NavList; use App\Libs\NavList;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser; use Symfony\Component\Security\Http\Attribute\CurrentUser;
class StaffController extends AbstractController class StaffController extends AbstractController
{ {
/**
* Variable to store unread notification messages
*
* @var array <int, Message>
*/
private array $msgs;
private int $notificationCount;
public function __construct( public function __construct(
private readonly EntityManagerInterface $entityManager, private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $userPasswordHasher,
private array $navLinks = [] private array $navLinks = []
) { ) {
$this->navLinks = NavList::LIST; $this->navLinks = NavList::LIST;
@ -26,17 +40,203 @@ class StaffController extends AbstractController
#[Route('/staff-dashboard', name: 'app_staff_dashboard')] #[Route('/staff-dashboard', name: 'app_staff_dashboard')]
public function staffDashboard(#[CurrentUser()] User $user): Response public function staffDashboard(#[CurrentUser()] User $user): Response
{ {
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$sups = $this->entityManager->getRepository(Supervision::class)->findBy(['supervisor' => $user]);
$staff = [];
foreach ($sups as $sup) {
$staff[] = $sup->getWorker();
}
return $this->render( return $this->render(
'internal/staff/staff-dashboard.html.twig', 'internal/staff/staff-dashboard.html.twig',
array_merge( array_merge(
$this->navLinks, $this->navLinks,
[ [
'staff' => $staff,
'breadcrumbs' => [ 'breadcrumbs' => [
new Breadcrumb('', 'Staff Dashboard') new Breadcrumb('', 'Staff Dashboard')
], ],
'notifications' => $user->retrieveUnreadNotifications(), 'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount,
] ]
) )
); );
} }
#[Route('/staff/my-cases', name:'app_staff_my_cases')]
public function staffMyCases(#[CurrentUser()] User $user): Response
{
$this->denyAccessUnlessGranted('IS_FULLY_AUTHENTICATED');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$ucs = $this->entityManager->getRepository(UserCase::class)->findBy(['user' => $user]);
$cases = [];
$this->navLinks['staff_dashboard'] = 'nav-link text-dark';
$this->navLinks['staff_notes'] = NavList::PRESENT_LINK;
foreach ($ucs as $uc) {
$cases[] = $uc->getMemberCase();
}
return $this->render(
'internal/staff/cases/my-cases.html.twig',
array_merge(
$this->navLinks,
[
'cases' => $cases,
'user' => $user,
'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_staff_dashboard'), 'Staff Dashboard'),
new Breadcrumb('', 'Staff Cases')
],
'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount,
]
)
);
}
#[Route('/staff/{staffId}', name: 'app_staff_cases')]
public function staffCases(string $staffId, #[CurrentUser()] User $user): Response
{
$this->denyAccessUnlessGranted(['ROLE_ADMIN', 'ROLE_CASE_MANAGER']);
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$staff = $this->entityManager->getRepository(User::class)->find($staffId);
$ucs = $this->entityManager->getRepository(UserCase::class)->findBy(['user' => $staff]);
$cases = [];
foreach ($ucs as $case) {
/** @var UserCase $case */
$cases[] = $case->getMemberCase();
}
return $this->render(
'internal/staff/cases/staff-cases.html.twig',
array_merge(
$this->navLinks,
[
'staffId' => $staffId,
'cases' => $cases,
'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_staff_dashboard'), 'Staff Dashboard'),
new Breadcrumb('', 'Staff Cases')
],
'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount,
]
)
);
}
#[Route('/staff/{staffId}/case/{caseId}/list-notes', name: 'app_staff_list_notes')]
public function staffListNotes(string $staffId, string $caseId, #[CurrentUser()] User $user): Response
{
$this->denyAccessUnlessGranted('IS_FULLY_AUTHENTICATED');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$staff = $this->entityManager->getRepository(User::class)->find($staffId);
$case = $this->entityManager->getRepository(MemberCase::class)->find($caseId);
$staffNotes = $this->entityManager->getRepository(StaffNote::class)->findBy(['memberCase' => $case]);
return $this->render(
'internal/staff/notes/list-notes.html.twig',
array_merge(
$this->navLinks,
[
'staffId' => $staffId,
'staff' => $staff,
'case' => $case,
'staffNotes' => $staffNotes,
'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_staff_dashboard'), 'Staff Dashboard'),
new Breadcrumb(
$this->generateUrl(
($staffId == $user->getId()->toHex() ? 'app_staff_my_cases' : 'app_staff_cases'),
['staffId' => $staffId]
),
($staffId == $user->getId()->toHex() ? 'My Cases' : 'Staff Cases')
),
new Breadcrumb('', 'List Notes')
],
'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount,
]
)
);
}
#[Route('/staff/add-note/{caseId}', name: 'app_staff_add_note')]
public function addNote(string $caseId, #[CurrentUser()] User $user, Request $request): Response
{
$this->denyAccessUnlessGranted('IS_FULLY_AUTHENTICATED');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$case = $this->entityManager->getRepository(MemberCase::class)->find($caseId);
$form = $this->createForm(StaffNoteFormType::class);
$this->navLinks['staff_dashboard'] = 'nav-link text-dark';
$this->navLinks['staff_notes'] = NavList::PRESENT_LINK;
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$note = $form->getData();
$note->setMemberCase($case);
$this->entityManager->persist($note);
$this->entityManager->flush();
return $this->redirectToRoute('app_staff_list_notes', ['staffId' => $user->getId()->toHex(), 'caseId' => $caseId]);
}
return $this->render(
'internal/staff/notes/add-note.html.twig',
array_merge(
$this->navLinks,
[
'case' => $case,
'form' => $form,
'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_staff_dashboard'), 'Staff Dashboard'),
new Breadcrumb($this->generateUrl('app_staff_list_notes', ['staffId' => $user->getId()->toHex(), 'caseId' => $caseId]), 'My Cases'),
new Breadcrumb('', 'Add Note'),
],
'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount,
]
)
);
}
#[Route('/staff/edit-note/{noteId}', name: 'app_staff_edit_note')]
public function editNote(string $noteId, #[CurrentUser()] User $user, Request $request): Response
{
$this->denyAccessUnlessGranted('IS_FULLY_AUTHENTICATED');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$note = $this->entityManager->getRepository(StaffNote::class)->find($noteId);
$case = $note->getMemberCase();
$form = $this->createForm(StaffNoteFormType::class, $note);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->flush();
$this->addFlash('info', 'Staff notes updated');
$this->redirectToRoute('app_staff_list_note', ['staffId' => $user->getId()->toHex(), 'caseId' => $case->getId()->toHex()]);
}
return new Response();
}
} }

101
src/Entity/StaffNote.php Normal file
View File

@ -0,0 +1,101 @@
<?php
namespace App\Entity;
use App\Enums\ReferralServiceType;
use App\Repository\StaffNoteRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: StaffNoteRepository::class)]
class StaffNote
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::DATE_MUTABLE)]
private ?\DateTimeInterface $date = null;
#[ORM\ManyToOne(inversedBy: 'staffNotes')]
#[ORM\JoinColumn(nullable: false)]
private ?MemberCase $memberCase = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY, enumType: ReferralServiceType::class)]
private array $servicesProvided = [];
#[ORM\Column(type: Types::TEXT)]
private ?string $note = null;
#[ORM\Column(length: 500, nullable: true)]
private ?string $recommendations = null;
public function getId(): ?int
{
return $this->id;
}
public function getDate(): ?\DateTimeInterface
{
return $this->date;
}
public function setDate(\DateTimeInterface $date): static
{
$this->date = $date;
return $this;
}
public function getMemberCase(): ?MemberCase
{
return $this->memberCase;
}
public function setMemberCase(?MemberCase $memberCase): static
{
$this->memberCase = $memberCase;
return $this;
}
/**
* @return ReferralServiceType[]
*/
public function getServicesProvided(): array
{
return $this->servicesProvided;
}
public function setServicesProvided(array $servicesProvided): static
{
$this->servicesProvided = $servicesProvided;
return $this;
}
public function getNote(): ?string
{
return $this->note;
}
public function setNote(string $note): static
{
$this->note = $note;
return $this;
}
public function getRecommendations(): ?string
{
return $this->recommendations;
}
public function setRecommendations(string $recommendations): static
{
$this->recommendations = $recommendations;
return $this;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Form;
use App\Entity\Member;
use App\Entity\StaffNote;
use App\Enums\ReferralServiceType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StaffNoteFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('date', null, [
'widget' => 'single_text',
])
->add('servicesProvided', EnumType::class, [
'class' => ReferralServiceType::class,
'expanded' => true,
'multiple' => true,
])
->add('note', TextareaType::class)
->add('recommendations')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => StaffNote::class,
]);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\StaffNote;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<StaffNote>
*/
class StaffNoteRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, StaffNote::class);
}
// /**
// * @return StaffNote[] Returns an array of StaffNote objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('s.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?StaffNote
// {
// return $this->createQueryBuilder('s')
// ->andWhere('s.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@ -0,0 +1,44 @@
{% extends 'base.html.twig' %}
{% block body %}
{{ block('nav', 'internal/libs/nav.html.twig') }}
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg ">
{{ block('topnav', 'internal/libs/top-nav.html.twig') }}
<div class='container-fluid py-2'>
<div class='row'>
<div class='ms-3'>
<h3 class='mb-0 h4 font-weight-bolder'>My Cases</h3>
<p class='mb-4'></p>
</div>
{% for c in cases %}
<div class='col-xl-3 col-sm-6 mb-xl-0 mb-4'>
<div class='card'>
<div class='card-header p-2 ps-3'>
<div class='d-flex justify-content-between'>
<div>
<h4 class='mb-0'>{{ c.caseName }}</h4>
<p class='text-sm mb-0 text-capitalize'>
<a href='tel:'></a>
</p>
</div>
<div class="icon icon-md icon-shape bg-gradient-dark shadow-dark shadow text-center border-radius-lg">
<i class="material-symbols-rounded opacity-10">weekend</i>
</div>
</div>
</div>
<hr class='dark horizontal my-0'>
<div class='card-footer p-2 ps-3'>
<p class='mb-0 text-sm'>
<span class='text-info font-weight-bolder'>
<a href='{{ path('app_staff_list_notes', {staffId: user.id, caseId: c.id}) }}'>Staff Notes</a>
</span>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends 'base.html.twig' %}
{% block body %}
{{ block('nav', 'internal/libs/nav.html.twig') }}
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg ">
{{ block('topnav', 'internal/libs/top-nav.html.twig') }}
<div class='container-fluid py-2'>
<div class='row'>
<div class='ms-3'>
<h3 class='mb-0 h4 font-weight-bolder'>Staff Cases</h3>
<p class='mb-4'></p>
</div>
{% for c in cases %}
<div class='col-xl-3 col-sm-6 mb-xl-0 mb-4'>
<div class='card'>
<div class='card-header p-2 ps-3'>
<div class='d-flex justify-content-between'>
<div>
<h4 class='mb-0'>{{ c.caseName }}</h4>
<p class='text-sm mb-0 text-capitalize'>
<a href='tel:'></a>
</p>
</div>
<div class="icon icon-md icon-shape bg-gradient-dark shadow-dark shadow text-center border-radius-lg">
<i class="material-symbols-rounded opacity-10">weekend</i>
</div>
</div>
</div>
<hr class='dark horizontal my-0'>
<div class='card-footer p-2 ps-3'>
<p class='mb-0 text-sm'>
<span class='text-info font-weight-bolder'>
<a href='{{ path('app_staff_list_notes', {staffId: staffId, caseId: c.id}) }}'>Staff Notes</a>
</span>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</main>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends 'base.html.twig' %}
{% block body %}
{{ block('nav', 'internal/libs/nav.html.twig') }}
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg ">
{{ block('topnav', 'internal/libs/top-nav.html.twig') }}
<section>
<div class="card card-plain">
<div class="card-header">
<h4 class="font-weight-bolder">Case Info</h4>
<p class="mb-0"></p>
</div>
<div class="card-body">
<div class="container">
{{ form_errors(form) }}
{{ form_start(form) }}
<div class="row">
<div class='col'>
<div class='input-group input-group-outline mb-3'>
<label for='note_form_date'></label>
<input type='date' name='{{ field_name(form.date) }}' id='note_form_date' class='form-control'/>
</div>
<div class='input-group input-group-outline mb-3'>
{{ form_row(form.servicesProvided) }}
</div>
<div class='input-group input-group-outline mb-3'>
<label for='note_form_recommendation' class='form-label'>Recommendation</label>
<input type='text' name='{{ field_name(form.recommendations) }}' id='note_form_recommendation' class='form-control'/>
</div>
</div>
<div class='col'>
<div class='input-group input-group-outline mb-3'>
<textarea name='{{ field_name(form.note) }}' id='note_form_note' class='form-control' placeholder='Note' style='width:100%;height:200px;'></textarea>
</div>
</div>
</div>
<div class='row'>
<div class="text-center">
<button type="submit" class="btn btn-lg bg-gradient-dark btn-lg w-100 mt-4 mb-0">Save Staff Note</button>
</div>
</div>
{{ form_end(form) }}
</div>
</div>
</div>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends 'base.html.twig' %}
{% block body %}
{{ block('nav', 'internal/libs/nav.html.twig') }}
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg ">
{{ block('topnav', 'internal/libs/top-nav.html.twig') }}
<div class="container-fluid py-2">
<div class="row">
<div class="col-12">
<div class="card my-4">
<div class="card-header p-0 position-relative mt-n4 mx-3 z-index-2">
<div class="d-flex justify-content-between bg-gradient-dark shadow-dark border-radius-lg pt-4 pb-3 ps-3 p-2">
<div>
<h6 class="text-white text-capitalize ps-3">{{ case.caseName }}</h6>
</div>
{% if app.user.id == staffId %}
<div>
<button type="button" class="btn btn-block btn-light mb-3" onclick="window.open('{{ path('app_staff_add_note', {caseId: case.id}) }}', '_self')">Add Staff Note</button>
</div>
{% endif %}
</div>
</div>
<div class="card-body px-0 pb-2">
<div class="table-responsive p-0">
<table class="table align-items-center mb-0">
<thead>
<tr>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">Date</th>
<th class="text-secondary opacity-7"></th>
</tr>
</thead>
<tbody id='case-list'>
{% for n in staffNotes %}
<tr>
<td>
<div class='d-flex px-2 py-1'>
<div class='d-flex flex-column justify-content-center'>
<h6 class='mb-0 text-small'>{{ n.date|date('F j, Y') }}</h6>
</div>
</div>
</td>
<td class='align-right'>
<a href='{{ path('app_staff_edit_note', {noteId: n.id}) }}' title='Edit Note'>
<i class='material-symbols-rounded opacity-5'>edit</i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}

View File

@ -5,5 +5,40 @@
<main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg "> <main class="main-content position-relative max-height-vh-100 h-100 border-radius-lg ">
{{ block('topnav', 'internal/libs/top-nav.html.twig') }} {{ block('topnav', 'internal/libs/top-nav.html.twig') }}
<div class='container-fluid py-2'>
<div class='row'>
<div class='ms-3'>
<h3 class='mb-0 h4 font-weight-bolder'>Staff Dashboard</h3>
<p class='mb-4'></p>
</div>
{% for s in staff %}
<div class='col-xl-3 col-sm-6 mb-xl-0 mb-4'>
<div class='card'>
<div class='card-header p-2 ps-3'>
<div class='d-flex justify-content-between'>
<div>
<h4 class='mb-0'>{{ s.name }}</h4>
<p class='text-sm mb-0 text-capitalize'>
<a href='tel:'></a>
</p>
</div>
<div class="icon icon-md icon-shape bg-gradient-dark shadow-dark shadow text-center border-radius-lg">
<i class="material-symbols-rounded opacity-10">weekend</i>
</div>
</div>
</div>
<hr class='dark horizontal my-0'>
<div class='card-footer p-2 ps-3'>
<p class='mb-0 text-sm'>
<span class='text-info font-weight-bolder'>
<a href='{{ path('app_staff_cases', {staffId: s.id}) }}'>Staff Cases</a>
</span>
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</main> </main>
{% endblock %} {% endblock %}