<?php
namespace Products\NotificationsBundle\Controller\Dashboard;
use App\Component\ViewLayer\Views\AbstractHtmlView;
use App\Component\ViewLayer\Views\AjaxHtmlView;
use App\Controller\PaginationTrait;
use App\Entity\System\School;
use App\Entity\System\SocialAccount;
use App\Entity\System\SocialAccounts\FacebookSocialAccount;
use App\Entity\System\SocialAccounts\InstagramSocialAccount;
use App\Entity\System\SocialAccounts\TwitterSocialAccount;
use App\Service\Social\FacebookService;
use App\Service\Social\InstagramService;
use App\Service\Social\TwitterService;
use Cms\ContainerBundle\Entity\Containers\StorageContainer;
use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
use Cms\CoreBundle\Entity\OneRosterSync;
use Cms\FileBundle\Entity\Nodes\Folder;
use DateTime;
use Products\NotificationsBundle\Controller\AbstractDashboardController;
use Products\NotificationsBundle\Doctrine\Repository\ProfileRepository;
use Products\NotificationsBundle\Doctrine\Repository\StudentRepository;
use Products\NotificationsBundle\Entity\AbstractNotification;
use Products\NotificationsBundle\Entity\Checkup;
use Products\NotificationsBundle\Entity\Lists\ConditionList;
use Products\NotificationsBundle\Entity\Notifications\Message;
use Products\NotificationsBundle\Entity\Notifications\Template;
use Products\NotificationsBundle\Entity\NotificationsConfig;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
use Products\NotificationsBundle\Entity\Recipients\EmailRecipient;
use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
use Products\NotificationsBundle\Entity\Student;
use Products\NotificationsBundle\Form\Forms\Templates\TemplateSearchForm;
use Products\NotificationsBundle\Model\Searching\TemplateSearch;
use Products\NotificationsBundle\Util\Preferences;
use Products\NotificationsBundle\Util\Reachability;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
/**
* Class DefaultController
* @package Products\NotificationsBundle\Controller\Dashboard
*/
final class DefaultController extends AbstractDashboardController
{
use PaginationTrait;
const ROUTES__MAIN = 'app.notifications.dashboard.default.main';
const ROUTES__SELECT_MODAL = 'app.notifications.dashboard.default.select_modal';
const ROUTES__BROADCAST_MODAL = 'app.notifications.dashboard.default.broadcast_modal';
const ROUTES__SOCIAL_AVATAR = 'app.notifications.dashboard.default.social_avatar';
/**
* @return AbstractHtmlView
*
* @Route(
* "",
* name = self::ROUTES__MAIN,
* )
*/
public function mainAction(): AbstractHtmlView
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('@app.notifications.access');
// check that we have a storage container for files
// make it if not
// TODO: need to do this somewhere else more reliable...
$storage = $this->getEntityManager()->getRepository(StorageContainer::class)->findOneBy([
'slug' => 'notifications',
]);
if (empty($storage)) {
$storage = (new StorageContainer())
->setName('Notifications')
->setSlug('notifications')
->setHidden(true);
$folder = (new Folder())
->setName('images')
->setContainer($storage);
$this->getEntityManager()->saveAll([
$storage,
$folder,
]);
}
$messages = $this->getEntityManager()->getRepository(Message::class)->findByRecentlySent(6);
return $this->html([
'sync' => $this->getEntityManager()->getRepository(OneRosterSync::class)->findOneByTenant(
$this->getTenant()
),
'messages' => $messages,
'messagesIsGrantedArray' => array_values(
array_filter(
array_map(
function (Message $message) {
try {
$this->audit($message);
return $message->getId();
} catch (\Throwable $exception) {
return null;
}
},
$messages,
)
)
),
'stats' => [
'callouts' => [
'sis' => $this->getEntityManager()->getRepository(Checkup::class)
->count([
'ok' => false,
'fixedAt' => null,
]),
'messages' => $this->getEntityManager()->getRepository(Message::class)
->createQueryBuilder('messages')
->select('COUNT(messages)')
->andWhere('messages.status IN (:statuses)')
->setParameter('statuses', [
AbstractNotification::STATUSES__SENDING,
AbstractNotification::STATUSES__COMPLETE,
])
->andWhere('messages.touchedAt >= :timestamp')
->setParameter('timestamp', new DateTime('-1 month'))
->getQuery()
->getSingleScalarResult(),
'schools' => $this->getEntityManager()->getRepository(School::class)
->count([]),
],
'contacts' => [
'total' => $this->getStudentRepository()
->createQueryBuilder('students')
->select('COUNT(students)')
->getQuery()
->getSingleScalarResult(),
'reachable' => $this->getStudentRepository()
->createQueryBuilder('students')
->select('COUNT(students)')
->andWhere('BIT_AND(students.reachability, :mask) > 0')
->setParameter('mask', Reachability::REACHABLE)
->getQuery()
->getSingleScalarResult(),
'unreachable' => $this->getStudentRepository()
->createQueryBuilder('students')
->select('COUNT(students)')
->andWhere('BIT_AND(students.reachability, :mask) > 0')
->setParameter('mask', Reachability::UNREACHABLE)
->getQuery()
->getSingleScalarResult(),
'no-contact' => $this->getStudentRepository()
->createQueryBuilder('students')
->select('COUNT(students)')
->andWhere('students.reachability = :mask')
->setParameter('mask', Reachability::NO_CONTACT)
->getQuery()
->getSingleScalarResult(),
],
'communication' => [
'total' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->getQuery()
->getSingleScalarResult(),
'email' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(DISTINCT profiles)')
->leftJoin('profiles.contacts', 'xrefs')
->leftJoin(EmailRecipient::class, 'recipients', 'WITH', 'IDENTITY(xrefs.recipient) = recipients.id')
->andWhere('recipients.id IS NOT NULL')
->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
->setParameter('preference', Preferences::PREFERENCES__EMAIL)
->getQuery()
->getSingleScalarResult(),
'sms' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(DISTINCT profiles)')
->leftJoin('profiles.contacts', 'xrefs')
->leftJoin(PhoneRecipient::class, 'recipients', 'WITH', 'IDENTITY(xrefs.recipient) = recipients.id')
->andWhere('recipients.id IS NOT NULL')
->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
->setParameter('preference', Preferences::PREFERENCES__SMS)
->andWhere('recipients.method IN (:methods)')
->setParameter('methods', [
PhoneRecipient::METHODS__SMS,
PhoneRecipient::METHODS__HYBRID,
])
->getQuery()
->getSingleScalarResult(),
'voice' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(DISTINCT profiles)')
->leftJoin('profiles.contacts', 'xrefs')
->leftJoin(PhoneRecipient::class, 'recipients', 'WITH', 'IDENTITY(xrefs.recipient) = recipients.id')
->andWhere('recipients.id IS NOT NULL')
->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
->setParameter('preference', Preferences::PREFERENCES__VOICE)
->andWhere('recipients.method IN (:methods)')
->setParameter('methods', [
PhoneRecipient::METHODS__VOICE,
PhoneRecipient::METHODS__HYBRID,
])
->getQuery()
->getSingleScalarResult(),
'app' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(DISTINCT profiles)')
->leftJoin('profiles.contacts', 'xrefs')
->leftJoin(AppRecipient::class, 'recipients', 'WITH', 'IDENTITY(xrefs.recipient) = recipients.id')
->andWhere('recipients.id IS NOT NULL')
->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
->setParameter('preference', Preferences::PREFERENCES__APP)
->getQuery()
->getSingleScalarResult(),
],
'debugging' => (function () {
$data = [
'all' => [
'total' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->getQuery()
->getSingleScalarResult(),
'reachable' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
->setParameter('mask', Reachability::REACHABLE)
->getQuery()
->getSingleScalarResult(),
'unreachable' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
->setParameter('mask', Reachability::UNREACHABLE)
->getQuery()
->getSingleScalarResult(),
'no-contact' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('profiles.reachability = :mask')
->setParameter('mask', Reachability::NO_CONTACT)
->getQuery()
->getSingleScalarResult(),
],
];
foreach (OneRosterSync::STRATEGIES__NOTIFICATIONS as $key => $strategy) {
if ($this->getOneRosterSync() && $this->getOneRosterSync()->hasStrategy($strategy)) {
$key = str_replace('notifications__', '', $key);
$data[$key] = [
'total' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('profiles.role IN (:roles)')
->setParameter('roles', OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
->getQuery()
->getSingleScalarResult(),
'reachable' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('profiles.role IN (:roles)')
->setParameter('roles', OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
->setParameter('mask', Reachability::REACHABLE)
->getQuery()
->getSingleScalarResult(),
'unreachable' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('profiles.role IN (:roles)')
->setParameter('roles', OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
->setParameter('mask', Reachability::UNREACHABLE)
->getQuery()
->getSingleScalarResult(),
'no-contact' => $this->getProfileRepository()
->createQueryBuilder('profiles')
->select('COUNT(profiles)')
->andWhere('profiles.role IN (:roles)')
->setParameter('roles', OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
->andWhere('profiles.reachability = :mask')
->setParameter('mask', Reachability::NO_CONTACT)
->getQuery()
->getSingleScalarResult(),
];
}
}
return $data;
})(),
]
]);
}
/**
* TODO: this likely should be moved elsewhere as it's not really related to the notifications at all.
*
* @param SocialAccount $account
* @return RedirectResponse
*
* @Route(
* "/_social_avatar/{account}",
* name = self::ROUTES__SOCIAL_AVATAR,
* requirements = {
* "account" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "account",
* class = SocialAccount::class,
* )
*/
public function socialAvatarAction(SocialAccount $account): RedirectResponse
{
switch (true) {
case $account instanceof TwitterSocialAccount:
$url = $this->getTwitterService()->thumb(
$account->getTwitterAccessToken(),
$account->getTwitterTokenSecret(),
);
break;
case $account instanceof FacebookSocialAccount:
$url = $this->getFacebookService()->getPagePicture(
$account->getFacebookAccessToken(),
$account->getFacebookPageId()
);
break;
case $account instanceof InstagramSocialAccount:
$url = $this->getInstagramService()->getProfilePicture(
$account->getInstagramAccessToken(),
$account->getInstagramProfileId()
);
break;
default:
throw new \Exception();
}
if (empty($url)) {
throw new \Exception();
}
return $this->redirect($url);
}
/**
* @return AjaxHtmlView
*
* @Route(
* "/_select_modal",
* name = self::ROUTES__SELECT_MODAL,
* )
*/
public function selectModalAction(): AjaxHtmlView
{
// AUDIT
$this->denyAccessUnlessMaybeGranted([
'app.notifications.messaging.general',
'app.notifications.messaging.urgent',
]);
return $this->ajax();
}
/**
* @param string $type
* @param int $pagination
* @return AjaxHtmlView|Response
*
* @Route(
* "/_broadcast_modal/{type}/{pagination}",
* name = self::ROUTES__BROADCAST_MODAL,
* requirements = {
* "type" = "all|general|urgent",
* "pagination" = "[0-9]+",
* },
* defaults = {
* "type" = "all",
* "pagination" = 0,
* },
* )
*/
public function broadcastModalAction(string $type, int $pagination = 0)
{
// AUDIT
$this->denyAccessUnlessMaybeGranted($type === 'all' ? [
'app.notifications.messaging.general',
'app.notifications.messaging.urgent',
] : [sprintf('app.notifications.messaging.%s', $type)]);
// create a search as it uses some more complex logic depending on type
$search = (new TemplateSearch())
->setDefaults(false);
switch ($type) {
case 'general':
$urgent = false;
$search->setFilter(TemplateSearch::FILTERS__SENDABLE_GENERAL);
break;
case 'urgent':
$urgent = true;
$search->setFilter(TemplateSearch::FILTERS__SENDABLE_URGENT);
break;
default:
$urgent = null;
}
// call common logic
$result = $this->doSearch(
Template::class,
'templates',
$search,
$this->createNamed('broadcast_query', TemplateSearchForm::class, $search)
->remove('filter')
->add('filter', HiddenType::class),
$pagination,
);
// find the default
$default = ($urgent !== null) ? $this->getEntityManager()->getRepository(Template::class)->findSendableDefault($urgent) : null;
return ($result instanceof Response) ? $result : $this->ajax(
array_merge(
$result,
[
'default' => $default,
'type' => ($type !== 'all') ? $type : null,
],
),
);
}
/**
* @return TwitterService|object
*/
private function getTwitterService(): TwitterService
{
return $this->get(__METHOD__);
}
/**
* @return FacebookService|object
*/
private function getFacebookService(): FacebookService
{
return $this->get(__METHOD__);
}
/**
* @return InstagramService|object
*/
private function getInstagramService(): InstagramService
{
return $this->get(__METHOD__);
}
/**
* @return OneRosterSync|null
*/
protected function getOneRosterSync(): ?OneRosterSync
{
return $this->getEntityManager()->getRepository(OneRosterSync::class)->findOneByTenant(
$this->getTenant()
);
}
/**
* @return ProfileRepository
*/
protected function getProfileRepository(): ProfileRepository
{
return $this->getEntityManager()->getRepository(Profile::class);
}
/**
* @return StudentRepository
*/
protected function getStudentRepository(): StudentRepository
{
return $this->getEntityManager()->getRepository(Student::class);
}
}