Compare commits

..

13 Commits

18 changed files with 254 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -14,6 +14,7 @@
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/reflection-docblock": "^5.4",
"phpstan/phpdoc-parser": "^1.33", "phpstan/phpdoc-parser": "^1.33",
"symfony-cmf/slugifier-api": "^2.1",
"symfony/asset": "7.2.*", "symfony/asset": "7.2.*",
"symfony/asset-mapper": "7.2.*", "symfony/asset-mapper": "7.2.*",
"symfony/console": "7.2.*", "symfony/console": "7.2.*",

View File

@ -79,4 +79,7 @@ return [
'underscore' => [ 'underscore' => [
'version' => '1.5.2', 'version' => '1.5.2',
], ],
'chartjs' => [
'version' => '0.3.24',
],
]; ];

View File

@ -12,6 +12,7 @@ use App\Form\UserFormType;
use App\Libs\Breadcrumb; use App\Libs\Breadcrumb;
use App\Libs\NavList; use App\Libs\NavList;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use DateTime;
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\Request;
@ -20,6 +21,7 @@ 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;
use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\String\Slugger\SluggerInterface;
#[IsGranted('ROLE_ADMIN')] #[IsGranted('ROLE_ADMIN')]
class AdminController extends AbstractController class AdminController extends AbstractController
@ -96,8 +98,11 @@ class AdminController extends AbstractController
} }
#[Route('/add-user', name: 'app_add_user')] #[Route('/add-user', name: 'app_add_user')]
public function addUser(Request $request, #[CurrentUser()] User $admin): Response public function addUser(
{ Request $request,
#[CurrentUser()] User $admin,
SluggerInterface $slugger
): Response {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($admin); $this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($admin);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($admin); $this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($admin);
@ -141,8 +146,22 @@ class AdminController extends AbstractController
->setRate($form->get('rate')->getData()) ->setRate($form->get('rate')->getData())
->setLevel($form->get('level')->getData()) ->setLevel($form->get('level')->getData())
->setCompany($admin->getCompany()) ->setCompany($admin->getCompany())
->setPasswordChanged(new DateTime())
; ;
if ($form->get('imageName')->getData()) {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $form['imageName']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads/user_images/';
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = $slugger->slug($originalFilename).'-'.uniqid().'.'.$file->guessExtension();
$file->move(
$destination,
$newFilename
);
$user->setImageName($newFilename);
}
$msg = MessageFactory::createUser($admin, $user, 'Welcome', "Welcome to CM Tracker"); $msg = MessageFactory::createUser($admin, $user, 'Welcome', "Welcome to CM Tracker");

View File

@ -71,17 +71,21 @@ class CommunityResourceController extends AbstractController
$rcs = $this->entityManager->getRepository(CommunityResource::class)->findAll(); $rcs = $this->entityManager->getRepository(CommunityResource::class)->findAll();
$map = new Map('default'); $map = new Map('default');
$map->center(new Point(39.768502, -86.157918)) $map->center(new Point($_ENV['MAP_CENTER_LAT'], $_ENV['MAP_CENTER_LON']))
->zoom(9) ->zoom($_ENV['MAP_ZOOM_LEVEL'])
; ;
foreach ($rcs as $rsc) { foreach ($rcs as $rsc) {
/** @var CommunityResource $rsc */
$map->addMarker(new Marker( $map->addMarker(new Marker(
position: new Point($rsc->getLat(), $rsc->getLon()), position: new Point($rsc->getLat(), $rsc->getLon()),
title: $rsc->getName(), title: $rsc->getName(),
infoWindow: new InfoWindow( infoWindow: new InfoWindow(
content: "{$rsc->getName()}<br>{$rsc->getAddress()}, {$rsc->getCity()}, {$rsc->getState()->value} {$rsc->getZip()}<br>Services: " . $rsc->getServicesAvailable() content: "{$rsc->getName()}<br>{$rsc->getAddress()}, {$rsc->getCity()}, {$rsc->getState()->value} {$rsc->getZip()}<br>Services: " . $rsc->getServicesAvailable()
) ),
extra: [
'type' => ''
]
)); ));
} }
@ -92,6 +96,7 @@ class CommunityResourceController extends AbstractController
[ [
'map' => $map, 'map' => $map,
'breadcrumbs' => [ 'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_community_resource'), 'List Resources'),
new Breadcrumb('#', 'Community Resources') new Breadcrumb('#', 'Community Resources')
], ],
'notifications' => $this->msgs, 'notifications' => $this->msgs,

View File

@ -6,13 +6,23 @@ use App\Entity\Messages;
use App\Entity\User; use App\Entity\User;
use App\Libs\Breadcrumb; use App\Libs\Breadcrumb;
use App\Libs\NavList; use App\Libs\NavList;
use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; 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;
use Symfony\Component\String\Slugger\SluggerInterface;
use Vich\UploaderBundle\Entity\File;
class DefaultController extends AbstractController class DefaultController extends AbstractController
{ {
@ -56,20 +66,106 @@ class DefaultController extends AbstractController
], ],
'notifications' => $this->msgs, 'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount, 'notificationCount' => $this->notificationCount,
'milesTravelledYTD' => 0,
'milesTravelled30Days' => 0,
] ]
) )
); );
} }
#[Route('/profile', name: 'app_profile')] #[Route('/profile', name: 'app_profile')]
public function profile(#[CurrentUser()] User $user): Response public function profile(
{ Request $request,
#[CurrentUser()] User $user,
SluggerInterface $slugger
): Response {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user); $this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user); $this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
$this->navLinks['profile'] = NavList::PRESENT_LINK; $this->navLinks['profile'] = NavList::PRESENT_LINK;
$form = $this->createFormBuilder($user)
->add('name', TextType::class, [
'label' => 'Name',
'label_attr' => ['class' => 'form-label'],
'attr' => ['class' => 'form-control'],
])
->add('email', EmailType::class, [
'label' => 'Email',
'label_attr' => ['class' => 'form-label'],
'attr' => ['class' => 'form-control'],
])
->add('password', RepeatedType::class, [
'type' => PasswordType::class,
'invalid_message' => 'The password fields must match.',
'required' => false,
'mapped' => false,
'first_options' => ['label' => 'Password', 'label_attr' => ['class' => 'form-label']],
'second_options' => ['label' => 'Repeat Password', 'label_attr' => ['class' => 'form-label']],
])
->add('imageName', FileType::class, [
'label' => 'Profile Picture',
'required' => false,
'mapped' => false
])
->add('submit', SubmitType::class, [
'label' => 'Save Profile',
'attr' => ['class' => 'btn btn-lg bg-gradient-dark btn-lg w-100 mt-4 mb-0']
])
->getForm()
;
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$firstPassword = $form->get('password')['first']->getData();
$secondPassword = $form->get('password')['second']->getData();
if ($firstPassword !== $secondPassword) {
$this->addFlash('danger', 'The password fields must match.');
return $this->redirectToRoute('app_profile');
}
$plainPassword = $form->get('password')['first']->getData();
if ($plainPassword) {
$user->setPassword(
$this->userPasswordHasher->hashPassword(
$user,
$plainPassword
)
);
$user->setPasswordChanged(new DateTime())
}
if ($form['imageName']->getData()) {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $form['imageName']->getData();
$destination = $this->getParameter('kernel.project_dir').'/public/uploads/user_images/';
if (!file_exists($destination)) {
mkdir($destination, 0775);
}
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = $slugger->slug($originalFilename).'-'.uniqid().'.'.$file->guessExtension();
$file->move(
$destination,
$newFilename
);
$user->setImageName($newFilename);
}
$this->entityManager->persist($user);
$this->entityManager->flush();
return $this->redirectToRoute('app_dashboard');
} elseif ($form->isSubmitted() && !$form->isValid()) {
$this->addFlash('danger', 'The form contains errors.');
}
return $this->render( return $this->render(
'internal/profile.html.twig', 'internal/profile.html.twig',
array_merge( array_merge(
@ -80,8 +176,16 @@ class DefaultController extends AbstractController
], ],
'notifications' => $this->msgs, 'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount, 'notificationCount' => $this->notificationCount,
'currentUser' => $user,
'form' => $form->createView(),
] ]
) )
); );
} }
#[Route('/uploads/user_images/{imageName}', name: 'app_user_image')]
public function displayUserImage(string $imageName): Response
{
return new BinaryFileResponse($this->getParameter('kernel.project_dir')."/public/uploads/user_images/{$imageName}");
}
} }

View File

@ -42,6 +42,8 @@ class ItineraryController extends AbstractController
#[Route('/itinerary/report', name: 'app_report_itinerary')] #[Route('/itinerary/report', name: 'app_report_itinerary')]
public function reportItinerary(Request $request, #[CurrentUser()] ?User $user): Response public function reportItinerary(Request $request, #[CurrentUser()] ?User $user): Response
{ {
$this->navLinks['case_itinerary'] = NavList::PRESENT_LINK;
$this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user); $this->msgs = $this->entityManager->getRepository(Messages::class)->getUnreadMessages($user);
$this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user); $this->notificationCount = $this->entityManager->getRepository(Messages::class)->getUnreadMessageCount($user);
@ -76,6 +78,8 @@ class ItineraryController extends AbstractController
#[Route('/itinerary/map', name: 'app_map_itinerary')] #[Route('/itinerary/map', name: 'app_map_itinerary')]
public function mapItinerary(Request $request, #[CurrentUser()] ?User $user): Response public function mapItinerary(Request $request, #[CurrentUser()] ?User $user): Response
{ {
$this->navLinks['case_itinerary'] = NavList::PRESENT_LINK;
$case = null; $case = null;
if ($request->getPayload()->get('caseId')) { if ($request->getPayload()->get('caseId')) {
$case = $this->entityManager->getRepository(MemberCase::class)->find($request->getPayload()->get('caseId')); $case = $this->entityManager->getRepository(MemberCase::class)->find($request->getPayload()->get('caseId'));
@ -93,8 +97,8 @@ class ItineraryController extends AbstractController
]); ]);
$map = new Map('default'); $map = new Map('default');
$map->center(new Point(39.768502, -86.157918)) $map->center(new Point($_ENV['MAP_CENTER_LAT'], $_ENV['MAP_CENTER_LON']))
->zoom(9) ->zoom($_ENV['MAP_ZOOM_LEVEL'])
; ;
$total_distance = 0; $total_distance = 0;
@ -148,7 +152,7 @@ class ItineraryController extends AbstractController
[ [
'breadcrumbs' => [ 'breadcrumbs' => [
new Breadcrumb($this->generateUrl('app_my_cases'), 'My Cases'), new Breadcrumb($this->generateUrl('app_my_cases'), 'My Cases'),
new Breadcrumb($this->generateUrl('app_itinerary_report'), 'Itinerary Report'), new Breadcrumb($this->generateUrl('app_report_itinerary'), 'Itinerary Report'),
], ],
'notifications' => $this->msgs, 'notifications' => $this->msgs,
'notificationCount' => $this->notificationCount, 'notificationCount' => $this->notificationCount,

View File

@ -14,10 +14,13 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
use Vich\UploaderBundle\Entity\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_USERNAME', fields: ['username'])]
#[UniqueEntity(fields: ['username'], message: 'There is already an account with this username')] #[UniqueEntity(fields: ['username'], message: 'There is already an account with this username')]
#[Vich\Uploadable]
class User implements UserInterface, PasswordAuthenticatedUserInterface class User implements UserInterface, PasswordAuthenticatedUserInterface
{ {
#[ORM\Id] #[ORM\Id]
@ -80,6 +83,15 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: UserCase::class, mappedBy: 'user')] #[ORM\OneToMany(targetEntity: UserCase::class, mappedBy: 'user')]
private Collection $userCases; private Collection $userCases;
#[Vich\UploadableField(mapping: 'profile_image', fileNameProperty: 'imageName', size: 'size', mimeType: 'mimeType', originalName: 'originalName', dimensions: 'dimensions')]
private ?File $imageFile = null;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
private ?string $imageName = null;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private ?\DateTimeInterface $passwordChanged = null;
public function __construct() public function __construct()
{ {
$this->userCases = new ArrayCollection(); $this->userCases = new ArrayCollection();
@ -351,4 +363,40 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $jobs; return $jobs;
} }
public function getImageFile(): ?File
{
return $this->imageFile;
}
public function setImageFile(?File $imageFile): static
{
$this->imageFile = $imageFile;
return $this;
}
public function getImageName(): ?string
{
return $this->imageName;
}
public function setImageName(?string $imageName): static
{
$this->imageName = $imageName;
return $this;
}
public function getPasswordChanged(): ?\DateTimeInterface
{
return $this->passwordChanged;
}
public function setPasswordChanged(\DateTimeInterface $passwordChanged): static
{
$this->passwordChanged = $passwordChanged;
return $this;
}
} }

View File

@ -27,8 +27,10 @@
<link <link
rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0"/> rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0"/>
<!-- CSS Files --> <!-- CSS Files -->
<link id="pagestyle" href="{{ asset('css/material-dashboard.css') }}?v=3.2.0" rel="stylesheet"/> {% block stylesheets %}{% endblock %} <link id="pagestyle" href="{{ asset('css/material-dashboard.css') }}?v=3.2.0" rel="stylesheet"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
{% block stylesheets %}{% endblock %}
{% block page_css %}{% endblock %}
</head> </head>
<body class="bg-gray-200"> {% block body %}{% endblock %} <body class="bg-gray-200"> {% block body %}{% endblock %}

View File

@ -83,6 +83,10 @@
<label for='user_form_rate' class='form-label'>Rate</label> <label for='user_form_rate' class='form-label'>Rate</label>
<input type='number' name='{{ field_name(form.rate) }}' id='user_form_rate' class='form-control' min='0' step='0.01'/> <input type='number' name='{{ field_name(form.rate) }}' id='user_form_rate' class='form-control' min='0' step='0.01'/>
</div> </div>
<div class='input-group input-group-outline mb-3'>
<label for='user_form_file' class='form-label'>Profile Image</label>
<input type='file' name='{{ field_name(form.imageName) }}' id='user_form_file' class='form-control'/>
</div>
<div class="text-center"> <div class="text-center">
<button type="submit" class="btn btn-lg bg-gradient-dark btn-lg w-100 mt-4 mb-0">Add User</button> <button type="submit" class="btn btn-lg bg-gradient-dark btn-lg w-100 mt-4 mb-0">Add User</button>

View File

@ -9,7 +9,7 @@
<div class="container-fluid py-2"> <div class="container-fluid py-2">
<div> <div>
Totals: Totals:
{{ total_distance }} {{ total_distance }}mi
/ /
{{ total_duration }} {{ total_duration }}
</div> </div>

View File

@ -114,10 +114,5 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="sidenav-footer position-absolute w-100 bottom-0 ">
<div class="mx-3">
<a class="btn btn-outline-dark mt-4 w-100" href="https://www.creative-tim.com/learning-lab/bootstrap/overview/material-dashboard?ref=sidebarfree" type="button">Documentation</a>
</div>
</div>
</aside> </aside>
{% endblock %} {% endblock %}

View File

@ -6,7 +6,7 @@
<div class='ms-md-auto pe-md-3 d-flex align-items-left' id='messages'> <div class='ms-md-auto pe-md-3 d-flex align-items-left' id='messages'>
{% for label, messages in app.flashes %} {% for label, messages in app.flashes %}
{% for message in messages %} {% for message in messages %}
<div class="flash-{{ label }} bg-gradient-info text-white"> <div class="flash-{{ label }} bg-gradient-{{ label }} text-white">
{{ message }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}

View File

@ -8,5 +8,56 @@
<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') }}
{{ form_start(form) }}
{{ form_errors(form) }}
<div class='container'>
<div class='row'>
<div class='col' id='profile-image'>
<input type='hidden' name='id' value='{{ currentUser.id }}'/>
<img class='profile-image' src='{% if currentUser.imageName %}/uploads/user_images/{{ currentUser.imageName }}{% endif %}'/>
{{ form_row(form.imageName) }}
</div>
<div class='col'>
<div class='input-group input-group-outline mb-3 is-filled'>
<label for='profile_form_name' class='form-label'>Name</label>
<input type='text' name='{{ field_name(form.name) }}' id='profile_form_name' class='form-control' value='{{ currentUser.name }}'/>
</div>
<div class='input-group input-group-outline mb-3 is-filled'>
<label for='profile_form_email' class='form-label'>Email</label>
<input type='email' name='{{ field_name(form.email) }}' id='profile_form_email' class='form-control' value='{{ currentUser.email }}'/>
</div>
<div class='input-group input-group-outline mb-3'>
<label for='profile_form_password' class='form-label'>Password</label>
<input type='password' name='{{ field_name(form.password.first) }}' id='profile_form_password' class='form-control' autocomplete='new-password'/>
</div>
<div class='input-group input-group-outline mb-3'>
<label for='profile_form_confirmPassword' class='form-label'>Confirm Password</label>
<input type='password' name='{{ field_name(form.password.second) }}' id='profile_form_confirmPassword' class='form-control' autocomplete='new-password'/>
</div>
</div>
</div>
<div class='row'>
<div class='col text-center'>
<div class='input-group input-group-outline mb-3'>
{{ form_row(form.submit) }}
</div>
</div>
</div>
</div>
{{ form_end(form) }}
</main> </main>
{% endblock %} {% endblock %}
{% block page_css %}
<style rel='stylesheet'>
.profile-image {
width: 300px;
height: 300px;
}
</style>
{% endblock %}