<?php
namespace Webkul\UVDesk\CoreFrameworkBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\EventDispatcher\GenericEvent;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\Ticket;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\Attachment;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\TicketStatus;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Form\CustomFieldsType;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Entity\CustomFieldsValues;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Entity\CustomFields;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Entity\Type;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Entity\TicketCustomFieldsValues;
use Webkul\UVDesk\CoreFrameworkBundle\Entity\Thread as TicketThread;
use Webkul\UVDesk\CoreFrameworkBundle\Workflow\Events as CoreWorkflowEvents;
use Webkul\UVDesk\CoreFrameworkBundle\Services\UserService;
use Symfony\Contracts\Translation\TranslatorInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Services\UVDeskService;
use Webkul\UVDesk\CoreFrameworkBundle\Services\TicketService;
use Webkul\UVDesk\CoreFrameworkBundle\Services\EmailService;
use Symfony\Component\HttpKernel\KernelInterface;
use Webkul\UVDesk\CoreFrameworkBundle\Services\FileUploadService;
use UVDesk\CommunityPackages\UVDesk\CustomFields\Services\CustomFieldsService;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class Thread extends AbstractController
{
private $userService;
private $translator;
private $eventDispatcher;
private $ticketService;
private $emailService;
private $kernel;
private $fileUploadService;
private $customFieldsService;
public function __construct(UserService $userService, TranslatorInterface $translator, TicketService $ticketService, EmailService $emailService, EventDispatcherInterface $eventDispatcher, KernelInterface $kernel, FileUploadService $fileUploadService, CustomFieldsService $customFieldsService)
{
$this->kernel = $kernel;
$this->userService = $userService;
$this->emailService = $emailService;
$this->translator = $translator;
$this->ticketService = $ticketService;
$this->eventDispatcher = $eventDispatcher;
$this->fileUploadService = $fileUploadService;
$this->customFieldsService = $customFieldsService;
}
public function saveThread($ticketId, Request $request)
{
$params = $request->request->all();
$entityManager = $this->getDoctrine()->getManager();
$ticket = $entityManager->getRepository(Ticket::class)->findOneById($ticketId);
// Proceed only if user has access to the resource
if (false == $this->ticketService->isTicketAccessGranted($ticket)) {
throw new \Exception('Access Denied', 403);
}
if (empty($ticket)) {
throw new \Exception('Ticket not found', 404);
} else if ('POST' !== $request->getMethod()) {
throw new \Exception('Invalid Request', 403);
}
if (empty($params)) {
return $this->redirect($request->headers->get('referer') ?: $this->generateUrl('helpdesk_member_ticket_collection'));
} else if ('note' == $params['threadType'] && false == $this->userService->isAccessAuthorized('ROLE_AGENT_ADD_NOTE')) {
// Insufficient user privilege to create a note
throw new \Exception('Insufficient Permisions', 400);
}
// // Deny access unles granted ticket view permission
// $this->denyAccessUnlessGranted('AGENT_VIEW', $ticket);
// Check if reply content is empty
$parsedMessage = trim(strip_tags($params['reply'], '<img>'));
$parsedMessage = str_replace(' ', '', $parsedMessage);
$parsedMessage = str_replace(' ', '', $parsedMessage);
$timeSpent = trim(strip_tags($params['time_spent']));
$timeSpent = str_replace(' ', '', $timeSpent);
$timeSpent = str_replace(' ', '', $timeSpent);
if (null == $parsedMessage) {
$this->addFlash('warning', $this->translator->trans('Reply content cannot be left blank.'));
}
// @TODO: Validate file attachments
// if (true !== $this->get('file.service')->validateAttachments($request->files->get('attachments'))) {
// $this->addFlash('warning', "Invalid attachments.");
// }
$sanitizedReply = htmlspecialchars($params['reply']);
// Initialize the parsed message with the sanitized reply followed by two <br> tags
$parsedMessage = $sanitizedReply . '<br><br>';
// // Check if customFields is present in the params array and is an array
// if (isset($params['customFields']) && is_array($params['customFields'])) {
// // Iterate through each custom field
// foreach ($params['customFields'] as $key => $value) {
// // Only process non-empty custom fields
// if (!empty($value)) {
// // Sanitize the custom field value
// $sanitizedValue = htmlspecialchars(trim(strip_tags($value, '<img>')));
// // Append the sanitized custom field value to the parsed message with a new line
// $parsedMessage .= $sanitizedValue . '<br>';
// }
// }
// }
// $adminReply = str_replace(['<p>','</p>'],"",$params['reply']);
$typevalue = htmlspecialchars($params['typeValue']);
$threadDetails = [
'user' => $this->getUser(),
'createdBy' => 'agent',
'source' => 'website',
'threadType' => strtolower($params['threadType']),
'message' => str_replace(['<script>', '</script>'], '', $parsedMessage),
'attachments' => $request->files->get('attachments'),
'timeSpent' => $timeSpent,
];
if(!empty($params['status'])){
$ticketStatus = $entityManager->getRepository(TicketStatus::class)->findOneByCode($params['status']);
$ticket->setStatus($ticketStatus);
}
if (isset($params['to'])) {
$threadDetails['to'] = $params['to'];
}
if (isset($params['cc'])) {
$threadDetails['cc'] = $params['cc'];
}
if (isset($params['cccol'])) {
$threadDetails['cccol'] = $params['cccol'];
}
if (isset($params['typeValue'])) {
$threadDetails['typeId'] = $params['typeValue'];
}
if (isset($params['bcc'])) {
$threadDetails['bcc'] = $params['bcc'];
}
// Create Thread
$thread = $this->ticketService->createThread($ticket, $threadDetails);
// $this->addFlash('success', ucwords($params['threadType']) . " added successfully.");
// @TODO: Remove Agent Draft Thread
// @TODO: Trigger Thread Created Event
// @TODO: Cross Review
// check for thread types
switch ($thread->getThreadType()) {
case 'note':
$event = new GenericEvent(CoreWorkflowEvents\Ticket\Note::getId(), [
'entity' => $ticket,
'thread' => $thread
]);
$this->eventDispatcher->dispatch($event, 'uvdesk.automation.workflow.execute');
// @TODO: Render response on the basis of event response (if propogation was stopped or not)
$this->addFlash('success', $this->translator->trans('Note added to ticket successfully.'));
break;
case 'reply':
// ✅ Step 1: Save the thread first
//$thread = $this->ticketService->createThread($ticket, $threadDetails);
//$entityManager->persist($thread);
//$entityManager->flush(); // ✅ Ensure thread_id is saved before proceeding
// ✅ Step 2: Now save custom field values
if (!empty($params['customFields'])) {
foreach ($params['customFields'] as $customFieldId => $customFieldValue) {
$customField = $entityManager->getRepository(CustomFields::class)->findOneById($customFieldId);
if (!$customField) {
continue; // Skip invalid fields
}
// Check if the value already exists for this thread and custom field
$ticketCustomFieldValue = $entityManager->getRepository(TicketCustomFieldsValues::class)
->findOneBy(['ticket' => $ticket, 'ThreadId' => $thread->getId(), 'ticketCustomFieldsValues' => $customFieldId]);
if (!$ticketCustomFieldValue) {
$ticketCustomFieldValue = new TicketCustomFieldsValues();
$ticketCustomFieldValue->setTicket($ticket);
$ticketCustomFieldValue->setThreadId($thread->getId());
$ticketCustomFieldValue->setTicketCustomFieldsValues($customField);
}
// Handle multiple values (checkbox, multi-select)
if (is_array($customFieldValue)) {
$ticketCustomFieldValue->setValue(json_encode($customFieldValue));
} else {
$ticketCustomFieldValue->setValue($customFieldValue);
}
$entityManager->persist($ticketCustomFieldValue);
}
}
// ✅ Step 3: Flush to save custom fields before workflow execution
$entityManager->flush();
// ✅ Step 4: Fetch updated custom fields after saving
$ticketCustomFieldsValues = $entityManager->getRepository(TicketCustomFieldsValues::class)->findBy(['ticket' => $ticket]);
$customFieldsArray = [];
foreach ($ticketCustomFieldsValues as $fieldValue) {
$customFieldsArray[$fieldValue->getTicketCustomFieldsValues()->getId()] = $fieldValue->getValue();
}
// ✅ Step 5: Now dispatch workflow event (after saving thread & custom fields)
$event = new GenericEvent(CoreWorkflowEvents\Ticket\AgentReply::getId(), [
'entity' => $ticket,
'thread' => $thread,
'customfields' => $customFieldsArray
]);
$this->eventDispatcher->dispatch($event, 'uvdesk.automation.workflow.execute');
$this->addFlash('success', $this->translator->trans('Success! Reply added successfully.'));
break;
case 'forward':
// Prepare headers
$headers = ['References' => $ticket->getReferenceIds()];
if (null != $ticket->currentThread->getMessageId()) {
$headers['In-Reply-To'] = $ticket->currentThread->getMessageId();
}
// Prepare attachments
$attachments = $entityManager->getRepository(Attachment::class)->findByThread($thread);
$projectDir = $this->kernel->getProjectDir();
$attachments = array_map(function($attachment) use ($projectDir) {
return str_replace('//', '/', $projectDir . "/public" . $attachment->getPath());
}, $attachments);
$message = '<html><body style="background-image: none"><p>'.html_entity_decode($thread->getMessage()).'</p></body></html>';
// Forward thread to users
try {
$messageId = $this->emailService->sendMail($params['subject'] ?? ("Forward: " . $ticket->getSubject()), $message, $thread->getReplyTo(), $headers, $ticket->getMailboxEmail(), $attachments ?? [], $thread->getCc() ?: [], $thread->getBcc() ?: []);
if (!empty($messageId)) {
$thread->setMessageId($messageId);
$entityManager->persist($createdThread);
$entityManager->flush();
}
} catch (\Exception $e) {
// Do nothing ...
// @TODO: Log exception
}
// @TODO: Render response on the basis of event response (if propogation was stopped or not)
$this->addFlash('success', $this->translator->trans('Reply added to the ticket and forwarded successfully.'));
break;
default:
break;
}
$params['threadId'] = $thread->getId();
$ThreadId = $params['threadId'];
// Check if ticket status needs to be updated
$updateTicketToStatus = !empty($params['status']) ? (trim($params['status']) ?: null) : null;
if (!empty($updateTicketToStatus) && $this->userService->isAccessAuthorized('ROLE_AGENT_UPDATE_TICKET_STATUS')) {
$ticketStatus = $entityManager->getRepository(TicketStatus::class)->findOneById($updateTicketToStatus);
if (!empty($ticketStatus) && $ticketStatus->getId() === $ticket->getStatus()->getId()) {
$ticket->setStatus($ticketStatus);
$entityManager->persist($ticket);
$entityManager->flush();
// @TODO: Trigger Ticket Status Updated Event
}
}
if (!empty($ticket)) {
$submittedCustomFields = $request->request->get('customFields');
$submittedCustomFileFields = $request->files->get('customFields');
$validationStatus = $this->customFieldsService->customFieldsValidationWithoutRequired($request, $request->request->get('area') ?: 'user');
if (!empty($validationStatus) && $validationStatus['errorMain'] == false && empty($validationStatus['formErrors'])) {
$customFieldRepository = $entityManager->getRepository(CustomFields::class);
$ticketCustomFieldsValuesCollection = $entityManager->getRepository(TicketCustomFieldsValues::class)->find($ticket);
if (!empty($submittedCustomFields)) {
foreach ($submittedCustomFields as $customFieldId => $customFieldValue) {
$existingCustomFieldValue = null;
// 1) Check existing records for this field
if (!empty($ticketCustomFieldsValuesCollection)) {
foreach ($ticketCustomFieldsValuesCollection as $ticketCustomField) {
if ($ticketCustomField->getTicketCustomFieldsValues()->getId() == $customFieldId) {
$existingCustomFieldValue = $ticketCustomField;
break;
}
}
}
// 2) Fetch the definition of this custom field
$customField = $customFieldRepository->findOneById($customFieldId);
// 3) Reuse or create a new TicketCustomFieldsValues entity
$ticketCustomFieldValue = $existingCustomFieldValue ?: new TicketCustomFieldsValues();
// 4) Associate this value with the correct Ticket + Field
$ticketCustomFieldValue->setTicket($ticket);
$ticketCustomFieldValue->setTicketCustomFieldsValues($customField);
// 5) Handle possible multiple checkbox selections
if (is_array($customFieldValue)) {
// Option A: Convert array to JSON
$ticketCustomFieldValue->setValue(json_encode($customFieldValue));
// or Option B: Convert to comma‐separated string
// $ticketCustomFieldValue->setValue(implode(',', $customFieldValue));
} else {
// It's a single value (string)
$ticketCustomFieldValue->setValue($customFieldValue);
}
// 6) Set thread ID if needed
$ticketCustomFieldValue->setThreadId($thread->getId());
// 7) Persist changes
$entityManager->persist($ticketCustomFieldValue);
$entityManager->persist($ticket);
}
}
if (!empty($submittedCustomFileFields)) {
$baseUploadPath = '/custom-fields/ticket/' . $ticket->getId() . '/';
$temporaryFiles = $request->files->get('customFields');
if (!empty($temporaryFiles)) {
foreach ($temporaryFiles as $key => $temporaryFile) {
// ✅ Ensure $temporaryFile is actually uploaded before processing
if ($temporaryFile instanceof UploadedFile) {
$fileName = $this->fileUploadService->uploadFile($temporaryFile, $baseUploadPath, true);
$fileName['key'] = $key;
$uploadedFileCollection[] = $fileName;
} else {
error_log("File not uploaded for key: " . $key);
}
}
} else {
error_log("No file uploaded in this workflow step.");
}
if (!empty($uploadedFileCollection)) {
foreach ($uploadedFileCollection as $uploadedFile) {
$existingCustomFieldValue = null;
if (!empty($ticketCustomFieldsValuesCollection)) {
foreach ($ticketCustomFieldsValuesCollection as $ticketCustomField) {
if ($ticketCustomField->getTicketCustomFieldsValues()->getId() == $uploadedFile['key']) {
$existingCustomFieldValue = $ticketCustomField;
break;
}
}
}
if(!empty($attachment)) {
$thread = $attachment->getThread();
}
$thread = ($thread == null ? $ticket->getThreads()->get(0) : $thread);
$uploadedAttachment = $this->customFieldsService->addFilesEntryToAttachmentTable([$uploadedFile], $thread);
if (!empty($uploadedAttachment[0])) {
$customField = $customFieldRepository->findOneById($uploadedFile['key']);
$ticketCustomFieldValue = !empty($existingCustomFieldValue) ? $existingCustomFieldValue : new TicketCustomFieldsValues();
// $ticketCustomFieldValue->setValue($resourceURL);
$ticketCustomFieldValue->setValue(json_encode(['name' => $uploadedAttachment[0]['name'], 'path' => $uploadedAttachment[0]['path'], 'id' => $uploadedAttachment[0]['id']]));
$ticketCustomFieldValue->setTicketCustomFieldsValues($customField);
$ticketCustomFieldValue->setTicket($ticket);
$entityManager->persist($ticketCustomFieldValue);
$entityManager->persist($ticket);
}
}
}
}
//$entityManager->flush();
$ticketCustomFieldsValuesCollection = $entityManager->getRepository(TicketCustomFieldsValues::class)->findBy(['ticket' => $ticket]);
if (!empty($ticketCustomFieldsValuesCollection)) {
$ticketCustomFieldArrayCollection = [];
foreach ($ticketCustomFieldsValuesCollection as $ticketCustomField) {
$ticketCustomFieldArrayCollection[$ticketCustomField->getTicketCustomFieldsValues()->getId()] = [
'id' => $ticketCustomField->getId(),
'encrypted' => $ticketCustomField->getEncrypted() ? true : false,
'targetCustomField' => $ticketCustomField->getTicketCustomFieldsValues()->getId(),
];
switch ($ticketCustomField->getTicketCustomFieldsValues()->getFieldType()) {
case 'select':
case 'radio':
case 'checkbox':
$fieldId = [];
$fieldValue = [];
if ($ticketCustomField->getEncrypted()) {
$ticketCustomField->decryptEntity();
}
$fieldOptions = json_decode($ticketCustomField->getValue(), true);
if (empty($fieldOptions)) {
$fieldOptions = explode(',', $ticketCustomField->getValue());
} else {
if (!is_array($fieldOptions)) {
$fieldOptions = [$fieldOptions];
}
}
foreach ($ticketCustomField->getTicketCustomFieldsValues()->getCustomFieldValues() as $multipleFieldValue) {
if (in_array($multipleFieldValue->getId(), $fieldOptions)) {
$fieldId[] = $multipleFieldValue->getId();
$fieldValue[] = $multipleFieldValue->getName();
}
}
$ticketCustomFieldArrayCollection[$ticketCustomField->getTicketCustomFieldsValues()->getId()]['valueId'] = $fieldId;
$ticketCustomFieldArrayCollection[$ticketCustomField->getTicketCustomFieldsValues()->getId()]['value'] = $ticketCustomField->getEncrypted() ? null: implode('</br>', $fieldValue);
break;
default:
$ticketCustomFieldArrayCollection[$ticketCustomField->getTicketCustomFieldsValues()->getId()]['value'] = (!$ticketCustomField->getEncrypted()
? (is_array(trim($ticketCustomField->getValue(), '"'))
? json_encode(trim($ticketCustomField->getValue(), '"'))
: strip_tags(htmlentities(trim($ticketCustomField->getValue(), '"')))
)
: null
);
break;
}
}
$responseContent = [
'success' => true,
'ticketCustomFieldsValuesCollection' => $ticketCustomFieldArrayCollection,
];
} else {
$responseContent = [
'success' => true,
'ticketCustomFieldsValuesCollection' => [],
];
}
} else {
$responseContent = [
'success' => false,
'message' => $validationStatus['formErrors'],
'ticketCustomFieldsValuesCollection' => [],
];
}
}
// Redirect to either Ticket View | Ticket Listings
if ('redirect' === $params['nextView']) {
return $this->redirect($this->generateUrl('helpdesk_member_ticket_collection'));
}
if (!empty($params['status']) && !empty($params['threadId'])) {
// Fetch the correct thread by ID
$thread = $entityManager->getRepository(TicketThread::class)->find($params['threadId']);
if ($thread) {
// Update only the specific thread's status_update
$thread->setStatusUpdate($params['status']);
$entityManager->persist($thread);
$entityManager->flush();
} else {
// Optional: Handle the case where the thread ID does not exist
throw new \Exception("Thread ID " . $params['threadId'] . " not found.");
}
}
return $this->redirect($this->generateUrl('helpdesk_member_ticket', ['ticketId' => $ticket->getId()]));
}
public function updateThreadXHR(Request $request)
{
$json = [];
$em = $this->getDoctrine()->getManager();
$content = json_decode($request->getContent(), true);
$thread = $em->getRepository(TicketThread::class)->find($request->attributes->get('threadId'));
$ticket = $thread->getTicket();
$user = $this->userService->getSessionUser();
// Proceed only if user has access to the resource
if ( (!$this->userService->getSessionUser()) || (false == $this->ticketService->isTicketAccessGranted($ticket, $user)) )
{
throw new \Exception('Access Denied', 403);
}
// Assuming 'thread_id' and 'customfields' are part of the $content array
$threadId = $content['id'];
$customFields = $content['customfields'];
foreach ($customFields as $customField) {
if (isset($customField['custom_field_id']) && isset($customField['value'])) {
$customFieldId = $customField['custom_field_id'];
$value = $customField['value'];
// Convert array to JSON string if it's an array
if (is_array($value)) {
$customField['value'] = json_encode($value);
$value = $customField['value'];
}
// Find the existing custom field value record
$customFieldValue = $em->getRepository(TicketCustomFieldsValues::class)
->findOneBy([
'ThreadId' => $threadId,
'ticketCustomFieldsValues' => $customFieldId
]);
// If the record exists, update the value
if ($customFieldValue) {
$customFieldValue->setValue($value);
$em->persist($customFieldValue);
$em->flush();
}
}
}
// // Flush changes to the database
// $this->$em->flush();
if ($request->getMethod() == "PUT") {
// $this->isAuthorized('ROLE_AGENT_EDIT_THREAD_NOTE');
if (str_replace(' ','',str_replace(' ','',trim(strip_tags($content['reply'], '<img>')))) != "") {
$thread->setMessage($content['reply']);
$em->persist($thread);
$em->flush();
$ticket->currentThread = $thread;
// Trigger agent reply event
$event = new GenericEvent(CoreWorkflowEvents\Ticket\ThreadUpdate::getId(), [
'entity' => $ticket,
]);
$this->eventDispatcher->dispatch($event, 'uvdesk.automation.workflow.execute');
$json['alertMessage'] = $this->translator->trans('Success ! Thread updated successfully.');
$json['alertClass'] = 'success';
$json['reload'] = true; // Add this flag to indicate a page reload
} else {
$json['alertMessage'] = $this->translator->trans('Error ! Reply field can not be blank.');
$json['alertClass'] = 'error';
}
}
return new Response(json_encode($json), 200, ['Content-Type' => 'application/json']);
}
public function threadXHR(Request $request)
{
$json = array();
$content = json_decode($request->getContent(), true);
$em = $this->getDoctrine()->getManager();
$ticket = $em->getRepository(Ticket::class)->findOneById($content['ticketId']);
// Proceed only if user has access to the resource
if (false == $this->ticketService->isTicketAccessGranted($ticket)){
throw new \Exception('Access Denied', 403);
}
$threadId = $request->attributes->get('threadId');
if ($request->getMethod() == "DELETE") {
$thread = $em->getRepository(TicketThread::class)->findOneBy(array('id' => $threadId, 'ticket' => $content['ticketId']));
$projectDir = $this->kernel->getProjectDir();
if ($thread) {
$this->fileUploadService->fileRemoveFromFolder($projectDir."/public/assets/threads/".$threadId);
// Trigger thread deleted event
// $event = new GenericEvent(CoreWorkflowEvents\Ticket\ThreadUpdate::getId(), [
// 'entity' => $ticket,
// ]);
// $this->eventDispatcher->dispatch('uvdesk.automation.workflow.execute', $event);
$em->remove($thread);
$em->flush();
$json['alertClass'] = 'success';
$json['alertMessage'] = $this->translator->trans('Success ! Thread removed successfully.');
} else {
$json['alertClass'] = 'danger';
$json['alertMessage'] = $this->translator->trans('Error ! Invalid thread.');
}
} elseif ($request->getMethod() == "PATCH") {
$thread = $em->getRepository(TicketThread::class)->findOneBy(array('id' => $request->attributes->get('threadId'), 'ticket' => $content['ticketId']));
if ($thread) {
if ($content['updateType'] == 'lock') {
$thread->setIsLocked($content['isLocked']);
$em->persist($thread);
$em->flush();
$json['alertMessage'] = $this->translator->trans($content['isLocked'] ? 'Success ! Thread locked successfully.' : 'Success ! Thread unlocked successfully.');
$json['alertClass'] = 'success';
} elseif ($content['updateType'] == 'bookmark') {
$thread->setIsBookmarked($content['bookmark']);
$em->persist($thread);
$em->flush();
$json['alertMessage'] = $this->translator->trans($content['bookmark'] ? 'Success ! Thread pinned successfully.' : 'Success ! unpinned removed successfully.');
$json['alertClass'] = 'success';
}
} else {
$json['alertClass'] = 'danger';
$json['alertMessage'] = $this->translator->trans('Error ! Invalid thread.');
}
}
return new Response(json_encode($json), 200, ['Content-Type' => 'application/json']);
}
}