src/Model/EwsNotificationModel.php line 4612

Open in your IDE?
  1. <?php
  2. namespace App\Model;
  3. use DateTime;
  4. use Pimcore\Db;
  5. use DOMDocument;
  6. use Pimcore\Model\DataObject\AlertAction;
  7. use Pimcore\Model\DataObject\AlertHazard;
  8. use Pimcore\Model\DataObject\AlertStatus;
  9. use Pimcore\Model\DataObject\Governorate;
  10. use Pimcore\Model\DataObject\EwsPolygon;
  11. use SimpleXMLElement;
  12. use Pimcore\Model\Asset;
  13. use Doctrine\DBAL\Connection;
  14. use Pimcore\Model\DataObject;
  15. use Symfony\Component\Uid\UuidV4;
  16. use Pimcore\Model\DataObject\Region;
  17. use Pimcore\Model\DataObject\AlertType;
  18. use Pimcore\Model\DataObject\EwsNotification;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Pimcore\Model\DataObject\FetchSentEwsEmail;
  21. use App\C2IntegrationBundle\Service\C2Service;
  22. use Pimcore\Model\Asset\MetaData\ClassDefinition\Data\DataObject as DataDataObject;
  23. use Pimcore\Model\DataObject\EwsAndReportUserGroup;
  24. use App\Model\UserModel;
  25. use App\Service\Translation\Bilingual;
  26. use Symfony\Component\Process\Process;
  27. use Symfony\Component\Process\Exception\ProcessFailedException;
  28. use App\Lib\ConsoleBackgroundProcess;
  29. use App\Lib\PublicPortalEwsWebhookProcessLauncher;
  30. use Psr\Log\LoggerInterface;
  31. use Knp\Snappy\Image as SnappyImage;
  32. class EwsNotificationModel
  33. {
  34.     /** Pimcore class definition maxItems for center / district many-to-many fields */
  35.     private const MAX_EWS_CENTER_DISTRICT_RELATIONS 1000;
  36.     public $c2Service;
  37.     private $userModel;
  38.     function __construct()
  39.     {
  40.         $this->c2Service = new C2Service();
  41.         $this->userModel = new UserModel();
  42.     }
  43.     /**
  44.      * Deduplicate by object id, preserve request order, enforce Pimcore maxItems on relation fields.
  45.      *
  46.      * @param array<int, DataObject\AbstractObject|null|false> $objects
  47.      * @return DataObject\AbstractObject[]
  48.      */
  49.     private function capUniqueDataObjectRelations(array $objectsint $max self::MAX_EWS_CENTER_DISTRICT_RELATIONS): array
  50.     {
  51.         $out = [];
  52.         $seen = [];
  53.         foreach ($objects as $obj) {
  54.             if (!$obj instanceof DataObject\AbstractObject) {
  55.                 continue;
  56.             }
  57.             $oid $obj->getId();
  58.             if (!$oid || isset($seen[$oid])) {
  59.                 continue;
  60.             }
  61.             $seen[$oid] = true;
  62.             $out[] = $obj;
  63.             if (count($out) >= $max) {
  64.                 break;
  65.             }
  66.         }
  67.         return $out;
  68.     }
  69.     /**
  70.      * Coerce a request ID (scalar or single-element array) to a value Pimcore getBy* can quote.
  71.      */
  72.     private function scalarId($value)
  73.     {
  74.         if (is_array($value)) {
  75.             $value reset($value);
  76.         }
  77.         if ($value === false || $value === null || $value === '') {
  78.             return null;
  79.         }
  80.         return $value;
  81.     }
  82.     /**
  83.      * Split a comma-separated ID string or array, skipping empty values, preserving request order.
  84.      *
  85.      * @return string[]
  86.      */
  87.     private function parseCommaSeparatedIds($value): array
  88.     {
  89.         if ($value === null || $value === '' || $value === false) {
  90.             return [];
  91.         }
  92.         if (is_array($value)) {
  93.             $ids = [];
  94.             foreach ($value as $id) {
  95.                 if (is_array($id)) {
  96.                     $id reset($id);
  97.                 }
  98.                 if ($id !== null && $id !== false && $id !== '') {
  99.                     $ids[] = $id;
  100.                 }
  101.             }
  102.             return $ids;
  103.         }
  104.         $ids = [];
  105.         foreach (explode(',', (string) $value) as $id) {
  106.             if ($id !== '') {
  107.                 $ids[] = $id;
  108.             }
  109.         }
  110.         return $ids;
  111.     }
  112.     public function createNotification($request$params$userInterface$translator): array
  113.     {
  114.         $result = [];
  115.         // try {
  116.         $regionId $params['regionId'] ?? null;
  117.         $polygonId $params['polygonId'] ?? null;
  118.         $governorateIds $params['governateIds'] ?? null;
  119.         $mapGovernorateIds $params['mapGovernorateIds'] ?? null;
  120.         $municipalities $params['municipalityIds'] ?? null;
  121.         $centerIds $params['centerIds'] ?? null;
  122.         $districtIds $params['districtIds'] ?? null;
  123.         $ewsOtherLocationIds $params['ewsOtherLocationIds'] ?? null;
  124.         $alertActionId $params['alertActionId'] ?? null;
  125.         $alertTypeId $params['alertTypeId'] ?? null;
  126.         $weatherPhenomenonId $params['weatherPhenomenonId'] ?? null;
  127.         $coordinates json_encode($params['coordinates'] ?? []);
  128.         $message $params['message'] ?? [];
  129.         $alertHazardId $params['alertHazardId'] ?? null;
  130.         $otherLocationId $params['otherLocationId'] ?? null;
  131.         $enableTwitterNotification $params['enableTwitterNotification'] ?? false;
  132.         $enableSMSNotification $params['enableSMSNotification'] ?? false;
  133.         $enableEmailNotification $params['enableEmailNotification'] ?? false;
  134.         $x_post $params['x_post'] ?? "";
  135.         $previewText = isset($params['previewText']) ? $params['previewText'] : false;
  136.         $notificationType =  $params['notificationType'] ?? null;
  137.         $alertPage $params['alertPage'] ?? null;
  138.         $ewsNotification = new DataObject\EwsNotification();
  139.         $ewsNotification->setParent(DataObject\Service::createFolderByPath('/Notification/EWSNotification'));
  140.         $ewsNotification->setKey(md5($coordinates $regionId $governorateIds) . '-' time());
  141.         $ewsNotification->setGuid(\App\Lib\Utility::getUUID()->toRfc4122());
  142.         if (isset($params['startDate'])) {
  143.             $startTime $params['startTime'] ?? '00:00:00';
  144.             $fromDate $params['startDate'] . ' ' $startTime;
  145.             $ewsNotification->setStartDate(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$fromDate));
  146.         }
  147.         if (isset($params['endDate'])) {
  148.             $endTime $params['endTime'] ?? '00:00:00';
  149.             $endDate $params['endDate'] . ' ' $endTime;
  150.             $ewsNotification->setEndDate(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$endDate));
  151.         }
  152.         if (isset($message['en'])) {
  153.             $ewsNotification->setMessage($message['en'], 'en');
  154.         }
  155.         if (isset($message['ar'])) {
  156.             $ewsNotification->setMessage($message['ar'], 'ar');
  157.         }
  158.         if (!empty($regionId)) {
  159.             $regionObj DataObject\Region::getByRegionId($regionId1);
  160.             if (!empty($regionObj)) {
  161.                 $ewsNotification->setRegion($regionObj);
  162.                 if ($governorateIds) {
  163.                     $governorateIdArr explode(","$governorateIds);
  164.                     if ($governorateIdArr) {
  165.                         $applyGovernates = [];
  166.                         foreach ($governorateIdArr as $id) {
  167.                             if (!empty($id)) {
  168.                                 $applyGovernates[] = \Pimcore\Model\DataObject\Governorate::getByGovernoteId($idtrue);
  169.                                 if (!empty(array_unique($applyGovernates))) {
  170.                                     $ewsNotification->setGovernorate(array_unique($applyGovernates));
  171.                                 }
  172.                             }
  173.                         }
  174.                     }
  175.                     if ($municipalities) {
  176.                         $municipalitiesIdArr explode(","$municipalities);
  177.                         if ($municipalitiesIdArr) {
  178.                             $applyMunicipalities = [];
  179.                             foreach ($municipalitiesIdArr as $id) {
  180.                                 $applyMunicipalities[] = \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($idtrue);
  181.                                 if (!empty(array_unique($applyMunicipalities))) {
  182.                                     $ewsNotification->setMunicipality(array_unique($applyMunicipalities));
  183.                                 }
  184.                             }
  185.                         }
  186.                     }
  187.                 } else {
  188.                     $ewsNotification->setGovernorate($this->governorateByRegion($regionObj->getId()));
  189.                 }
  190.             }
  191.         }
  192.         if(!empty($mapGovernorateIds)) {
  193.             $mapGovernorateIdArr explode(","$mapGovernorateIds);
  194.             if ($mapGovernorateIdArr) {
  195.                 $applyMapGovernates = [];
  196.                 foreach ($mapGovernorateIdArr as $id) {
  197.                     if (!empty($id)) {
  198.                         $applyMapGovernates[] = \Pimcore\Model\DataObject\Governorate::getByGovernoteId($idtrue);
  199.                         if (!empty(array_unique($applyMapGovernates))) {
  200.                             $ewsNotification->setMapGovernorate(array_unique($applyMapGovernates));
  201.                         }
  202.                     }
  203.                 }
  204.             }
  205.         }
  206.         if ($ewsOtherLocationIds) {
  207.             $ewsOtherLocationIdArr explode(","$ewsOtherLocationIds);
  208.             if ($ewsOtherLocationIdArr) {
  209.                 $applyOtherGovernates = [];
  210.                 foreach ($ewsOtherLocationIdArr as $id) {
  211.                     $applyOtherGovernates[] = \Pimcore\Model\DataObject\EwsOtherLocation::getByGovernoteId($idtrue);
  212.                     if (!empty(array_unique($applyOtherGovernates))) {
  213.                         $ewsNotification->setEwsOtherLocations(array_unique($applyOtherGovernates));
  214.                     }
  215.                 }
  216.             }
  217.         }
  218.         if (!empty($alertHazardId)) {
  219.             $alertHazardArr explode(","$alertHazardId);
  220.             if ($alertHazardArr) {
  221.                 $alertHazardArrRequest = [];
  222.                 foreach ($alertHazardArr as $alertHazardId) {
  223.                     $weatherPhenomenonAffect DataObject\AlertHazard::getByAlertHazardId($alertHazardId1);
  224.                     if ($weatherPhenomenonAffect) {
  225.                         $alertHazardArrRequest[] = $weatherPhenomenonAffect;
  226.                     }
  227.                 }
  228.                 $ewsNotification->setAlertHazard($alertHazardArrRequest);
  229.             }
  230.         }
  231.         if (!empty($centerIds)) {
  232.             $applyCenters = [];
  233.             foreach (array_filter(array_map('trim'explode(',', (string) $centerIds))) as $id) {
  234.                 $center DataObject\Centers::getById((int) $id);
  235.                 if ($center instanceof DataObject\Centers) {
  236.                     $applyCenters[] = $center;
  237.                 }
  238.             }
  239.             $applyCenters $this->capUniqueDataObjectRelations($applyCenters);
  240.             if ($applyCenters !== []) {
  241.                 $ewsNotification->setCenter($applyCenters);
  242.             }
  243.         }
  244.         if (!empty($districtIds)) {
  245.             $applyDistrict = [];
  246.             foreach (array_filter(array_map('trim'explode(',', (string) $districtIds))) as $id) {
  247.                 $district DataObject\District::getById((int) $id);
  248.                 if ($district instanceof DataObject\District) {
  249.                     $applyDistrict[] = $district;
  250.                 }
  251.             }
  252.             $applyDistrict $this->capUniqueDataObjectRelations($applyDistrict);
  253.             if ($applyDistrict !== []) {
  254.                 $ewsNotification->setDistrict($applyDistrict);
  255.             }
  256.         }
  257.         if (!empty($alertTypeId)) {
  258.             $alertType DataObject\AlertType::getByAlertTypeId($alertTypeId1);
  259.             $ewsNotification->setAlertType($alertType);
  260.         }
  261.         if (!empty($alertActionId)) {
  262.             $alertActionArr explode(","$alertActionId);
  263.             if ($alertActionArr) {
  264.                 $alertActionArrRequest = [];
  265.                 foreach ($alertActionArr as $alertActionId) {
  266.                     $alertAction DataObject\AlertAction::getByAlertActionId($alertActionId1);
  267.                     if ($alertAction) {
  268.                         $alertActionArrRequest[] = $alertAction;
  269.                     }
  270.                 }
  271.                 $ewsNotification->setAlertAction($alertActionArrRequest);
  272.             }
  273.         }
  274.         if (!empty($otherLocationId)) {
  275.             $otherLocationArr explode(","$otherLocationId);
  276.             if ($otherLocationArr) {
  277.                 $otherLocationArrRequest = [];
  278.                 foreach ($otherLocationArr as $otherLocationId) {
  279.                     $otherLocation DataObject\OtherLocation::getByOtherLocationId($otherLocationId1);
  280.                     if ($otherLocation) {
  281.                         $otherLocationArrRequest[] = $otherLocation;
  282.                     }
  283.                 }
  284.                 $ewsNotification->setOtherLocations($otherLocationArrRequest);
  285.             }
  286.         }
  287.         if (!empty($weatherPhenomenonId)) {
  288.             $alertStatus DataObject\AlertStatus::getByAlertStatusId($weatherPhenomenonIdtrue);
  289.             $ewsNotification->setAlertStatus($alertStatus);
  290.         }
  291.         if (!empty($polygonId)) {
  292.             $ewsPolygon EwsPolygon::getById($polygonId ?? null);
  293.             $ewsNotification->setPolygon($ewsPolygon);
  294.             $ewsNotification->setIsPolygon(true);
  295.         }
  296.         $ewsNotification->setCoordinates($coordinates);
  297.         $addressComponents $params['address_components'] ?? [];
  298.         $this->addAddressComponentsFieldCollection($addressComponents$ewsNotification);
  299.         if (!empty($params['file'])) {
  300.             $asset $this->createAsset($params['file'], time() . $params['filename']);
  301.             if ($asset) {
  302.                 $ewsNotification->setAttachment($asset);
  303.             }
  304.         }
  305.         if (!empty($params['x_img'])) {
  306.             $asset $this->createAsset($params['x_img'], uniqid() . "EWSTwitterImages.png");
  307.             if ($asset) {
  308.                 $ewsNotification->setXAttachment($asset);
  309.             }
  310.         }
  311.         if (!empty($params['polygon_x_img'])) {
  312.             $asset $this->createAsset($params['polygon_x_img'], uniqid() . "EWSPolygonTwitterImages.png");
  313.             if ($asset) {
  314.                 $ewsNotification->setPolygonXAtachment($asset);
  315.             }
  316.         }
  317.         $ewsNotification->setXPost($x_post);
  318.         $ewsNotification->setEnableTwitterNotification($enableTwitterNotification);
  319.         $ewsNotification->setEnableSMSNotification($enableSMSNotification);
  320.         $ewsNotification->setEnableEmailNotification($enableEmailNotification);
  321.         $ewsNotification->setUser($userInterface);
  322.         $ewsNotification->setEditor($userInterface);
  323.         $ewsNotification->setPublished(false);
  324.         $ewsNotification->setPreviewText($previewText);
  325.         $ewsNotification->setNotificationType($notificationType);
  326.         if (isset($params['alertPage'])) {
  327.             $ewsNotification->setAlertPage($alertPage);
  328.         }
  329.         //$ewsNotification->save(["versionNote" => "Update"]);
  330.         //set ews search Id
  331.         $currentDate = new \DateTime();
  332.         $formattedDate $currentDate->format('dmY') . '-' $ewsNotification->getId();
  333.         $searchIdEn 'Early Warning System | ' $formattedDate ' | ' ucfirst($ewsNotification->getAlertType()?->getColor()) . ' Alert | ' $ewsNotification->getWeatherPhenomenon()?->getTitle("en");
  334.         $searchIdAr $translator->trans('Early Warning System', [], null"ar") . ' | ' $formattedDate ' | ' $translator->trans(ucfirst($ewsNotification->getAlertType()?->getColor()) . ' Alert', [], null"ar") . ' | ' $ewsNotification->getWeatherPhenomenon()?->getTitle("ar");
  335.         $ewsNotification->setEwsSearchId($searchIdEn"en");
  336.         $ewsNotification->setEwsSearchId($searchIdAr"ar");
  337.         //$ewsNotification->save(["versionNote" => "Update"]);
  338.         $ewsNotification->save();
  339.         $this->saveUserMessageSuggestions(
  340.             (int) $userInterface->getId(),
  341.             $message
  342.         );
  343.         return ['success' => true'message' => $translator->trans('ews_notification_added_successifully'), "notification_id" => $ewsNotification->getId()];
  344.         // } catch (\Exception $ex) {
  345.         //     $result = ['success' => false, 'message' => $ex->getMessage()];
  346.         // }
  347.         return $result;
  348.     }
  349.     public function updateNotification($request$params$userInterface$translator$userGroupIds$isPublished$emailService$templating$logger): array
  350.     {
  351.         $result = [];
  352.         // try {
  353.         $id $params['id'] ?? null;
  354.         if (!$id) {
  355.             throw new \Exception("EWS id is required");
  356.         }
  357.         $regionId $params['regionId'] ?? null;
  358.         $governorateIds $params['governateIds'] ?? null;
  359.         $mapGovernorateIds $params['mapGovernorateIds'] ?? null;
  360.         $municipalities $params['municipalityIds'] ?? null;
  361.         $centerIds $params['centerIds'] ?? null;
  362.         $districtIds $params['districtIds'] ?? null;
  363.         $ewsOtherLocationIds $params['ewsOtherLocationIds'] ?? null;
  364.         $alertActionId $params['alertActionId'] ?? null;
  365.         $alertTypeId $params['alertTypeId'] ?? null;
  366.         $weatherPhenomenonId $params['weatherPhenomenonId'] ?? null;
  367.         $coordinates json_encode($params['coordinates'] ?? []);
  368.         $message $params['message'] ?? [];
  369.         $alertHazardId $params['alertHazardId'] ?? null;
  370.         $enableTwitterNotification $params['enableTwitterNotification'] ?? false;
  371.         $enableSMSNotification $params['enableSMSNotification'] ?? false;
  372.         $enableEmailNotification $params['enableEmailNotification'] ?? false;
  373.         $previewText = isset($params['previewText']) ? $params['previewText'] : false;
  374.         $alertPage $params['alertPage'] ?? null;
  375.         $ewsNotification \Pimcore\Model\DataObject::getById($id);
  376.         if (!$ewsNotification instanceof \Pimcore\Model\DataObject\EwsNotification) {
  377.             // throw new \Exception("Ews notification not found");
  378.             return ['success' => false'message' => $translator->trans('ews_notification_not_found')];
  379.         }
  380.         if (isset($params['startDate'])) {
  381.             $startTime $params['startTime'] ?? '00:00:00';
  382.             $fromDate $params['startDate'] . ' ' $startTime;
  383.             $ewsNotification->setStartDate(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$fromDate));
  384.         }
  385.         if (isset($params['endDate'])) {
  386.             $endTime $params['endTime'] ?? '00:00:00';
  387.             $endDate $params['endDate'] . ' ' $endTime;
  388.             $ewsNotification->setEndDate(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$endDate));
  389.         }
  390.         if (isset($message['en'])) {
  391.             $ewsNotification->setMessage($message['en'], 'en');
  392.         }
  393.         if (isset($message['ar'])) {
  394.             $ewsNotification->setMessage($message['ar'], 'ar');
  395.         }
  396.         if (!empty($regionId)) {
  397.             $regionObj DataObject\Region::getByRegionId($regionId1);
  398.             if (!empty($regionObj)) {
  399.                 $ewsNotification->setRegion($regionObj);
  400.                 if ($governorateIds) {
  401.                     $governorateIdArr explode(","$governorateIds);
  402.                     if ($governorateIdArr) {
  403.                         $applyGovernates = [];
  404.                         foreach ($governorateIdArr as $id) {
  405.                             if (!empty($id)) {
  406.                                 $applyGovernates[] = \Pimcore\Model\DataObject\Governorate::getByGovernoteId($idtrue);
  407.                                 if (!empty(array_unique($applyGovernates))) {
  408.                                     $ewsNotification->setGovernorate(array_unique($applyGovernates));
  409.                                 }
  410.                             }
  411.                         }
  412.                     }
  413.                     if ($municipalities) {
  414.                         $municipalitiesIdArr explode(","$municipalities);
  415.                         if ($municipalitiesIdArr) {
  416.                             $applyMunicipalities = [];
  417.                             foreach ($municipalitiesIdArr as $id) {
  418.                                 $applyMunicipalities[] = \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($idtrue);
  419.                                 if (!empty(array_unique($applyMunicipalities))) {
  420.                                     $ewsNotification->setMunicipality(array_unique($applyMunicipalities));
  421.                                 }
  422.                             }
  423.                         }
  424.                     }
  425.                 } else {
  426.                     $ewsNotification->setGovernorate($this->governorateByRegion($regionObj->getId()));
  427.                 }
  428.             }
  429.         }
  430.         if(!empty($mapGovernorateIds)) {
  431.             $mapGovernorateIdArr explode(","$mapGovernorateIds);
  432.             if ($mapGovernorateIdArr) {
  433.                 $applyMapGovernates = [];
  434.                 foreach ($mapGovernorateIdArr as $id) {
  435.                     if (!empty($id)) {
  436.                         $applyMapGovernates[] = \Pimcore\Model\DataObject\Governorate::getByGovernoteId($idtrue);
  437.                         if (!empty(array_unique($applyMapGovernates))) {
  438.                             $ewsNotification->setMapGovernorate(array_unique($applyMapGovernates));
  439.                         }
  440.                     }
  441.                 }
  442.             }
  443.         }
  444.         if ($ewsOtherLocationIds) {
  445.             $ewsOtherLocationIdArr explode(","$ewsOtherLocationIds);
  446.             if ($ewsOtherLocationIdArr) {
  447.                 $applyOtherGovernates = [];
  448.                 foreach ($ewsOtherLocationIdArr as $id) {
  449.                     $applyOtherGovernates[] = \Pimcore\Model\DataObject\EwsOtherLocation::getByGovernoteId($idtrue);
  450.                     if (!empty(array_unique($applyOtherGovernates))) {
  451.                         $ewsNotification->setEwsOtherLocations(array_unique($applyOtherGovernates));
  452.                     }
  453.                 }
  454.             }
  455.         }
  456.         if (!empty($alertTypeId)) {
  457.             $alertType DataObject\AlertType::getByAlertTypeId($alertTypeId1);
  458.             $ewsNotification->setAlertType($alertType);
  459.         }
  460.         if (!empty($alertActionId)) {
  461.             $alertActionArr explode(","$alertActionId);
  462.             if ($alertActionArr) {
  463.                 $alertActionArrRequest = [];
  464.                 foreach ($alertActionArr as $alertActionId) {
  465.                     $alertAction DataObject\AlertAction::getByAlertActionId($alertActionId1);
  466.                     if ($alertAction) {
  467.                         $alertActionArrRequest[] = $alertAction;
  468.                     }
  469.                 }
  470.                 $ewsNotification->setAlertAction($alertActionArrRequest);
  471.             }
  472.         }
  473.         if (!empty($weatherPhenomenonId)) {
  474.             $alertStatus DataObject\AlertStatus::getByAlertStatusId($weatherPhenomenonIdtrue);
  475.             $ewsNotification->setAlertStatus($alertStatus);
  476.         }
  477.         if (!empty($centerIds)) {
  478.             $applyCenters = [];
  479.             foreach (array_filter(array_map('trim'explode(',', (string) $centerIds))) as $id) {
  480.                 $center DataObject\Centers::getById((int) $id);
  481.                 if ($center instanceof DataObject\Centers) {
  482.                     $applyCenters[] = $center;
  483.                 }
  484.             }
  485.             $applyCenters $this->capUniqueDataObjectRelations($applyCenters);
  486.             if ($applyCenters !== []) {
  487.                 $ewsNotification->setCenter($applyCenters);
  488.             }
  489.         }
  490.         if (!empty($districtIds)) {
  491.             $applyDistrict = [];
  492.             foreach (array_filter(array_map('trim'explode(',', (string) $districtIds))) as $id) {
  493.                 $district DataObject\District::getById((int) $id);
  494.                 if ($district instanceof DataObject\District) {
  495.                     $applyDistrict[] = $district;
  496.                 }
  497.             }
  498.             $applyDistrict $this->capUniqueDataObjectRelations($applyDistrict);
  499.             if ($applyDistrict !== []) {
  500.                 $ewsNotification->setDistrict($applyDistrict);
  501.             }
  502.         }
  503.         if (!empty($alertHazardId)) {
  504.             $alertHazardArr explode(","$alertHazardId);
  505.             if ($alertHazardArr) {
  506.                 $alertHazardArrRequest = [];
  507.                 foreach ($alertHazardArr as $alertHazardId) {
  508.                     $weatherPhenomenonAffect DataObject\AlertHazard::getByAlertHazardId($alertHazardId1);
  509.                     if ($weatherPhenomenonAffect) {
  510.                         $alertHazardArrRequest[] = $weatherPhenomenonAffect;
  511.                     }
  512.                 }
  513.                 $ewsNotification->setAlertHazard($alertHazardArrRequest);
  514.             }
  515.         }
  516.         $ewsNotification->setCoordinates($coordinates);
  517.         $addressComponents $params['address_components'] ?? [];
  518.         $this->addAddressComponentsFieldCollection($addressComponents$ewsNotification);
  519.         if (!empty($params['file'])) {
  520.             $asset $this->createAsset($params['file'], uniqid() . $params['filename']);
  521.             if ($asset) {
  522.                 $ewsNotification->setAttachment($asset);
  523.             }
  524.         }else{
  525.             $ewsNotification->setAttachment(null);
  526.         }
  527.         if (!empty($params['x_img'])) {
  528.             $asset $this->createAsset($params['x_img'], uniqid() . "EWSTwitterImages.png");
  529.             if ($asset) {
  530.                 $ewsNotification->setXAttachment($asset);
  531.             }
  532.         }else{
  533.             $ewsNotification->setXAttachment(null);
  534.         }
  535.         if (!empty($params['polygon_x_img'])) {
  536.             $asset $this->createAsset($params['polygon_x_img'], uniqid() . "EWSPolygonTwitterImages.png");
  537.             if ($asset) {
  538.                 $ewsNotification->setPolygonXAtachment($asset);
  539.             }
  540.         }else{
  541.             $ewsNotification->setPolygonXAtachment(null);
  542.         }
  543.         if (!empty($params['x_post'])) {
  544.             $ewsNotification->setXPost($params['x_post']);
  545.         }
  546.         $ewsNotification->setEnableTwitterNotification($enableTwitterNotification);
  547.         $ewsNotification->setEnableSMSNotification($enableSMSNotification);
  548.         $ewsNotification->setEnableEmailNotification($enableEmailNotification);
  549.         $ewsNotification->setPreviewText($previewText);
  550.         if (isset($params['alertPage'])) {
  551.             $ewsNotification->setAlertPage($alertPage);
  552.         }
  553.         //$ewsNotification->setPublished(true);
  554.         $ewsNotification->setUser($userInterface);
  555.         $ewsNotification->setEditor($userInterface);
  556.         //set ews search Id
  557.         $currentDate = new \DateTime();
  558.         $formattedDate $currentDate->format('dmY') . '-' $ewsNotification->getId();
  559.         $searchIdEn 'Early Warning System | ' $formattedDate ' | ' ucfirst($ewsNotification->getAlertType()?->getColor()) . ' Alert | ' $ewsNotification->getWeatherPhenomenon()?->getTitle("en");
  560.         $searchIdAr $translator->trans('Early Warning System', [], null"ar") . ' | ' $formattedDate ' | ' $translator->trans(ucfirst($ewsNotification->getAlertType()?->getColor()) . ' Alert', [], null"ar") . ' | ' $ewsNotification->getWeatherPhenomenon()?->getTitle("ar");
  561.         $ewsNotification->setEwsSearchId($searchIdEn"en");
  562.         $ewsNotification->setEwsSearchId($searchIdAr"ar");
  563.         // // When publishing an updated alert to Twitter, generate Alert History image via wkhtmltoimage.
  564.         // if ($enableTwitterNotification && $templating) {
  565.         //     try {
  566.         //         $historyLang = method_exists($translator, 'getLocale') ? (string) $translator->getLocale() : 'en';
  567.         //         $historyAsset = $this->generateAlertHistoryTwitterImage(
  568.         //             $ewsNotification,
  569.         //             $templating,
  570.         //             $logger,
  571.         //             !empty($alertActionId),
  572.         //             $translator,
  573.         //             $historyLang
  574.         //         );
  575.         //         if ($historyAsset) {
  576.         //             $ewsNotification->setXHistoryAtachment($historyAsset);
  577.         //         }
  578.         //     } catch (\Throwable $e) {
  579.         //         $logger->error('Failed to generate EWS alert history Twitter image: ' . $e->getMessage());
  580.         //     }
  581.         // }
  582.         // Allow TwitterEventListner to post again when republishing an updated alert.
  583.         if ($isPublished && $enableTwitterNotification && $ewsNotification->isPublished(true)) {
  584.             $ewsNotification->setTwitterId('');
  585.             $ewsNotification->setTwitterLog('');
  586.         }
  587.         // Persist the selected user groups so the End alert can email the same groups later.
  588.         if (!empty($userGroupIds)) {
  589.             $groups = [];
  590.             foreach ($userGroupIds as $gid) {
  591.                 $group EwsAndReportUserGroup::getById($gidtrue);
  592.                 if ($group instanceof EwsAndReportUserGroup) {
  593.                     $groups[] = $group;
  594.                 }
  595.             }
  596.             $ewsNotification->setUserGroup($groups);
  597.         }
  598.         $ewsNotification->save(["versionNote" => "Update"]);
  599.         $this->saveUserMessageSuggestions(
  600.             (int) $userInterface->getId(),
  601.             $message
  602.         );
  603.         if ($this->shouldDispatchPublicPortalEwsWebhook($ewsNotification)) {
  604.             $this->dispatchPublicPortalEwsWebhookIfConfigured((int) $ewsNotification->getId(), $logger);
  605.         }
  606.         if ($isPublished) {
  607.             // send email
  608.             // Ensure you use 'php' to execute the command.
  609.             $jsonUserGroupIds json_encode($userGroupIds);
  610.             $process = new Process(['php''bin/console''app:send-early-warning-alert-email''--alertId=' $ewsNotification->getId(), '--userGroupIds=' $jsonUserGroupIds]);
  611.             $process->setWorkingDirectory(PIMCORE_PROJECT_ROOT);
  612.             try {
  613.                 $process->mustRun();
  614.                 $result['success'] = true;
  615.                 $logger->info("update EwsNotification command executed successfully: " $process->getOutput());
  616.                 $result['message'] = $process->getOutput();
  617.             } catch (ProcessFailedException $exception) {
  618.                 $logger->error("update EwsNotification command failed: " $exception->getMessage());
  619.                 return ['success' => false'message' => $exception->getMessage()];
  620.             }
  621.         }
  622.         return ['success' => true'message' => $translator->trans('ews_notification_updated_successifully'), "notification_id" => $ewsNotification->getId()];
  623.         // } catch (\Exception $ex) {
  624.         //     $result = ['success' => false, 'message' => $ex->getMessage()];
  625.         // }
  626.         return $result;
  627.     }
  628.     public function governorateByRegion($regionId)
  629.     {
  630.         $governorateArr = [];
  631.         $governorateList = new DataObject\Governorate\Listing();
  632.         $governorateList->setCondition('regionId__id = ?'$regionId);
  633.         $governorateList->load();
  634.         if (!empty($governorateList)) {
  635.             foreach ($governorateList as $governorate) {
  636.                 array_push($governorateArr$governorate);
  637.             }
  638.         }
  639.         return $governorateArr;
  640.     }
  641.     public function notificationListing($params$paginator$translator): array
  642.     {
  643.         $response = [];
  644.         // try {
  645.         $pageSize = isset($params['page_size']) ? $params['page_size'] : LIMIT_PER_PAGE;
  646.         $page = isset($params['page']) ? $params['page'] : 1;
  647.         $notificationList = new DataObject\EwsNotification\Listing();
  648.         // $notificationList->setOffset(!empty($params['offset']) ? $params['offset'] : 0);
  649.         // $notificationList->setLimit(!empty($params['limit']) ? $params['limit'] : LIMIT_PER_PAGE);
  650.         $notificationList->setOrderKey("o_creationDate");
  651.         $notificationList->setOrder("desc");
  652.         $notificationList->setUnpublished(true);
  653.         if ($params['unpublished'] == false) {
  654.             $notificationList->filterByPublished(true);
  655.         } else {
  656.             $notificationList->filterByPublished(false);
  657.         }
  658.         $notificationList->setOrder("desc");
  659.         if (isset($params['region']) && !empty($params['region'])) {
  660.             $regionSql null;
  661.             $alertRegion \Pimcore\Model\DataObject\Region::getByRegionId($params['region'], true);
  662.             if ($alertRegion) {
  663.                 $regionSql .= "region__id = " $alertRegion->getId() . " OR ";
  664.             }
  665.             $notificationList->addConditionParam("(" substr($regionSql0, -3) . ")");
  666.         }
  667.         if (isset($params['status']) && !empty($params['status'])) {
  668.             $notificationList->addConditionParam("status IN (?)", [$params['status']]);
  669.         }
  670.         if (isset($params['addressCmp']) && !empty($params['addressCmp'])) {
  671.             //TODO
  672.         }
  673.         if (isset($params['start_date']) && !empty($params['start_date'])  && isset($params['end_date']) && !empty($params['end_date'])) {
  674.             $notificationList->filterByStartDate(strtotime($params['start_date']), ">=");
  675.             $notificationList->filterByEndDate(strtotime($params['end_date']), "<=");
  676.         }
  677.         if (isset($params['fromDate']) && isset($params['toDate']) && !empty($params['fromDate']) && !empty($params['toDate'])) {
  678.             $fromDate = new \DateTime($params['fromDate']);
  679.             $toDate = new \DateTime($params['toDate']);
  680.             // Ensure the dates are in the correct format
  681.             $fromDateStr strtotime($fromDate->format('Y-m-d H:i:s'));
  682.             $toDateStr strtotime($toDate->format('Y-m-d') . ' 23:59:59');
  683.             $notificationList->addConditionParam(
  684.                 "(o_creationDate >= ? AND o_creationDate <= ?)",
  685.                 [$fromDateStr$toDateStr]
  686.             );
  687.         }
  688.         $notificationList->load();
  689.         if ($notificationList) {
  690.             foreach ($notificationList as $notification) {
  691.                 $response[] = $this->createNotificationFormat($notification$translator);
  692.             }
  693.         }
  694.         if ($paginator == null) {
  695.             return ["success" => TRUE"data" => $response];
  696.         }
  697.         $paginator $paginator->paginate(
  698.             $response,
  699.             $page,
  700.             $pageSize
  701.         );
  702.         return ["success" => TRUE"data" => $paginator"paginationVariables" => $paginator->getPaginationData()];
  703.         // return $response;
  704.         // } catch (\Exception $ex) {
  705.         //     $result = ["success" => false, "message" => $ex->getMessage()];
  706.         // }
  707.         // return $result;
  708.     }
  709.     public function capAlertNotificationListingxml($request$params$lang$translator)
  710.     {
  711.         try {
  712.             $notificationList = new DataObject\EwsNotification\Listing();
  713.             // Get the timezone for date operations
  714.             $timezone = new \DateTimeZone(TIMEZONE);
  715.             // Filter by pubDate if provided (format: d-m-Y, e.g., 25-12-2025)
  716.             if (isset($params['pubDate']) && !empty($params['pubDate'])) {
  717.                 $pubDateStr $params['pubDate'];
  718.                 // Parse the date from d-m-Y format
  719.                 $pubDate \DateTime::createFromFormat('d-m-Y'$pubDateStr$timezone);
  720.                 if ($pubDate === false) {
  721.                     // Try alternative format Y-m-d
  722.                     $pubDate \DateTime::createFromFormat('Y-m-d'$pubDateStr$timezone);
  723.                 }
  724.                 if ($pubDate !== false) {
  725.                     // Set time to start of day (00:00:00)
  726.                     $pubDate->setTime(000);
  727.                     $startTimestamp $pubDate->getTimestamp();
  728.                     // Set time to end of day (23:59:59)
  729.                     $pubDateEnd = clone $pubDate;
  730.                     $pubDateEnd->setTime(235959);
  731.                     $endTimestamp $pubDateEnd->getTimestamp();
  732.                     // Filter notifications where startDate falls within the specified date
  733.                     // Use filterByStartDate for >= start of day
  734.                     $notificationList->filterByStartDate($startTimestamp">=");
  735.                     // Add condition for <= end of day using addConditionParam
  736.                     $notificationList->addConditionParam("startDate <= ?", [$endTimestamp]);
  737.                 }
  738.             } else {
  739.                 $notificationList->setCondition("endDate > ?", [time()]);
  740.             }
  741.             $notificationList->setOrderKey("oo_id");
  742.             $notificationList->setOrder("desc");
  743.             // Get the current date as a DateTime object
  744.             $currentDate = new \DateTime('now'$timezone);
  745.             // Set the condition to filter for notifications where endDate is greater than the current date
  746.             // Loop through the notifications to find the last updated date
  747.             // Initialize $lastUpdatedDate with current date as default
  748.             $lastUpdatedDate $currentDate;
  749.             foreach ($notificationList as $notification) {
  750.                 $notificationUpdatedDate $notification->getEndDate()->setTimezone($timezone);
  751.                 if ($notificationUpdatedDate $lastUpdatedDate) {
  752.                     $lastUpdatedDate $notificationUpdatedDate;
  753.                 }
  754.             }
  755.             // Create an empty RSS XML structure
  756.             // $rss = new SimpleXMLElement('<rss version="2.0"/>');
  757.             $rss = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"/>');
  758.             // Create the <channel> element
  759.             $channel $rss->addChild('channel');
  760.             // Add elements inside <channel>
  761.             $channel->addChild('title'$translator->trans("Early Warning System from National Center of Meteorology, Saudi Arabia"));
  762.             $channel->addChild('link'$_ENV['BASE_URL'] . '/' $lang '/cap-alerts');
  763.             $channel->addChild('description'$translator->trans("Early Warning System from National Center of Meteorology, Saudi Arabia"));
  764.             $channel->addChild('language'$lang);
  765.             $channel->addChild('copyright'$translator->trans("Copyright (c) " date('Y') . ", NCM. Licensed under Creative Commons BY 4.0"));
  766.             $channel->addChild('pubDate'$lastUpdatedDate->format('D, d M Y H:i:s \G\M\T'));
  767.             // Create the <image> element
  768.             $image $channel->addChild('image');
  769.             $image->addChild('title'$translator->trans("Early Warning System from National Center of Meteorology, Saudi Arabia"));
  770.             $image->addChild('url'$_ENV['BASE_URL'] . '/assets/images/logo2.3.png');
  771.             $image->addChild('link'$_ENV['BASE_URL'] . '/' $lang '/cap-alerts');
  772.             // Create multiple <alert> elements and add them to the <alerts> element
  773.             // Create child elements and add them to the alert
  774.             foreach ($notificationList as $notification) { // Create two <alert> elements as an example
  775.                 $hazardNames "";
  776.                 if (!empty($notification->getAlertHazard())) {
  777.                     $hazardNamesArr = [];
  778.                     foreach ($notification->getAlertHazard() as $hazard) {
  779.                         $hazardNamesArr[] = $hazard->getName($lang);
  780.                     }
  781.                     $hazardNames .= implode(", "$hazardNamesArr);
  782.                 }
  783.                 $governorateNames "";
  784.                 $governorateCoordinates "";
  785.                 $governorateNamesArr = [];
  786.                 if ($notification->getGovernorate() && !empty($notification->getGovernorate())) {
  787.                     $governorateCoordinatesArr = [];
  788.                     foreach ($notification->getGovernorate() as $governorate) {
  789.                         $governorateNamesArr[] = $governorate->getName($lang);
  790.                         $governorateCoordinatesArr[] = $governorate->getLatitude() . "," $governorate->getLongitude();
  791.                     }
  792.                     $governorateCoordinates .= implode(" "$governorateCoordinatesArr);
  793.                     // Format governorates with "and" before the last one
  794.                     if (count($governorateNamesArr) > 1) {
  795.                         $lastGovernorate array_pop($governorateNamesArr);
  796.                         $governorateNames implode(" - "$governorateNamesArr) . " - " $lastGovernorate;
  797.                     } else {
  798.                         $governorateNames $governorateNamesArr[0];
  799.                     }
  800.                 }
  801.                 // Get region and alert status names
  802.                 $regionName $notification->getRegion() ? $notification->getRegion()->getName($lang) : $translator->trans("UNKNOWN");
  803.                 $alertStatusName = !empty($notification->getAlertStatus()) ? $notification->getAlertStatus()->getName($lang) : $translator->trans("UNKNOWN");
  804.                 // Format title
  805.                 $title =  $alertStatusName ", " $regionName;
  806.                 // Format dates in GMT format for description
  807.                 $startDateGMT = !empty($notification->getStartDate()) ? $notification->getStartDate()->setTimezone(new \DateTimeZone('GMT'))->format('D, d M Y H:i:s \G\M\T') : $translator->trans("UNKNOWN");
  808.                 $endDateGMT = !empty($notification->getEndDate()) ? $notification->getEndDate()->setTimezone(new \DateTimeZone('GMT'))->format('D, d M Y H:i:s \G\M\T') : $translator->trans("UNKNOWN");
  809.                 // Format description: "{Hazard names} From: {startDate GMT} To: {endDate GMT}, {Location}"
  810.                 $descriptionParts = [];
  811.                 // if (!empty($hazardNames)) {
  812.                 //     $descriptionParts[] = $hazardNames;
  813.                 // }
  814.                 $descriptionParts[] = $alertStatusName " " $translator->trans("From") . ": " $startDateGMT " " $translator->trans("To") . ": " $endDateGMT;
  815.                 if (!empty($governorateNames)) {
  816.                     $descriptionParts[] = ", " $governorateNames;
  817.                 }
  818.                 $description implode(" "$descriptionParts);
  819.                 // Create one or more <item> elements
  820.                 $item $channel->addChild('item');
  821.                 $item->addChild('guid'"{" $notification->getGuid() . "}");
  822.                 $item->addChild('title'$title);
  823.                 $item->addChild('link'$_ENV['BASE_URL'] . '/cap/' $lang '/alerts/' $notification->getGuid() . '.xml');
  824.                 $item->addChild('description'$description);
  825.                 $item->addChild('author'AUTHOR);
  826.                 $item->addChild('pubDate'$notification->getStartDate()->setTimeZone(new \DateTimeZone('GMT'))->format('D, d M Y H:i:s \G\M\T'));
  827.             }
  828.             // Create additional <item> elements as needed
  829.             // Format the XML for output
  830.             $dom dom_import_simplexml($rss)->ownerDocument;
  831.             $dom->formatOutput true;
  832.             // Save the XML to a file
  833.             $xmlString $dom->saveXML();
  834.             // Set the Content-Type header to specify that the response is XML
  835.             header("Content-Type: application/xml");
  836.             // Save the XML to a file or output it
  837.             $xmlString $rss->saveXML();
  838.             print $xmlString;
  839.             exit;
  840.             return $xmlString;
  841.         } catch (\Exception $ex) {
  842.             $result = ["success" => false"message" => $ex->getMessage()];
  843.         }
  844.         return $result;
  845.     }
  846.     public function capAlertNotificationDetailxml($guid$lang$translator)
  847.     {
  848.         $response = [];
  849.         $timezone = new \DateTimeZone(TIMEZONE);
  850.         try {
  851.             $notification \Pimcore\Model\DataObject\EwsNotification::getByGuid($guidtrue);
  852.             if (empty($notification)) {
  853.                 throw new \Exception('Notification not found.');
  854.             }
  855.             $doc = new DomDocument('1.0''UTF-8');
  856.             // Create the root element
  857.             $alert $doc->createElement('alert');
  858.             $alert->setAttribute('xmlns''urn:oasis:names:tc:emergency:cap:1.2');
  859.             $alert->setAttribute('xmlns:xsl''http://www.w3.org/1999/XSL/Transform');
  860.             $alert->setAttribute('xmlns:ds''http://www.w3.org/2000/09/xmldsig#');
  861.             $doc->appendChild($alert);
  862.             // Create child elements and add them to the alert
  863.             $identifier $doc->createElement('identifier',  $notification->getGuid()); //uuid
  864.             $sender $doc->createElement('sender'$translator->trans('NCM'));
  865.             $sent $doc->createElement('sent'$notification->getStartDate()->setTimezone($timezone)->format('Y-m-d\TH:i:sP'));
  866.             $status $doc->createElement('status''Actual');
  867.             $msgType $doc->createElement('msgType''Alert');
  868.             $scope $doc->createElement('scope''Public');
  869.             $alert->appendChild($identifier);
  870.             $alert->appendChild($sender);
  871.             $alert->appendChild($sent);
  872.             $alert->appendChild($status);
  873.             $alert->appendChild($msgType);
  874.             $alert->appendChild($scope);
  875.             // Create the <info> element and add it to the alert
  876.             $info $doc->createElement('info');
  877.             $alert->appendChild($info);
  878.             // Create child elements for the <info> element
  879.             $category $doc->createElement('category''Met');
  880.             $event $doc->createElement('event'$notification->getAlertStatus() ? $notification->getAlertStatus()->getName($lang) : $translator->trans('UNKNOWN')); //high rain
  881.             $responseType $doc->createElement('responseType''None');
  882.             $effective $doc->createElement('effective'$notification->getStartDate()->setTimezone($timezone)->format('Y-m-d\TH:i:sP'));
  883.             // Determine CAP urgency, severity, and certainty based on alert type and event
  884.             $eventName $notification->getAlertStatus() ? strtolower($notification->getAlertStatus()->getName('en')) : '';
  885.             $alertTypeColor $notification->getAlertType() ? strtoupper($notification->getAlertType()->getColor() ?? '') : '';
  886.             // Determine urgency based on alert type and event severity
  887.             // Immediate: Life-threatening, requires immediate action (RED alerts, tornadoes, etc.)
  888.             // Expected: Forecasted events that will occur (most weather alerts)
  889.             // Past: Events that have already occurred
  890.             // Future: Long-term forecasts
  891.             $urgency 'Expected'// Default for most weather alerts
  892.             if ($alertTypeColor === 'RED' || stripos($eventName'tornado') !== false || stripos($eventName'extreme') !== false) {
  893.                 $urgency 'Immediate';
  894.             }
  895.             // Determine severity based on alert type color and event
  896.             // Extreme: Widespread destruction
  897.             // Severe: Significant threat to life/property
  898.             // Moderate: Some threat
  899.             // Minor: Minimal threat
  900.             // Unknown: Severity unknown
  901.             $severity 'Moderate'// Default
  902.             if ($alertTypeColor === 'RED') {
  903.                 $severity 'Extreme';
  904.             } elseif ($alertTypeColor === 'ORANGE') {
  905.                 $severity 'Severe';
  906.             } elseif ($alertTypeColor === 'YELLOW') {
  907.                 $severity 'Moderate';
  908.             } elseif ($alertTypeColor === 'GREEN' || stripos($eventName'light') !== false || stripos($eventName'minor') !== false) {
  909.                 $severity 'Minor';
  910.             }
  911.             // Certainty: Observed (happening now), Likely (will occur), Possible (may occur), Unlikely, Unknown
  912.             $certainty 'Possible'// Default - assume observed for active alerts
  913.             $urgencyElement $doc->createElement('urgency'$urgency);
  914.             $severityElement $doc->createElement('severity'$severity);
  915.             $certaintyElement $doc->createElement('certainty'$certainty);
  916.             // Remove eventCode element - SAME codes are US-specific and not applicable
  917.             // If eventCode is needed in future, use WMO codes instead:
  918.             // $eventCode = $doc->createElement('eventCode');
  919.             // $eventCodeValueName = $doc->createElement('valueName', 'WMO');
  920.             // $eventCodeValueText = $doc->createElement('value', 'FOG'); // Example WMO code
  921.             // $eventCode->appendChild($eventCodeValueName);
  922.             // $eventCode->appendChild($eventCodeValueText);
  923.             // Fix: onset should use startDate (when the event begins), not endDate
  924.             $onsetDate $notification->getStartDate() ? $notification->getStartDate()->setTimezone($timezone) : null;
  925.             if (!$onsetDate) {
  926.                 // Fallback to current time if startDate is not available
  927.                 $onsetDate = new \DateTime('now'$timezone);
  928.             }
  929.             $onset $doc->createElement('onset'$onsetDate->format('Y-m-d\TH:i:sP'));
  930.             // Fix: expires must be after onset (use endDate, ensure it's after startDate)
  931.             $expiresDate $notification->getEndDate() ? $notification->getEndDate()->setTimezone($timezone) : null;
  932.             if (!$expiresDate) {
  933.                 // Fallback: if no endDate, set expires to 24 hours after onset
  934.                 $expiresDate = clone $onsetDate;
  935.                 $expiresDate->modify('+24 hours');
  936.             } else {
  937.                 // Ensure expires is after onset (CAP requirement)
  938.                 if ($expiresDate <= $onsetDate) {
  939.                     // If expires equals or is before onset, add at least 1 hour
  940.                     $expiresDate = clone $onsetDate;
  941.                     $expiresDate->modify('+1 hour');
  942.                 }
  943.             }
  944.             $expires $doc->createElement('expires'$expiresDate->format('Y-m-d\TH:i:sP'));
  945.             $senderName $doc->createElement('senderName'$translator->trans('NCM'));
  946.             $headline $doc->createElement('headline'$notification->getAlertType()->getName($lang) . ', ' .  $notification->getAlertStatus()->getName($lang));
  947.             // $headline = $doc->createElement(
  948.             //     'headline',
  949.             //     $translator->trans('Early Warning Alert in') . ' ' .
  950.             //     (
  951.             //         $notification->getRegion()
  952.             //             ? $notification->getRegion()->getName($lang)
  953.             //             : $translator->trans('UNKNOWN')
  954.             //     ) . ': ' .
  955.             //     (
  956.             //         $notification->getAlertStatus()
  957.             //             ? $notification->getAlertStatus()->getName($lang)
  958.             //             : $translator->trans('UNKNOWN')
  959.             //     )
  960.             // );
  961.             $hazardNames "";
  962.             if (!empty($notification->getAlertHazard())) {
  963.                 $hazardNamesArr = [];
  964.                 foreach ($notification->getAlertHazard() as $hazard) {
  965.                     $hazardNamesArr[] = $hazard->getName($lang);
  966.                 }
  967.                 $hazardNames .= implode(", "$hazardNamesArr);
  968.             }
  969.             $governorateNames "";
  970.             $governorateCoordinates "";
  971.             $governorateNamesArr = [];
  972.             if ($notification->getGovernorate() && !empty($notification->getGovernorate())) {
  973.                 $governorateCoordinatesArr = [];
  974.                 foreach ($notification->getGovernorate() as $governorate) {
  975.                     $governorateNamesArr[] = $governorate->getName($lang);
  976.                     $governorateCoordinatesArr[] = $governorate->getLatitude() . "," $governorate->getLongitude();
  977.                 }
  978.                 // Format governorates with "و" for Arabic or "and" for English before the last one
  979.                 if (count($governorateNamesArr) > 1) {
  980.                     $lastGovernorate array_pop($governorateNamesArr);
  981.                     $andText = ($lang == 'ar') ? ' و ' ', and ';
  982.                     $governorateNames implode(", "$governorateNamesArr) . $andText $lastGovernorate;
  983.                 } else {
  984.                     $governorateNames $governorateNamesArr[0];
  985.                 }
  986.                 $governorateCoordinates .= implode(" "$governorateCoordinatesArr);
  987.             }
  988.             // Get region and alert status names
  989.             $regionName $notification->getRegion() ? $notification->getRegion()->getName($lang) : $translator->trans("UNKNOWN");
  990.             $alertStatusName $notification->getAlertStatus() ? $notification->getAlertStatus()->getName($lang) : $translator->trans("UNKNOWN");
  991.             // Format dates
  992.             $startDateFormatted = !empty($notification->getStartDate()) ? $notification->getStartDate()->setTimezone($timezone)->format('d/m/Y H:i:s A') : $translator->trans("UNKNOWN");
  993.             $endDateFormatted = !empty($notification->getEndDate()) ? $notification->getEndDate()->setTimezone($timezone)->format("d/m/Y H:i:s A") : $translator->trans("UNKNOWN");
  994.             // Format description: Comma-separated list of hazard names (e.g., "Raised dust, Active winds,Near lack of horizontal visibility (1-3) km")
  995.             $descriptionText = !empty($hazardNames) ? $hazardNames "";
  996.             $description $doc->createElement('description'$notification->getAlertStatus()->getName($lang) . ' ' $descriptionText);
  997.             // Create actionable instruction based on alert type and hazards (CAP compliance)
  998.             // Handle multiple hazards by checking all types and combining instructions
  999.             $instructionText '';
  1000.             if (!empty($hazardNames)) {
  1001.                 $instructions = [];
  1002.                 $hazardNamesLower strtolower($hazardNames);
  1003.                 // Check for all hazard types and collect relevant instructions
  1004.                 // Priority hazards first
  1005.                 if (stripos($hazardNames'tornado') !== false || stripos($hazardNames'إعصار') !== false) {
  1006.                     $instructions[] = ($lang == 'ar')
  1007.                         ? 'انتقل فوراً إلى أقرب ملجأ أو الطابق السفلي. تجنب النوافذ والمناطق المكشوفة.'
  1008.                         'Move immediately to the nearest shelter or basement. Avoid windows and exposed areas.';
  1009.                 }
  1010.                 if (
  1011.                     stripos($hazardNames'rain') !== false || stripos($hazardNames'مطر') !== false ||
  1012.                     stripos($hazardNames'flood') !== false || stripos($hazardNames'فيضان') !== false ||
  1013.                     stripos($hazardNames'pooling') !== false || stripos($hazardNames'تجمع') !== false
  1014.                 ) {
  1015.                     $instructions[] = ($lang == 'ar')
  1016.                         ? 'انتقل إلى أرض مرتفعة لتجنب الفيضانات. تجنب القيادة في المناطق المغمورة.'
  1017.                         'Move to higher ground to avoid flooding. Avoid driving through flooded areas.';
  1018.                 }
  1019.                 if (stripos($hazardNames'wave') !== false || stripos($hazardNames'موجة') !== false) {
  1020.                     $instructions[] = ($lang == 'ar')
  1021.                         ? 'تجنب المناطق الساحلية والأنشطة البحرية. ابق بعيداً عن الشواطئ.'
  1022.                         'Avoid coastal areas and marine activities. Stay away from beaches.';
  1023.                 }
  1024.                 if (stripos($hazardNames'wind') !== false || stripos($hazardNames'رياح') !== false) {
  1025.                     $instructions[] = ($lang == 'ar')
  1026.                         ? 'تجنب الخروج من المنزل وتأكد من إغلاق النوافذ والأبواب بإحكام. تجنب القيادة إذا أمكن.'
  1027.                         'Avoid going outside and ensure windows and doors are securely closed. Avoid driving if possible.';
  1028.                 }
  1029.                 if (stripos($hazardNames'thunder') !== false || stripos($hazardNames'رعد') !== false) {
  1030.                     $instructions[] = ($lang == 'ar')
  1031.                         ? 'تجنب المناطق المكشوفة والبقاء في الداخل. تجنب استخدام الأجهزة الإلكترونية والمياه.'
  1032.                         'Avoid exposed areas and stay indoors. Avoid using electronic devices and water.';
  1033.                 }
  1034.                 if (stripos($hazardNames'hail') !== false || stripos($hazardNames'برد') !== false) {
  1035.                     $instructions[] = ($lang == 'ar')
  1036.                         ? 'ابق في الداخل وتجنب النوافذ. إذا كنت في الخارج، ابحث عن مأوى فوري.'
  1037.                         'Stay indoors and avoid windows. If outside, seek immediate shelter.';
  1038.                 }
  1039.                 if (
  1040.                     stripos($hazardNames'visibility') !== false || stripos($hazardNames'رؤية') !== false ||
  1041.                     stripos($hazardNames'fog') !== false || stripos($hazardNames'ضباب') !== false ||
  1042.                     stripos($hazardNames'low visibility') !== false || stripos($hazardNames'lack of horizontal visibility') !== false
  1043.                 ) {
  1044.                     $instructions[] = ($lang == 'ar')
  1045.                         ? 'تجنب القيادة إلا للضرورة القصوى. إذا كنت تقود، استخدم الأضواء المنخفضة واتبع المسافة الآمنة.'
  1046.                         'Avoid driving except in emergencies. If driving, use low beams and maintain safe distance.';
  1047.                 }
  1048.                 if (stripos($hazardNames'snow') !== false || stripos($hazardNames'ثلج') !== false) {
  1049.                     $instructions[] = ($lang == 'ar')
  1050.                         ? 'تجنب السفر غير الضروري. إذا كنت في الخارج، ارتدِ ملابس دافئة وتجنب الطرق الزلقة.'
  1051.                         'Avoid unnecessary travel. If outside, wear warm clothing and avoid slippery roads.';
  1052.                 }
  1053.                 if (
  1054.                     stripos($hazardNames'frost') !== false || stripos($hazardNames'صقيع') !== false ||
  1055.                     (stripos($hazardNames'temperature') !== false && stripos($hazardNames'drop') !== false) ||
  1056.                     (stripos($hazardNames'درجة') !== false && stripos($hazardNames'انخفاض') !== false) ||
  1057.                     stripos($hazardNames'below zero') !== false
  1058.                 ) {
  1059.                     $instructions[] = ($lang == 'ar')
  1060.                         ? 'احمِ نفسك من البرد الشديد. ارتدِ ملابس دافئة وتجنب التعرض الطويل للطقس البارد.'
  1061.                         'Protect yourself from extreme cold. Wear warm clothing and avoid prolonged exposure to cold weather.';
  1062.                 }
  1063.                 if ((stripos($hazardNames'rise') !== false && stripos($hazardNames'temperature') !== false) ||
  1064.                     (stripos($hazardNames'ارتفاع') !== false && stripos($hazardNames'درجة') !== false) ||
  1065.                     stripos($hazardNames'degrees Celsius') !== false || stripos($hazardNames'درجة مئوية') !== false
  1066.                 ) {
  1067.                     $instructions[] = ($lang == 'ar')
  1068.                         ? 'احمِ نفسك من الحرارة الشديدة. ابق في أماكن مكيفة، اشرب الكثير من الماء، وتجنب الأنشطة الخارجية.'
  1069.                         'Protect yourself from extreme heat. Stay in air-conditioned areas, drink plenty of water, and avoid outdoor activities.';
  1070.                 }
  1071.                 // Combine all instructions or use default
  1072.                 if (!empty($instructions)) {
  1073.                     // Remove duplicates and combine with appropriate separator
  1074.                     $uniqueInstructions array_unique($instructions);
  1075.                     $separator = ($lang == 'ar') ? ' ' ' ';
  1076.                     $instructionText implode($separator$uniqueInstructions);
  1077.                 } else {
  1078.                     $instructionText = ($lang == 'ar')
  1079.                         ? 'اتخذ الاحتياطات اللازمة واتبع التعليمات الرسمية. ابق في مكان آمن.'
  1080.                         'Take necessary precautions and follow official instructions. Stay in a safe place.';
  1081.                 }
  1082.             } else {
  1083.                 // Default instruction if no specific hazards
  1084.                 $instructionText = ($lang == 'ar')
  1085.                     ? 'اتخذ الاحتياطات اللازمة واتبع التعليمات الرسمية من المركز الوطني للأرصاد.'
  1086.                     'Take necessary precautions and follow official instructions from the National Center of Meteorology.';
  1087.             }
  1088.             $instruction $doc->createElement('instruction'$instructionText);
  1089.             $contact $doc->createElement('contact'CONTACT);
  1090.             // Create dynamic web URL using notification ID (CAP compliance - absolute HTTPS URL)
  1091.             $web $doc->createElement('web'htmlspecialchars($_ENV['PUBLIC_PORTAL_URL'] . $lang '/test-early-warning/' $notification->getId(), ENT_XML1'UTF-8'));
  1092.             $info->appendChild($category);
  1093.             $info->appendChild($event);
  1094.             $info->appendChild($responseType);
  1095.             $info->appendChild($urgencyElement);
  1096.             $info->appendChild($severityElement);
  1097.             $info->appendChild($certaintyElement);
  1098.             // eventCode removed - SAME codes are US-specific
  1099.             $info->appendChild($effective);
  1100.             $info->appendChild($onset);
  1101.             $info->appendChild($expires);
  1102.             $info->appendChild($senderName);
  1103.             $info->appendChild($headline);
  1104.             $info->appendChild($description);
  1105.             $info->appendChild($instruction);
  1106.             $info->appendChild($web);
  1107.             $info->appendChild($contact);
  1108.             // Get polygon coordinates from JSON lookup sheet - returns array of polygons per governorate
  1109.             $governoratePolygons = [];
  1110.             // Try to get polygons from the JSON lookup sheet first
  1111.             if ($notification->getGovernorate() && !empty($notification->getGovernorate())) {
  1112.                 // Convert collection to array if needed
  1113.                 $governorates $notification->getGovernorate();
  1114.                 if (is_object($governorates) && method_exists($governorates'getItems')) {
  1115.                     $governorates $governorates->getItems();
  1116.                 } elseif (!is_array($governorates)) {
  1117.                     $governorates iterator_to_array($governorates);
  1118.                 }
  1119.                 if (!empty($governorates)) {
  1120.                     $governoratePolygons $this->getPolygonFromJsonSheet($governorates);
  1121.                 }
  1122.             }
  1123.             // Fallback to notification coordinates if JSON lookup didn't provide polygons
  1124.             if (empty($governoratePolygons)) {
  1125.                 $coordinatesString $notification->getCoordinates();
  1126.                 if (!empty($coordinatesString)) {
  1127.                     // Try to parse as JSON (GeoJSON format)
  1128.                     $coordinatesData json_decode($coordinatesStringtrue);
  1129.                     if (json_last_error() === JSON_ERROR_NONE && is_array($coordinatesData)) {
  1130.                         // Handle GeoJSON format: coordinates can be in various structures
  1131.                         // [[[lng, lat], [lng, lat], ...]] for Polygon
  1132.                         // [[lng, lat], [lng, lat], ...] for LineString
  1133.                         // [lng, lat] for Point
  1134.                         $coordArray = [];
  1135.                         if (isset($coordinatesData['type']) && $coordinatesData['type'] === 'Polygon' && isset($coordinatesData['coordinates'])) {
  1136.                             // GeoJSON Polygon format
  1137.                             $coordArray $coordinatesData['coordinates'][0] ?? [];
  1138.                         } elseif (isset($coordinatesData['type']) && $coordinatesData['type'] === 'LineString' && isset($coordinatesData['coordinates'])) {
  1139.                             // GeoJSON LineString format
  1140.                             $coordArray $coordinatesData['coordinates'];
  1141.                         } elseif (is_array($coordinatesData) && isset($coordinatesData[0])) {
  1142.                             // Nested array format: [[[lng, lat], ...]] or [[lng, lat], ...]
  1143.                             if (is_array($coordinatesData[0]) && is_array($coordinatesData[0][0])) {
  1144.                                 // [[[lng, lat], ...]] - take first ring
  1145.                                 $coordArray $coordinatesData[0];
  1146.                             } elseif (is_array($coordinatesData[0]) && count($coordinatesData[0]) >= && is_numeric($coordinatesData[0][0])) {
  1147.                                 // [[lng, lat], ...]
  1148.                                 $coordArray $coordinatesData;
  1149.                             }
  1150.                         }
  1151.                         // Process coordinate array
  1152.                         if (!empty($coordArray)) {
  1153.                             $polygon '';
  1154.                             $firstCoordinate null;
  1155.                             foreach ($coordArray as $coord) {
  1156.                                 if (is_array($coord) && count($coord) >= 2) {
  1157.                                     $lng = (float)$coord[0];
  1158.                                     $lat = (float)$coord[1];
  1159.                                     // CAP polygon format: "lat,lng lat,lng ..." (note: lat first, then lng)
  1160.                                     // Format with 6 decimal places as per client requirement
  1161.                                     $coordString number_format($lat6'.''') . "," number_format($lng6'.''');
  1162.                                     if ($firstCoordinate === null) {
  1163.                                         $firstCoordinate $coordString;
  1164.                                     }
  1165.                                     $polygon .= $coordString ' ';
  1166.                                 }
  1167.                             }
  1168.                             // Remove the trailing space
  1169.                             $polygon rtrim($polygon);
  1170.                             // Validate and ensure polygon is closed
  1171.                             if (!empty(trim($polygon)) && $firstCoordinate !== null) {
  1172.                                 $polygonTrimmed trim($polygon);
  1173.                                 $coords explode(' '$polygonTrimmed);
  1174.                                 $uniqueCoords array_unique($coords);
  1175.                                 // Check if we have at least 4 distinct points
  1176.                                 if (count($uniqueCoords) >= 4) {
  1177.                                     // Ensure polygon is closed (first coordinate = last coordinate) - CAP compliance requirement
  1178.                                     $lastCoordinate end($coords);
  1179.                                     if ($lastCoordinate !== $firstCoordinate) {
  1180.                                         $polygon .= ' ' $firstCoordinate;
  1181.                                     }
  1182.                                     // Create a single polygon entry for fallback
  1183.                                     $governoratePolygons[] = [
  1184.                                         'governorate' => null,
  1185.                                         'nameEn' => null,
  1186.                                         'nameAr' => null,
  1187.                                         'polygon' => $polygon
  1188.                                     ];
  1189.                                 }
  1190.                             }
  1191.                         }
  1192.                     } else {
  1193.                         // Fallback: try to parse as string format "[[lat,lng],[lat,lng],...]"
  1194.                         $coordinatesString trim($coordinatesString'[]');
  1195.                         $coordinates explode("],["$coordinatesString);
  1196.                         $polygon '';
  1197.                         $firstCoordinate null;
  1198.                         foreach ($coordinates as $coordinate) {
  1199.                             $parts explode(","trim($coordinate'[]'));
  1200.                             if (count($parts) >= && is_numeric($parts[0]) && is_numeric($parts[1])) {
  1201.                                 $lat = (float)$parts[0];
  1202.                                 $lng = (float)$parts[1];
  1203.                                 // Format with 6 decimal places as per client requirement
  1204.                                 $coordString number_format($lat6'.''') . "," number_format($lng6'.''');
  1205.                                 if ($firstCoordinate === null) {
  1206.                                     $firstCoordinate $coordString;
  1207.                                 }
  1208.                                 $polygon .= $coordString ' ';
  1209.                             }
  1210.                         }
  1211.                         // Remove the trailing space and validate
  1212.                         $polygon rtrim($polygon);
  1213.                         if (!empty(trim($polygon)) && $firstCoordinate !== null) {
  1214.                             $polygonTrimmed trim($polygon);
  1215.                             $coords explode(' '$polygonTrimmed);
  1216.                             $uniqueCoords array_unique($coords);
  1217.                             if (count($uniqueCoords) >= 4) {
  1218.                                 $lastCoordinate end($coords);
  1219.                                 if ($lastCoordinate !== $firstCoordinate) {
  1220.                                     $polygon .= ' ' $firstCoordinate;
  1221.                                 }
  1222.                                 $governoratePolygons[] = [
  1223.                                     'governorate' => null,
  1224.                                     'nameEn' => null,
  1225.                                     'nameAr' => null,
  1226.                                     'polygon' => $polygon
  1227.                                 ];
  1228.                             }
  1229.                         }
  1230.                     }
  1231.                 }
  1232.             }
  1233.             // Create area elements - one for each governorate polygon (separate polygons, not merged)
  1234.             // Each governorate gets its own <area> element with its own <polygon>, even if from the same region
  1235.             if (!empty($governoratePolygons)) {
  1236.                 foreach ($governoratePolygons as $govPolygon) {
  1237.                     $area $doc->createElement('area');
  1238.                     $info->appendChild($area);
  1239.                     // Create areaDesc for this governorate (format: "Region region - GovernorateName")
  1240.                     $areaDescText '';
  1241.                     if ($lang == 'ar') {
  1242.                         $areaDescText "منطقة " $regionName;
  1243.                         if (!empty($govPolygon['nameAr'])) {
  1244.                             $areaDescText .= " - " $govPolygon['nameAr'];
  1245.                         }
  1246.                     } else {
  1247.                         $areaDescText $regionName " " $translator->trans("region");
  1248.                         if (!empty($govPolygon['nameEn'])) {
  1249.                             $areaDescText .= " - " $govPolygon['nameEn'];
  1250.                         }
  1251.                     }
  1252.                     // Note: Removed $notification->getMessage() to match the required format
  1253.                     $areaDesc $doc->createElement('areaDesc'$areaDescText ' : ' $translator->trans('The entire governorate'));
  1254.                     // Validate and create polygon element for this specific governorate
  1255.                     $polygon $govPolygon['polygon'];
  1256.                     if (!empty(trim($polygon))) {
  1257.                         $polygonTrimmed trim($polygon);
  1258.                         $coords explode(' '$polygonTrimmed);
  1259.                         $uniqueCoords array_unique($coords);
  1260.                         // Check if we have at least 4 distinct points
  1261.                         if (count($uniqueCoords) >= 4) {
  1262.                             // Ensure polygon is closed (first coordinate = last coordinate) - CAP compliance requirement
  1263.                             $firstCoord $coords[0];
  1264.                             $lastCoord end($coords);
  1265.                             if ($lastCoord !== $firstCoord) {
  1266.                                 $polygon .= ' ' $firstCoord;
  1267.                             }
  1268.                             $polygonElement $doc->createElement('polygon'$polygon);
  1269.                         } else {
  1270.                             // Not enough distinct points - create a bounding box for this specific governorate
  1271.                             // Use governorate-specific coordinates if available
  1272.                             $govSpecificCoords '';
  1273.                             if (is_object($govPolygon['governorate']) && method_exists($govPolygon['governorate'], 'getLatitude')) {
  1274.                                 $govLat $govPolygon['governorate']->getLatitude();
  1275.                                 $govLng $govPolygon['governorate']->getLongitude();
  1276.                                 if (is_numeric($govLat) && is_numeric($govLng)) {
  1277.                                     $govSpecificCoords number_format($govLat6'.''') . ',' number_format($govLng6'.''');
  1278.                                 }
  1279.                             }
  1280.                             $polygonElement $this->createFallbackPolygon($doc$notification$govSpecificCoords$timezone);
  1281.                         }
  1282.                     } else {
  1283.                         // No valid coordinates - create fallback polygon for this specific governorate
  1284.                         $govSpecificCoords '';
  1285.                         if (is_object($govPolygon['governorate']) && method_exists($govPolygon['governorate'], 'getLatitude')) {
  1286.                             $govLat $govPolygon['governorate']->getLatitude();
  1287.                             $govLng $govPolygon['governorate']->getLongitude();
  1288.                             if (is_numeric($govLat) && is_numeric($govLng)) {
  1289.                                 $govSpecificCoords number_format($govLat6'.''') . ',' number_format($govLng6'.''');
  1290.                             }
  1291.                         }
  1292.                         $polygonElement $this->createFallbackPolygon($doc$notification$govSpecificCoords$timezone);
  1293.                     }
  1294.                     $area->appendChild($areaDesc);
  1295.                     $area->appendChild($polygonElement);
  1296.                 }
  1297.             } else {
  1298.                 // No polygons found - create a single fallback area
  1299.                 $area $doc->createElement('area');
  1300.                 $info->appendChild($area);
  1301.                 // Create child elements for the <area> element
  1302.                 if ($lang == 'ar') {
  1303.                     $areaDescText "منطقة " $regionName;
  1304.                 } else {
  1305.                     $areaDescText $regionName " " $translator->trans("region");
  1306.                 }
  1307.                 if (!empty($governorateNames)) {
  1308.                     $areaDescText .= " " $translator->trans("including") . " " $governorateNames;
  1309.                 }
  1310.                 $areaDescText .= $notification->getMessage();
  1311.                 $areaDesc $doc->createElement('areaDesc'$areaDescText);
  1312.                 $polygonElement $this->createFallbackPolygon($doc$notification$governorateCoordinates$timezone);
  1313.                 $area->appendChild($areaDesc);
  1314.                 $area->appendChild($polygonElement);
  1315.             }
  1316.             // Add XML stylesheet reference (CAP compliance)
  1317.             $stylesheet $doc->createProcessingInstruction('xml-stylesheet''type="text/xsl" href="https://ncm.gov.sa/assets/styles/cap.xsl"');
  1318.             $doc->insertBefore($stylesheet$alert);
  1319.             // Add digital signature structure (placeholder - CAP compliance recommendation)
  1320.             // Note: This is a placeholder. For production, implement real XML digital signatures
  1321.             $dsSignature $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:Signature');
  1322.             // Create SignedInfo element
  1323.             $dsSignedInfo $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:SignedInfo');
  1324.             // CanonicalizationMethod
  1325.             $dsCanonicalizationMethod $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:CanonicalizationMethod');
  1326.             $dsCanonicalizationMethod->setAttribute('Algorithm''http://www.w3.org/2001/10/xml-exc-c14n#');
  1327.             $dsSignedInfo->appendChild($dsCanonicalizationMethod);
  1328.             // SignatureMethod
  1329.             $dsSignatureMethod $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:SignatureMethod');
  1330.             $dsSignatureMethod->setAttribute('Algorithm''http://www.w3.org/2001/04/xmldsig-more#rsa-sha256');
  1331.             $dsSignedInfo->appendChild($dsSignatureMethod);
  1332.             // Reference - Empty URI for enveloped signature (signs the parent document)
  1333.             $dsReference $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:Reference');
  1334.             $dsReference->setAttribute('URI''');
  1335.             // Transforms
  1336.             $dsTransforms $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:Transforms');
  1337.             $dsTransform $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:Transform');
  1338.             $dsTransform->setAttribute('Algorithm''http://www.w3.org/2000/09/xmldsig#enveloped-signature');
  1339.             $dsTransforms->appendChild($dsTransform);
  1340.             $dsReference->appendChild($dsTransforms);
  1341.             // DigestMethod
  1342.             $dsDigestMethod $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:DigestMethod');
  1343.             $dsDigestMethod->setAttribute('Algorithm''http://www.w3.org/2001/04/xmlenc#sha256');
  1344.             $dsReference->appendChild($dsDigestMethod);
  1345.             // DigestValue - Generate hash from notification data (placeholder)
  1346.             $digestData $notification->getGuid() . '_' $notification->getId() . '_' time();
  1347.             $digestHash hash('sha256'$digestDatatrue);
  1348.             $digestValue base64_encode($digestHash);
  1349.             $dsDigestValue $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:DigestValue'$digestValue);
  1350.             $dsReference->appendChild($dsDigestValue);
  1351.             $dsSignedInfo->appendChild($dsReference);
  1352.             $dsSignature->appendChild($dsSignedInfo);
  1353.             // SignatureValue - Generate base64-encoded signature-like value
  1354.             $signatureData $notification->getGuid() . '_' $notification->getId() . '_' time();
  1355.             $hash hash('sha256'$signatureDatatrue);
  1356.             // Generate additional random bytes to create a signature-like length (RSA-2048 signature is ~256 bytes)
  1357.             $randomBytes random_bytes(192); // 192 + 32 (sha256) = 224 bytes, base64 = ~300 chars
  1358.             $combinedBytes $hash $randomBytes;
  1359.             $uniqueSignatureValue base64_encode($combinedBytes);
  1360.             $dsSignatureValue $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:SignatureValue'$uniqueSignatureValue);
  1361.             $dsSignature->appendChild($dsSignatureValue);
  1362.             // KeyInfo
  1363.             $dsKeyInfo $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:KeyInfo');
  1364.             $dsKeyName $doc->createElementNS('http://www.w3.org/2000/09/xmldsig#''ds:KeyName''NCM-Public-Key');
  1365.             $dsKeyInfo->appendChild($dsKeyName);
  1366.             $dsSignature->appendChild($dsKeyInfo);
  1367.             $alert->appendChild($dsSignature);
  1368.             // Save the XML to a file or output it
  1369.             $xmlString $doc->saveXML();
  1370.             print $xmlString;
  1371.             exit;
  1372.             return $xmlString;
  1373.         } catch (\Exception $ex) {
  1374.             $result = ["success" => false"message" => $ex->getMessage()];
  1375.         }
  1376.         return $result;
  1377.     }
  1378.     /**
  1379.      * Get polygon coordinates from JSON lookup sheet for given governorates
  1380.      * 
  1381.      * This method validates and formats latitude/longitude coordinates to exactly 6 decimal places
  1382.      * as required by CAP specification. The JSON file contains the official polygon boundaries
  1383.      * for Saudi Arabia governorates, and coordinates are validated and standardized here.
  1384.      * 
  1385.      * Returns separate polygons for each governorate (not merged)
  1386.      * 
  1387.      * @param array $governorates Array of Governorate objects
  1388.      * @return array Array of arrays with 'governorate', 'nameEn', 'nameAr', and 'polygon' keys
  1389.      */
  1390.     private function getPolygonFromJsonSheet($governorates)
  1391.     {
  1392.         $governoratePolygons = [];
  1393.         // JSON file path containing validated polygon boundaries with coordinates
  1394.         // Use only ksa_governorate_boundaries.geojson file
  1395.         $jsonFilePath PIMCORE_PROJECT_ROOT '/public/import/ksa_governorate_boundaries.geojson';
  1396.         // Check if file exists
  1397.         if (!file_exists($jsonFilePath)) {
  1398.             error_log("getPolygonFromJsonSheet: JSON file not found at: " $jsonFilePath);
  1399.             return [];
  1400.         }
  1401.         // Load JSON file content
  1402.         $jsonContent file_get_contents($jsonFilePath);
  1403.         if ($jsonContent === false) {
  1404.             error_log("getPolygonFromJsonSheet: Failed to read JSON file");
  1405.             return [];
  1406.         }
  1407.         // Try to decode JSON with increased depth limit for large nested structures
  1408.         $jsonData json_decode($jsonContenttrue512JSON_BIGINT_AS_STRING);
  1409.         // If JSON decode fails, try alternative approach: extract coordinates directly from file
  1410.         if (json_last_error() !== JSON_ERROR_NONE) {
  1411.             $errorMsg "getPolygonFromJsonSheet: JSON decode error: " json_last_error_msg() . " (Error code: " json_last_error() . ")";
  1412.             error_log($errorMsg);
  1413.             error_log("getPolygonFromJsonSheet: File size: " strlen($jsonContent) . " bytes");
  1414.             // Fallback: Extract coordinates directly from file content using regex/string matching
  1415.             return $this->extractCoordinatesFromFileContent($jsonContent$governorates);
  1416.         }
  1417.         if (!isset($jsonData['features']) || !is_array($jsonData['features'])) {
  1418.             $availableKeys = isset($jsonData) ? implode(', 'array_keys($jsonData)) : 'null';
  1419.             error_log("getPolygonFromJsonSheet: JSON missing 'features' array. Available keys: " $availableKeys);
  1420.             error_log("getPolygonFromJsonSheet: JSON type: " gettype($jsonData));
  1421.             return [];
  1422.         }
  1423.         $debugMatches = [];
  1424.         $debugInfo null// Initialize debug info variable
  1425.         // Log total features in JSON for debugging
  1426.         error_log("getPolygonFromJsonSheet: Total features in JSON: " count($jsonData['features']));
  1427.         // Process each governorate separately to create individual polygons
  1428.         foreach ($governorates as $governorate) {
  1429.             // Handle both object and array cases
  1430.             if (is_object($governorate)) {
  1431.                 $governorateId method_exists($governorate'getGovernoteId') ? $governorate->getGovernoteId() : null;
  1432.                 $governorateNameEn method_exists($governorate'getName') ? $governorate->getName('en') : null;
  1433.                 $governorateNameAr method_exists($governorate'getName') ? $governorate->getName('ar') : null;
  1434.                 $governorateObj $governorate;
  1435.                 // Check if this governorate is a municipality and fetch parent governorate
  1436.                 if (method_exists($governorate'getIsMunicipality') && $governorate->getIsMunicipality()) {
  1437.                     $municipalityId method_exists($governorate'getMunicipalityID') ? $governorate->getMunicipalityID() : null;
  1438.                     if ($municipalityId !== null) {
  1439.                         try {
  1440.                             $municipality \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($municipalityIdtrue);
  1441.                             if ($municipality && $municipality->getGovernorate()) {
  1442.                                 $parentGovernorate $municipality->getGovernorate();
  1443.                                 // Use parent governorate's names for matching (English first, then Arabic)
  1444.                                 $governorateNameEn $parentGovernorate->getName('en');
  1445.                                 $governorateNameAr $parentGovernorate->getName('ar');
  1446.                                 // Also update the ID to parent governorate's ID for matching
  1447.                                 $governorateId $parentGovernorate->getGovernoteId();
  1448.                                 error_log("getPolygonFromJsonSheet: Municipality detected (ID: " $municipalityId "), using parent governorate - ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1449.                             }
  1450.                         } catch (\Exception $ex) {
  1451.                             error_log("getPolygonFromJsonSheet: Error fetching municipality (ID: " $municipalityId "): " $ex->getMessage());
  1452.                         }
  1453.                     }
  1454.                 }
  1455.             } elseif (is_array($governorate)) {
  1456.                 $governorateId $governorate['id'] ?? $governorate['governoteId'] ?? null;
  1457.                 $governorateNameEn $governorate['name_en'] ?? $governorate['name']['en'] ?? null;
  1458.                 $governorateNameAr $governorate['name_ar'] ?? $governorate['name']['ar'] ?? null;
  1459.                 $governorateObj $governorate;
  1460.                 // Check if this governorate is a municipality (array format)
  1461.                 if (isset($governorate['IsMunicipality']) && $governorate['IsMunicipality']) {
  1462.                     $municipalityId $governorate['MunicipalityID'] ?? null;
  1463.                     if ($municipalityId !== null) {
  1464.                         try {
  1465.                             $municipality \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($municipalityIdtrue);
  1466.                             if ($municipality && $municipality->getGovernorate()) {
  1467.                                 $parentGovernorate $municipality->getGovernorate();
  1468.                                 // Use parent governorate's names for matching (English first, then Arabic)
  1469.                                 $governorateNameEn $parentGovernorate->getName('en');
  1470.                                 $governorateNameAr $parentGovernorate->getName('ar');
  1471.                                 // Also update the ID to parent governorate's ID for matching
  1472.                                 $governorateId $parentGovernorate->getGovernoteId();
  1473.                                 error_log("getPolygonFromJsonSheet: Municipality detected (ID: " $municipalityId "), using parent governorate - ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1474.                             }
  1475.                         } catch (\Exception $ex) {
  1476.                             error_log("getPolygonFromJsonSheet: Error fetching municipality (ID: " $municipalityId "): " $ex->getMessage());
  1477.                         }
  1478.                     }
  1479.                 }
  1480.             } else {
  1481.                 error_log("getPolygonFromJsonSheet: Unexpected governorate type: " gettype($governorate));
  1482.                 continue;
  1483.             }
  1484.             // Log what we're looking for
  1485.             error_log("getPolygonFromJsonSheet: Searching for governorate - ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1486.             // Log sample features from JSON (only once) - showing REGION_N_1 field that we match against
  1487.             static $loggedSamples false;
  1488.             if (!$loggedSamples) {
  1489.                 $sampleFeatures = [];
  1490.                 foreach (array_slice($jsonData['features'], 010) as $sampleIdx => $sampleFeature) {
  1491.                     $sampleProps $sampleFeature['properties'] ?? [];
  1492.                     $sampleFeatures[] = [
  1493.                         'REGION_N_1' => $sampleProps['REGION_N_1'] ?? 'N/A',
  1494.                     ];
  1495.                 }
  1496.                 error_log("getPolygonFromJsonSheet: Sample REGION_N_1 values from JSON (first 10): " json_encode($sampleFeaturesJSON_UNESCAPED_UNICODE));
  1497.                 $loggedSamples true;
  1498.             }
  1499.             $matched false;
  1500.             $matchReason '';
  1501.             $matchAttempts = [];
  1502.             $foundAnyMatch false// Track if we found at least one match for this governorate
  1503.             // Find matching feature in JSON by matching governorate nameEn with REGION_N_1 field only
  1504.             foreach ($jsonData['features'] as $featureIndex => $feature) {
  1505.                 $matched false// Reset for each feature
  1506.                 if (!isset($feature['properties']) || !isset($feature['geometry'])) {
  1507.                     continue;
  1508.                 }
  1509.                 $properties $feature['properties'];
  1510.                 $geometry $feature['geometry'];
  1511.                 // Match by English name only (exact case-insensitive match - using REGION_N_1 field only)
  1512.                 if ($governorateNameEn !== null) {
  1513.                     $govNameEn trim((string)$governorateNameEn);
  1514.                     if (!empty($govNameEn) && isset($properties['REGION_N_1'])) {
  1515.                         // Normalize the governorate name (lowercase, normalize whitespace)
  1516.                         $govNameNormalized strtolower(preg_replace('/\s+/'' '$govNameEn));
  1517.                         $jsonNameEn trim((string)$properties['REGION_N_1']);
  1518.                         if (!empty($jsonNameEn)) {
  1519.                             $jsonNameNormalized strtolower(preg_replace('/\s+/'' '$jsonNameEn));
  1520.                             // Only exact case-insensitive match (normalized)
  1521.                             if ($jsonNameNormalized === $govNameNormalized) {
  1522.                                 $matched true;
  1523.                                 $matchReason 'Name_EN (exact via REGION_N_1): ' $governorateNameEn ' matches ' $jsonNameEn;
  1524.                             }
  1525.                         }
  1526.                     }
  1527.                 }
  1528.                 if ($matched && isset($geometry['type']) && isset($geometry['coordinates'])) {
  1529.                     error_log("getPolygonFromJsonSheet: Match found! Reason: " $matchReason ", Geometry type: " $geometry['type'] . ", ADMIN: " . ($properties['ADMIN'] ?? 'N/A'));
  1530.                     // Extract coordinates based on geometry type
  1531.                     $coords $this->extractCoordinatesFromGeometry($geometry);
  1532.                     error_log("getPolygonFromJsonSheet: Extracted " count($coords) . " coordinates");
  1533.                     if (!empty($coords)) {
  1534.                         // Format coordinates for this governorate separately
  1535.                         $polygonCoords $this->formatCoordinates($coords);
  1536.                         // Check if we already have a polygon for THIS SPECIFIC governorate
  1537.                         // IMPORTANT: We do NOT merge polygons from different features to avoid creating straight connecting lines.
  1538.                         // If a governorate appears in multiple features, we use only the first match to prevent invalid polygon connections.
  1539.                         $existingIndex null;
  1540.                         foreach ($governoratePolygons as $idx => $existingPoly) {
  1541.                             if (($existingPoly['governorate'] === $governorateObj) ||
  1542.                                 (is_object($governorateObj) && is_object($existingPoly['governorate']) &&
  1543.                                     method_exists($governorateObj'getGovernoteId') &&
  1544.                                     method_exists($existingPoly['governorate'], 'getGovernoteId') &&
  1545.                                     $governorateObj->getGovernoteId() === $existingPoly['governorate']->getGovernoteId())
  1546.                             ) {
  1547.                                 $existingIndex $idx;
  1548.                                 break;
  1549.                             }
  1550.                         }
  1551.                         if ($existingIndex !== null) {
  1552.                             // Governorate already has a polygon - skip this match to avoid creating straight connecting lines
  1553.                             // Using only the first match ensures clean polygon boundaries without invalid connections
  1554.                             error_log("getPolygonFromJsonSheet: Skipping duplicate match for governorate ID: " $governorateId " (already has polygon)");
  1555.                         } else {
  1556.                             // Store polygon with governorate info (separate polygon per governorate)
  1557.                             // Each governorate gets its own entry, even if multiple governorates are from the same region
  1558.                             $governoratePolygons[] = [
  1559.                                 'governorate' => $governorateObj,
  1560.                                 'nameEn' => $governorateNameEn,
  1561.                                 'nameAr' => $governorateNameAr,
  1562.                                 'polygon' => $polygonCoords
  1563.                             ];
  1564.                             error_log("getPolygonFromJsonSheet: Successfully added polygon for governorate ID: " $governorateId);
  1565.                         }
  1566.                         $matchDebug = [
  1567.                             'match_reason' => $matchReason,
  1568.                             'coords_count' => count($coords),
  1569.                             'geometry_type' => $geometry['type'],
  1570.                             'matched' => true
  1571.                         ];
  1572.                         $debugMatches[] = $matchDebug;
  1573.                         if ($debugInfo !== null$debugInfo['matches'][] = $matchDebug;
  1574.                         $foundAnyMatch true// Mark that we found at least one match
  1575.                         // Continue searching for more matching features (same governorate can have multiple regions)
  1576.                         // We'll merge all matching polygons for this governorate
  1577.                         continue;
  1578.                     } else {
  1579.                         $errorMsg "Matched but extracted 0 coordinates. Geometry type: " $geometry['type'] . ", Coordinates structure: " json_encode($geometry['coordinates']);
  1580.                         error_log("getPolygonFromJsonSheet: " $errorMsg);
  1581.                         if ($debugInfo !== null) {
  1582.                             $debugInfo['errors'][] = $errorMsg;
  1583.                             $debugInfo['matches'][] = ['match_reason' => $matchReason'matched' => true'coords_extracted' => 0];
  1584.                         }
  1585.                     }
  1586.                 }
  1587.             }
  1588.             if (!$foundAnyMatch) {
  1589.                 error_log("getPolygonFromJsonSheet: No match found for governorate ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1590.                 $noMatchDebug = [
  1591.                     'governorate_id' => $governorateId,
  1592.                     'governorate_id_type' => gettype($governorateId),
  1593.                     'name_en' => $governorateNameEn,
  1594.                     'name_ar' => $governorateNameAr,
  1595.                     'matched' => false,
  1596.                     'match_attempts' => array_slice($matchAttempts010// Limit to first 10 attempts
  1597.                 ];
  1598.                 $debugMatches[] = $noMatchDebug;
  1599.                 if ($debugInfo !== null$debugInfo['matches'][] = $noMatchDebug;
  1600.             }
  1601.         }
  1602.         // Log summary
  1603.         error_log("getPolygonFromJsonSheet: Processed " count($governorates) . " governorates, found " count($governoratePolygons) . " polygons");
  1604.         if (empty($governoratePolygons)) {
  1605.             // Log available REGION_N_1 values from JSON for debugging
  1606.             $availableRegionNames = [];
  1607.             foreach ($jsonData['features'] as $feature) {
  1608.                 $props $feature['properties'] ?? [];
  1609.                 $regionName $props['REGION_N_1'] ?? null;
  1610.                 if ($regionName !== null) {
  1611.                     $availableRegionNames[] = $regionName;
  1612.                 }
  1613.             }
  1614.             error_log("getPolygonFromJsonSheet: No coordinates found. Debug matches: " json_encode($debugMatches));
  1615.             error_log("getPolygonFromJsonSheet: Available REGION_N_1 values in JSON (first 20): " implode(', 'array_slice($availableRegionNames020)));
  1616.         } else {
  1617.             error_log("getPolygonFromJsonSheet: Successfully extracted polygons for " count($governoratePolygons) . " governorates");
  1618.         }
  1619.         return $governoratePolygons;
  1620.     }
  1621.     /**
  1622.      * Extract coordinates from GeoJSON geometry (supports Polygon and MultiPolygon)
  1623.      * 
  1624.      * @param array $geometry GeoJSON geometry object
  1625.      * @return array Array of coordinate pairs [lng, lat]
  1626.      */
  1627.     private function extractCoordinatesFromGeometry($geometry)
  1628.     {
  1629.         $coordinates = [];
  1630.         if (!isset($geometry['type']) || !isset($geometry['coordinates'])) {
  1631.             return $coordinates;
  1632.         }
  1633.         $type $geometry['type'];
  1634.         $coords $geometry['coordinates'];
  1635.         if ($type === 'Polygon' && is_array($coords)) {
  1636.             // Polygon: coordinates[0] is the outer ring
  1637.             if (isset($coords[0]) && is_array($coords[0])) {
  1638.                 foreach ($coords[0] as $coord) {
  1639.                     if (is_array($coord) && count($coord) >= 2) {
  1640.                         $coordinates[] = $coord;
  1641.                     }
  1642.                 }
  1643.             }
  1644.         } elseif ($type === 'MultiPolygon' && is_array($coords)) {
  1645.             // MultiPolygon: array of polygons, each polygon has coordinates[0] as outer ring
  1646.             // IMPORTANT: Use only the FIRST polygon to avoid creating straight connecting lines between disconnected parts
  1647.             // CAP XML polygons should represent a single continuous boundary
  1648.             if (!empty($coords) && is_array($coords[0]) && isset($coords[0][0]) && is_array($coords[0][0])) {
  1649.                 foreach ($coords[0][0] as $coord) {
  1650.                     if (is_array($coord) && count($coord) >= 2) {
  1651.                         $coordinates[] = $coord;
  1652.                     }
  1653.                 }
  1654.             }
  1655.         }
  1656.         return $coordinates;
  1657.     }
  1658.     /**
  1659.      * Extract coordinates directly from file content when JSON parsing fails
  1660.      * This is a fallback method that uses string/regex matching
  1661.      */
  1662.     private function extractCoordinatesFromFileContent($fileContent$governorates)
  1663.     {
  1664.         $governoratePolygons = [];
  1665.         foreach ($governorates as $governorate) {
  1666.             $governorateCoords = [];
  1667.             // Get governorate ID and names
  1668.             if (is_object($governorate)) {
  1669.                 $governorateId method_exists($governorate'getGovernoteId') ? $governorate->getGovernoteId() : null;
  1670.                 $governorateNameEn method_exists($governorate'getName') ? $governorate->getName('en') : null;
  1671.                 $governorateNameAr method_exists($governorate'getName') ? $governorate->getName('ar') : null;
  1672.                 $governorateObj $governorate;
  1673.                 // Check if this governorate is a municipality and fetch parent governorate
  1674.                 if (method_exists($governorate'getIsMunicipality') && $governorate->getIsMunicipality()) {
  1675.                     $municipalityId method_exists($governorate'getMunicipalityID') ? $governorate->getMunicipalityID() : null;
  1676.                     if ($municipalityId !== null) {
  1677.                         try {
  1678.                             $municipality \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($municipalityIdtrue);
  1679.                             if ($municipality && $municipality->getGovernorate()) {
  1680.                                 $parentGovernorate $municipality->getGovernorate();
  1681.                                 // Use parent governorate's names for matching (English first, then Arabic)
  1682.                                 $governorateNameEn $parentGovernorate->getName('en');
  1683.                                 $governorateNameAr $parentGovernorate->getName('ar');
  1684.                                 // Also update the ID to parent governorate's ID for matching
  1685.                                 $governorateId $parentGovernorate->getGovernoteId();
  1686.                                 error_log("extractCoordinatesFromFileContent: Municipality detected (ID: " $municipalityId "), using parent governorate - ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1687.                             }
  1688.                         } catch (\Exception $ex) {
  1689.                             error_log("extractCoordinatesFromFileContent: Error fetching municipality (ID: " $municipalityId "): " $ex->getMessage());
  1690.                         }
  1691.                     }
  1692.                 }
  1693.             } elseif (is_array($governorate)) {
  1694.                 $governorateId $governorate['id'] ?? $governorate['governoteId'] ?? null;
  1695.                 $governorateNameEn $governorate['name_en'] ?? $governorate['name']['en'] ?? null;
  1696.                 $governorateNameAr $governorate['name_ar'] ?? $governorate['name']['ar'] ?? null;
  1697.                 $governorateObj $governorate;
  1698.                 // Check if this governorate is a municipality (array format)
  1699.                 if (isset($governorate['IsMunicipality']) && $governorate['IsMunicipality']) {
  1700.                     $municipalityId $governorate['MunicipalityID'] ?? null;
  1701.                     if ($municipalityId !== null) {
  1702.                         try {
  1703.                             $municipality \Pimcore\Model\DataObject\Municipality::getByMunicipalityid($municipalityIdtrue);
  1704.                             if ($municipality && $municipality->getGovernorate()) {
  1705.                                 $parentGovernorate $municipality->getGovernorate();
  1706.                                 // Use parent governorate's names for matching (English first, then Arabic)
  1707.                                 $governorateNameEn $parentGovernorate->getName('en');
  1708.                                 $governorateNameAr $parentGovernorate->getName('ar');
  1709.                                 // Also update the ID to parent governorate's ID for matching
  1710.                                 $governorateId $parentGovernorate->getGovernoteId();
  1711.                                 error_log("extractCoordinatesFromFileContent: Municipality detected (ID: " $municipalityId "), using parent governorate - ID: " var_export($governorateIdtrue) . ", EN: " var_export($governorateNameEntrue) . ", AR: " var_export($governorateNameArtrue));
  1712.                             }
  1713.                         } catch (\Exception $ex) {
  1714.                             error_log("extractCoordinatesFromFileContent: Error fetching municipality (ID: " $municipalityId "): " $ex->getMessage());
  1715.                         }
  1716.                     }
  1717.                 }
  1718.             } else {
  1719.                 continue;
  1720.             }
  1721.             if ($governorateId === null) {
  1722.                 continue;
  1723.             }
  1724.             // Search for the governorate section in the file
  1725.             // Try multiple patterns for both old (GovID) and new (ADMIN) formats
  1726.             // Patterns: "GovID": "76", "GovID": 76, "ADMIN": 12, "ADMIN": "12"
  1727.             $idStr = (string)$governorateId;
  1728.             $patterns = [
  1729.                 '"ADMIN"\s*:\s*"' preg_quote($idStr'/') . '"',
  1730.                 '"ADMIN"\s*:\s*' preg_quote($idStr'/'),
  1731.                 '"GovID"\s*:\s*"' preg_quote($idStr'/') . '"',
  1732.                 '"GovID"\s*:\s*' preg_quote($idStr'/'),
  1733.             ];
  1734.             $found false;
  1735.             foreach ($patterns as $pattern) {
  1736.                 if (preg_match('/' $pattern '/'$fileContent$matchesPREG_OFFSET_CAPTURE)) {
  1737.                     $matchPos $matches[0][1];
  1738.                     $found true;
  1739.                     // Find the coordinates array that follows this ADMIN/GovID (within next 100000 chars)
  1740.                     $searchArea substr($fileContent$matchPos100000);
  1741.                     // Look for coordinates array: "coordinates": [[[lng, lat], ...]]
  1742.                     if (preg_match('/"coordinates"\s*:\s*\[/'$searchArea$coordMatchPREG_OFFSET_CAPTURE)) {
  1743.                         $coordStartInArea $coordMatch[0][1];
  1744.                         $coordStart $matchPos $coordStartInArea;
  1745.                         // Extract the coordinates array by finding matching brackets
  1746.                         $depth 0;
  1747.                         $startBracket false;
  1748.                         $coordString '';
  1749.                         $bracketCount 0;
  1750.                         // Find the opening bracket
  1751.                         for ($i $coordStart$i strlen($fileContent) && $i $coordStart 200000$i++) {
  1752.                             $char $fileContent[$i];
  1753.                             if ($char === '[') {
  1754.                                 if (!$startBracket) {
  1755.                                     $startBracket true;
  1756.                                 }
  1757.                                 $depth++;
  1758.                                 $bracketCount++;
  1759.                                 $coordString .= $char;
  1760.                             } elseif ($char === ']') {
  1761.                                 $depth--;
  1762.                                 $bracketCount++;
  1763.                                 $coordString .= $char;
  1764.                                 if ($depth === && $startBracket) {
  1765.                                     break; // Found complete coordinates array
  1766.                                 }
  1767.                             } elseif ($startBracket) {
  1768.                                 $coordString .= $char;
  1769.                             } elseif (preg_match('/\S/'$char)) {
  1770.                                 // Non-whitespace before bracket - might be start
  1771.                                 if ($char === '[') {
  1772.                                     $startBracket true;
  1773.                                     $depth 1;
  1774.                                     $coordString '[';
  1775.                                 }
  1776.                             }
  1777.                         }
  1778.                         // Extract coordinates using regex (more reliable than JSON parsing for malformed JSON)
  1779.                         // Pattern: [ lng, lat ] or [lng,lat] - matches coordinate pairs
  1780.                         $coordPattern '/\[\s*([+-]?\d+\.?\d*)\s*,\s*([+-]?\d+\.?\d*)\s*\]/';
  1781.                         preg_match_all($coordPattern$coordString$coordMatchesPREG_SET_ORDER);
  1782.                         if (!empty($coordMatches)) {
  1783.                             foreach ($coordMatches as $match) {
  1784.                                 $lng = (float)$match[1];
  1785.                                 $lat = (float)$match[2];
  1786.                                 // Validate ranges
  1787.                                 if ($lat >= -90 && $lat <= 90 && $lng >= -180 && $lng <= 180) {
  1788.                                     $governorateCoords[] = [$lng$lat];
  1789.                                 }
  1790.                             }
  1791.                         } else {
  1792.                             // Fallback: Try JSON decode if regex fails
  1793.                             $coordsArray json_decode($coordStringtrue);
  1794.                             if ($coordsArray !== null && is_array($coordsArray)) {
  1795.                                 $coords $this->extractCoordsFromArray($coordsArray);
  1796.                                 if (!empty($coords)) {
  1797.                                     $governorateCoords $coords;
  1798.                                 }
  1799.                             }
  1800.                         }
  1801.                         // Format coordinates for this governorate separately
  1802.                         if (!empty($governorateCoords)) {
  1803.                             $polygonCoords $this->formatCoordinates($governorateCoords);
  1804.                             $governoratePolygons[] = [
  1805.                                 'governorate' => $governorateObj,
  1806.                                 'nameEn' => $governorateNameEn,
  1807.                                 'nameAr' => $governorateNameAr,
  1808.                                 'polygon' => $polygonCoords
  1809.                             ];
  1810.                         }
  1811.                     }
  1812.                     break; // Found match, no need to try other patterns
  1813.                 }
  1814.             }
  1815.         }
  1816.         return $governoratePolygons;
  1817.     }
  1818.     /**
  1819.      * Extract coordinate pairs from nested array structure (Polygon/MultiPolygon)
  1820.      */
  1821.     private function extractCoordsFromArray($coordsArray)
  1822.     {
  1823.         $coordinates = [];
  1824.         if (!is_array($coordsArray)) {
  1825.             return $coordinates;
  1826.         }
  1827.         // Check if it's a coordinate pair [lng, lat]
  1828.         if (count($coordsArray) === && is_numeric($coordsArray[0]) && is_numeric($coordsArray[1])) {
  1829.             $coordinates[] = $coordsArray;
  1830.             return $coordinates;
  1831.         }
  1832.         // Recursively process nested arrays
  1833.         foreach ($coordsArray as $item) {
  1834.             if (is_array($item)) {
  1835.                 $subCoords $this->extractCoordsFromArray($item);
  1836.                 $coordinates array_merge($coordinates$subCoords);
  1837.             }
  1838.         }
  1839.         return $coordinates;
  1840.     }
  1841.     /**
  1842.      * Format coordinates for CAP format
  1843.      */
  1844.     private function formatCoordinates($allCoordinates)
  1845.     {
  1846.         $formattedCoords = [];
  1847.         foreach ($allCoordinates as $coord) {
  1848.             if (is_array($coord) && count($coord) >= 2) {
  1849.                 if (!is_numeric($coord[0]) || !is_numeric($coord[1])) {
  1850.                     continue;
  1851.                 }
  1852.                 $lng = (float)$coord[0];
  1853.                 $lat = (float)$coord[1];
  1854.                 // Validate ranges
  1855.                 if ($lat < -90 || $lat 90 || $lng < -180 || $lng 180) {
  1856.                     continue;
  1857.                 }
  1858.                 $formattedLat number_format($lat6'.''');
  1859.                 $formattedLng number_format($lng6'.''');
  1860.                 $formattedCoords[] = $formattedLat ',' $formattedLng;
  1861.             }
  1862.         }
  1863.         if (!empty($formattedCoords)) {
  1864.             $polygonCoords implode(' '$formattedCoords);
  1865.             // Ensure polygon is closed
  1866.             $coordsArray explode(' '$polygonCoords);
  1867.             $firstCoord $coordsArray[0];
  1868.             $lastCoord end($coordsArray);
  1869.             if ($firstCoord !== $lastCoord) {
  1870.                 $polygonCoords .= ' ' $firstCoord;
  1871.             }
  1872.             return $polygonCoords;
  1873.         }
  1874.         return '';
  1875.     }
  1876.     /**
  1877.      * Create a fallback polygon when coordinates are invalid or missing
  1878.      * Creates a bounding box from region/governorate coordinates
  1879.      */
  1880.     private function createFallbackPolygon($doc$notification$governorateCoordinates$timezone)
  1881.     {
  1882.         $polygonCoords '';
  1883.         // Try to use governorate coordinates first
  1884.         if (!empty($governorateCoordinates)) {
  1885.             $coords explode(' 'trim($governorateCoordinates));
  1886.             $validCoords = [];
  1887.             foreach ($coords as $coord) {
  1888.                 if (strpos($coord',') !== false) {
  1889.                     $parts explode(','$coord);
  1890.                     if (count($parts) == && is_numeric($parts[0]) && is_numeric($parts[1])) {
  1891.                         $validCoords[] = $coord;
  1892.                     }
  1893.                 }
  1894.             }
  1895.             if (count($validCoords) >= 4) {
  1896.                 // Use governorate coordinates
  1897.                 $polygonCoords implode(' '$validCoords);
  1898.                 // Close the polygon
  1899.                 if (!empty($validCoords)) {
  1900.                     $firstCoord $validCoords[0];
  1901.                     $lastCoord end($validCoords);
  1902.                     if ($firstCoord !== $lastCoord) {
  1903.                         $polygonCoords .= ' ' $firstCoord;
  1904.                     }
  1905.                 }
  1906.             }
  1907.         }
  1908.         // Fallback to region coordinates if governorate coordinates are insufficient
  1909.         if (empty($polygonCoords) && $notification->getRegion()) {
  1910.             $lat $notification->getRegion()->getLatitude();
  1911.             $lng $notification->getRegion()->getLongitude();
  1912.             if (is_numeric($lat) && is_numeric($lng)) {
  1913.                 // Create a small bounding box around the region center (0.1 degree radius)
  1914.                 $offset 0.1;
  1915.                 $coords = [
  1916.                     ($lat $offset) . ',' . ($lng $offset), // SW
  1917.                     ($lat $offset) . ',' . ($lng $offset), // SE
  1918.                     ($lat $offset) . ',' . ($lng $offset), // NE
  1919.                     ($lat $offset) . ',' . ($lng $offset), // NW
  1920.                     ($lat $offset) . ',' . ($lng $offset)  // Close polygon
  1921.                 ];
  1922.                 $polygonCoords implode(' '$coords);
  1923.             }
  1924.         }
  1925.         // Final fallback: use default coordinates if nothing else works
  1926.         if (empty($polygonCoords)) {
  1927.             // Default coordinates for Saudi Arabia center (Riyadh area)
  1928.             $defaultCoords = [
  1929.                 '24.5,46.5',  // SW
  1930.                 '24.5,46.7',  // SE
  1931.                 '24.7,46.7',  // NE
  1932.                 '24.7,46.5',  // NW
  1933.                 '24.5,46.5'   // Close polygon
  1934.             ];
  1935.             $polygonCoords implode(' '$defaultCoords);
  1936.         }
  1937.         return $doc->createElement('polygon'$polygonCoords);
  1938.     }
  1939.     public function viewNotification($params$translator): array
  1940.     {
  1941.         $decodedJwtToken $params['decodedJwtToken'] ?? null;
  1942.         $userPermission $params['userPermission'] ?? null;
  1943.         if (is_array($params['id'])) {
  1944.             $ids array_values(array_unique(array_filter($params['id'], static function ($v) {
  1945.                 return $v !== null && $v !== '' && $v !== false;
  1946.             })));
  1947.             if ($ids === []) {
  1948.                 return ['success' => false'message' => $translator->trans('ews_notification_does_not_exists')];
  1949.             }
  1950.             $items = [];
  1951.             foreach ($ids as $id) {
  1952.                 $viewNotification DataObject\EwsNotification::getById($idfalse);
  1953.                 if (!$viewNotification) {
  1954.                     $viewNotification DataObject\EwsNotification::getById($idtrue);
  1955.                 }
  1956.                 if ($viewNotification) {
  1957.                     $items[] = [
  1958.                         'id' => $id,
  1959.                         'success' => true,
  1960.                         'data' => $this->formatNotificationWithUpdateChanges($viewNotification$translator$decodedJwtToken$userPermission),
  1961.                         'history' => $this->getVersions($viewNotification$translator$decodedJwtToken$userPermission),
  1962.                     ];
  1963.                 } else {
  1964.                     $items[] = [
  1965.                         'id' => $id,
  1966.                         'success' => false,
  1967.                         'message' => $translator->trans('ews_notification_does_not_exists'),
  1968.                     ];
  1969.                 }
  1970.             }
  1971.             $anyOk false;
  1972.             foreach ($items as $row) {
  1973.                 if (!empty($row['success'])) {
  1974.                     $anyOk true;
  1975.                     break;
  1976.                 }
  1977.             }
  1978.             return $anyOk
  1979.                 ? ['success' => true'items' => $items]
  1980.                 : ['success' => false'message' => $translator->trans('ews_notification_does_not_exists'), 'items' => $items];
  1981.         }
  1982.         $viewNotification DataObject\EwsNotification::getById($params['id'], false);
  1983.         if (!$viewNotification) {
  1984.             $viewNotification DataObject\EwsNotification::getById($params['id'], true);
  1985.         }
  1986.         if ($viewNotification) {
  1987.             $notificationData $this->formatNotificationWithUpdateChanges($viewNotification$translator$decodedJwtToken$userPermission);
  1988.             $history $this->getVersions($viewNotification$translator$decodedJwtToken$userPermission);
  1989.             return ['success' => true'data' => $notificationData'history' => $history];
  1990.         }
  1991.         return ['success' => false'message' => $translator->trans('ews_notification_does_not_exists')];
  1992.     }
  1993.     public function searchEwsNotificationByRegion($regionId$translator)
  1994.     {
  1995.         $items = [];
  1996.         // The "region" field on EwsNotification is a manyToOneRelation, so the
  1997.         // magic getByRegion() expects an ElementInterface (or ['id','type'] array),
  1998.         // NOT the raw regionId integer that comes from the API request. We must
  1999.         // resolve the Region object first via its custom regionId field.
  2000.         $regionObj DataObject\Region::getByRegionId($regionId1);
  2001.         if (!$regionObj) {
  2002.             return ['success' => false'message' => $translator->trans('Region not found.')];
  2003.         }
  2004.         $currentTimestamp time();
  2005.         $ewsNotificationList DataObject\EwsNotification::getByRegion($regionObj);
  2006.         if (!empty($ewsNotificationList)) {
  2007.             $ewsNotificationList->addConditionParam("status != ?", ["ended"]);
  2008.             $ewsNotificationList->filterByEndDate($currentTimestamp">=");
  2009.             foreach ($ewsNotificationList as $ewsNotification) {
  2010.                 $items[] = $ewsNotification->getId();
  2011.             }
  2012.         }
  2013.         return ['success' => true'items' => $items];
  2014.     }
  2015.     
  2016.     public function addAddressComponentsFieldCollection($addressComponentsDataObject\EwsNotification $object)
  2017.     {
  2018.         $items = new \Pimcore\Model\DataObject\Fieldcollection();
  2019.         foreach ($addressComponents as $Data) {
  2020.             #creating field collections
  2021.             $item = new DataObject\Fieldcollection\Data\AddressComponents();
  2022.             $item->setAddressValue(strip_tags($Data['long_name']));
  2023.             $item->setAddressKey(strip_tags($Data['types'][0]));
  2024.             $items->add($item);
  2025.         }
  2026.         return $object->setAddressComponents($items);
  2027.     }
  2028.     public function getAlertAction($id)
  2029.     {
  2030.         $alertActionObj DataObject\AlertAction::getById($id);
  2031.         $data = [];
  2032.         if (!empty($alertActionObj)) {
  2033.             $data['id'] = $alertActionObj->getAlertActionId();
  2034.             $data['severity'] = $alertActionObj->getSeverity();
  2035.             $data['nameEn'] = $alertActionObj->getName('en');
  2036.             $data['nameAr'] = $alertActionObj->getName('ar');
  2037.         }
  2038.         return $data;
  2039.     }
  2040.     public function getAlertType($id)
  2041.     {
  2042.         $alertTypeObj DataObject\AlertType::getById($id);
  2043.         $data = [];
  2044.         if (!empty($alertTypeObj)) {
  2045.             $data['id'] = $alertTypeObj->getAlertTypeId();
  2046.             $data['nameEn'] = $alertTypeObj->getName('en');
  2047.             $data['nameAr'] = $alertTypeObj->getName('ar');
  2048.         }
  2049.         return $data;
  2050.     }
  2051.     public function getAlertStatus($id)
  2052.     {
  2053.         $alertStatus DataObject\AlertStatus::getById($id);
  2054.         $data = [];
  2055.         if (!empty($alertStatus)) {
  2056.             $alertType $alertStatus->getAlertType();
  2057.             $alertData = [];
  2058.             if (!empty($alertType)) {
  2059.                 $alertData = [
  2060.                     "id" => $alertType->getAlertTypeId(),
  2061.                     "nameEn" => $alertType->getName('en'),
  2062.                     "nameAr" => $alertType->getName('ar')
  2063.                 ];
  2064.             }
  2065.             $data['id'] = $alertStatus->getAlertStatusId();
  2066.             $data['nameEn'] = $alertStatus->getName('en');
  2067.             $data['nameAr'] = $alertStatus->getName('ar');
  2068.             $data['alertType'] = $alertData;
  2069.         }
  2070.         return $data;
  2071.     }
  2072.     public function getAlertHazard($id)
  2073.     {
  2074.         $alertHazardObj DataObject\AlertHazard::getById($id);
  2075.         $data = [];
  2076.         if (!empty($alertHazardObj)) {
  2077.             $data['id'] = $alertHazardObj->getAlertHazardId();
  2078.             $data['nameEn'] = $alertHazardObj->getName('en');
  2079.             $data['nameAr'] = $alertHazardObj->getName('ar');
  2080.         }
  2081.         return $data;
  2082.     }
  2083.     public function getRegion($id)
  2084.     {
  2085.         $regionObj DataObject\Region::getById($id);
  2086.         $data = [];
  2087.         if (!empty($regionObj)) {
  2088.             $data['id'] = $regionObj->getRegionId();
  2089.             $data['nameEn'] = $regionObj->getName('en');
  2090.             $data['nameAr'] = $regionObj->getName('ar');
  2091.             $data['longitude'] = $regionObj->getLongitude();
  2092.             $data['latitude'] = $regionObj->getLatitude();
  2093.         }
  2094.         return $data;
  2095.     }
  2096.     public function getEvent($id)
  2097.     {
  2098.         $eventObj DataObject\Event::getById($id);
  2099.         $data = [];
  2100.         if (!empty($eventObj)) {
  2101.             $data['id'] = $eventObj->getEventId();
  2102.             $data['nameEn'] = $eventObj->getName('en');
  2103.             $data['nameAr'] = $eventObj->getName('ar');
  2104.         }
  2105.         return $data;
  2106.     }
  2107.     public function getWeatherPhenomenon($id)
  2108.     {
  2109.         $weatherPhenObj DataObject\PhenomenaList::getById($id);
  2110.         $data = [];
  2111.         if (!empty($weatherPhenObj)) {
  2112.             $data['id'] = $weatherPhenObj->getPhenomenaListId();
  2113.             $data['nameEn'] = $weatherPhenObj->getTitle('en');
  2114.             $data['nameAr'] = $weatherPhenObj->getTitle('ar');
  2115.         }
  2116.         return $data;
  2117.     }
  2118.     public function getWeatherPhenAffect($id)
  2119.     {
  2120.         $weatherPhenObj DataObject\WeatherPhenomenonAffect::getById($id);
  2121.         $data = [];
  2122.         if (!empty($weatherPhenObj)) {
  2123.             $data['id'] = $weatherPhenObj->getWeatherPhenomenonAffectId();
  2124.             $data['nameEn'] = $weatherPhenObj->getName('en');
  2125.             $data['nameAr'] = $weatherPhenObj->getName('ar');
  2126.         }
  2127.         return $data;
  2128.     }
  2129.     // public function getGovernorateDetail($governorateList)
  2130.     // {
  2131.     //     $result = [];
  2132.     //     if (!empty($governorateList)) {
  2133.     //         for ($i = 0; $i < count($governorateList); $i++) {
  2134.     //             $parentGovernates = null;
  2135.     //             if ($governorateList[$i]->getIsMunicipality()) {
  2136.     //                 $municipatlity = \Pimcore\Model\DataObject\Municipality::getByMunicipalityId($governorateList[$i]->getMunicipalityID(), true);
  2137.     //                 if ($municipatlity &&  $municipatlity->getGovernorate()) {
  2138.     //                     $parentGovernates = $municipatlity->getGovernorate();
  2139.     //                 }
  2140.     //             }
  2141.     //             $result[$i]['id'] = $governorateList[$i]->getGovernoteId();
  2142.     //             $result[$i]['nameEn'] = $governorateList[$i]->getName('en');
  2143.     //             $result[$i]['nameAr'] = $governorateList[$i]->getName('ar');
  2144.     //             $result[$i]['longitude'] = $governorateList[$i]->getLongitude();
  2145.     //             $result[$i]['latitude'] = $governorateList[$i]->getLatitude();
  2146.     //             $result[$i]['parentId'] = $parentGovernates ? $parentGovernates->getGovernoteId() : null;
  2147.     //             $result[$i]['parenNameEn'] = $parentGovernates ? $parentGovernates->getName('en') : null;
  2148.     //             $result[$i]['parenNameAr'] = $parentGovernates ? $parentGovernates->getName('ar') : null;
  2149.     //             $result[$i]['parentLongitude'] = $parentGovernates ? $parentGovernates->getLongitude() : null;
  2150.     //             $result[$i]['parentLatitude'] = $parentGovernates ? $parentGovernates->getLatitude() : null;
  2151.     //             $result[$i]['isMunicipality'] = $governorateList[$i]->getIsMunicipality();
  2152.     //             $result[$i]['municipalities'] = [];
  2153.     //         }
  2154.     //     }
  2155.     //     return $result;
  2156.     // }
  2157.     public function getGovernorateDetail($governorateList)
  2158.     {
  2159.         $result = [];
  2160.         if (!empty($governorateList)) {
  2161.             $uniqueGovernorateArray = [];
  2162.             $isCapital = [];
  2163.             $hasCapital false;
  2164.             foreach ($governorateList as $governorate) {
  2165.                 $parentGovernates null;
  2166.                 // Check if it is a municipality and fetch parent governorate
  2167.                 if ($governorate->getIsMunicipality()) {
  2168.                     $municipality \Pimcore\Model\DataObject\Municipality::getByMunicipalityId($governorate->getMunicipalityID(), true);
  2169.                     if ($municipality && $municipality->getGovernorate()) {
  2170.                         $parentGovernates $municipality->getGovernorate();
  2171.                     }
  2172.                 }
  2173.                 // Initialize a unique governorate array if needed
  2174.                 $parentId $parentGovernates $parentGovernates->getGovernoteId() : $governorate->getGovernoteId();
  2175.                 if (!isset($uniqueGovernorateArray[$parentId])) {
  2176.                     $uniqueGovernorateArray[$parentId] = [
  2177.                         'id' => $parentId,
  2178.                         'nameEn' => $parentGovernates $parentGovernates->getName('en') : $governorate->getName('en'),
  2179.                         'nameAr' => $parentGovernates $parentGovernates->getName('ar') : $governorate->getName('ar'),
  2180.                         'longitude' => $parentGovernates $parentGovernates->getLongitude() : $governorate->getLongitude(),
  2181.                         'latitude' => $parentGovernates $parentGovernates->getLatitude() : $governorate->getLatitude(),
  2182.                         'municipalities' => []
  2183.                     ];
  2184.                     $gov $parentGovernates ?: \Pimcore\Model\DataObject\Governorate::getByGovernoteId($parentIdtrue) ?: $governorate;
  2185.                     $isCapital[$parentId] = method_exists($gov'getIsCapital') ? (bool) $gov->getIsCapital() : false;
  2186.                 }
  2187.                 // Add municipality details if it is a municipality
  2188.                 if ($governorate->getIsMunicipality()) {
  2189.                     $uniqueGovernorateArray[$parentId]['municipalities'][] = [
  2190.                         'id' => $governorate->getGovernoteId(),
  2191.                         'nameEn' => $governorate->getName('en'),
  2192.                         'nameAr' => $governorate->getName('ar'),
  2193.                         'longitude' => $governorate->getLongitude(),
  2194.                         'latitude' => $governorate->getLatitude()
  2195.                     ];
  2196.                 }
  2197.             }
  2198.             // Flatten the associative array to a numerical array for output
  2199.             $result array_values($uniqueGovernorateArray);
  2200.             if (in_array(true$isCapitaltrue)) {
  2201.                 foreach ($result as $i => &$row) {
  2202.                     $row['_'] = $i;
  2203.                 }
  2204.                 unset($row);
  2205.                 usort($result, static function ($a$b) use ($isCapital) {
  2206.                     $c = (int) !empty($isCapital[$b['id']]) - (int) !empty($isCapital[$a['id']]);
  2207.                     return $c ?: ($a['_'] <=> $b['_']);
  2208.                 });
  2209.                 foreach ($result as &$row) {
  2210.                     unset($row['_']);
  2211.                 }
  2212.                 unset($row);
  2213.             }
  2214.         }
  2215.         return $result;
  2216.     }
  2217.     public function getMessageDetail($notificationObj)
  2218.     {
  2219.         $data = [];
  2220.         if (!empty($notificationObj)) {
  2221.             $data['messageEn'] = $notificationObj->getMessage('en');
  2222.             $data['messageAr'] = $notificationObj->getMessage('ar');
  2223.         }
  2224.         return $data;
  2225.     }
  2226.     public function createAsset($fileData$filename)
  2227.     {
  2228.         // Validate the file data
  2229.         if (preg_match('/^data:(image\/(png|jpe?g)|application\/(pdf|vnd.openxmlformats-officedocument.wordprocessingml.document|vnd.ms-excel));base64,/'$fileData) !== 1) {
  2230.             return false// Invalid file data or MIME type
  2231.         }
  2232.         // Extract the file extension from the MIME type
  2233.         $extension '';
  2234.         if (preg_match('/^data:image\/(png|jpe?g);base64,/'$fileData)) {
  2235.             $extension 'jpg'// Assume JPG for base64-encoded image data (PNG or JPEG)
  2236.         } elseif (preg_match('/^data:application\/(pdf|vnd.openxmlformats-officedocument.wordprocessingml.document|vnd.ms-excel);base64,/'$fileData)) {
  2237.             $extension 'pdf'// PDF, DOCX, or Excel
  2238.         }
  2239.         // Remove the "data:image/png;base64," or "data:application/pdf;base64," prefix to get the actual base64-encoded content
  2240.         $base64Content preg_replace('/^data:(image\/png|application\/(pdf|vnd.openxmlformats-officedocument.wordprocessingml.document|vnd.ms-excel));base64,/'''$fileData);
  2241.         // Decode the base64-encoded content
  2242.         $fileContent base64_decode($base64Content);
  2243.         // Validate the decoded content
  2244.         if ($fileContent === false) {
  2245.             return false// Invalid base64-encoded content
  2246.         }
  2247.         // Create the Pimcore asset
  2248.         $asset = new \Pimcore\Model\Asset();
  2249.         $parent Asset\Service::createFolderByPath('/EWSNotification');
  2250.         $asset->setFilename($filename); // Replace with the desired filename and extension
  2251.         $asset->setParent($parent);
  2252.         $asset->setData($fileContent);
  2253.         // Set the MIME type based on the file extension
  2254.         $mimeType '';
  2255.         if ($extension === 'pdf') {
  2256.             $mimeType 'application/pdf';
  2257.         } elseif ($extension === 'jpg') {
  2258.             $mimeType 'image/jpeg';
  2259.         } elseif ($extension === 'png') {
  2260.             $mimeType 'image/png';
  2261.         } elseif ($extension === 'docx') {
  2262.             $mimeType 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
  2263.         } elseif ($extension === 'xlsx') {
  2264.             $mimeType 'application/vnd.ms-excel';
  2265.         }
  2266.         // Validate the MIME type
  2267.         if (!in_array($mimeType, ['image/jpeg''image/png''application/pdf''application/vnd.openxmlformats-officedocument.wordprocessingml.document''application/vnd.ms-excel'])) {
  2268.             return false// Invalid MIME type
  2269.         }
  2270.         $asset->setType($mimeType);
  2271.         // Save the asset
  2272.         $asset->save();
  2273.         return $asset;
  2274.     }
  2275.     public function getPhenomenaListByAlertId($alertID$lang "en")
  2276.     {
  2277.         $response = [];
  2278.         $alertType \Pimcore\Model\DataObject\AlertType::getByAlertTypeId($alertIDtrue);
  2279.         if ($alertType instanceof \Pimcore\Model\DataObject\AlertType) {
  2280.             $phenomenaList $alertType->getPhenomenaList();
  2281.             if ($phenomenaList) {
  2282.                 foreach ($phenomenaList as $phenomena) {
  2283.                     $phenomenaId $phenomena->getPhenomena();
  2284.                     if ($phenomenaId) {
  2285.                         $phenomenaObj \Pimcore\Model\DataObject::getById($phenomenaId);
  2286.                         $response[] = ["id" => $phenomenaObj->getphenomenaListId(), "nameEn" => $phenomenaObj->getTitle("en"), "nameAr" => $phenomenaObj->getTitle("ar"), "criteria" => $phenomena->getCriteria()];
  2287.                     }
  2288.                 }
  2289.             } else {
  2290.                 return ["success" => false"message" => "No criteria is set"];
  2291.             }
  2292.         } else {
  2293.             return ["success" => false"message" => "Invalid alert type id"];
  2294.         }
  2295.         return ["success" => true"data" => $response];
  2296.     }
  2297.     public function getPhenomenaListByAlertIds($alertIDs$lang "en")
  2298.     {
  2299.         $response = [];
  2300.         if (count($alertIDs) > 0) {
  2301.             foreach ($alertIDs as $alertID) {
  2302.                 $alertType \Pimcore\Model\DataObject\AlertType::getByAlertTypeId($alertIDtrue);
  2303.                 if ($alertType instanceof \Pimcore\Model\DataObject\AlertType) {
  2304.                     $phenomenaList $alertType->getPhenomenaList();
  2305.                     if ($phenomenaList) {
  2306.                         foreach ($phenomenaList as $phenomena) {
  2307.                             $phenomenaId $phenomena->getPhenomena();
  2308.                             if ($phenomenaId) {
  2309.                                 $phenomenaObj \Pimcore\Model\DataObject::getById($phenomenaId);
  2310.                                 if ($phenomenaObj instanceof \Pimcore\Model\DataObject) {
  2311.                                     $currentId $phenomenaObj->getphenomenaListId();
  2312.                                     $existingIds array_column($response'id');
  2313.                                     if (!in_array($currentId$existingIds)) {
  2314.                                         $response[] = [
  2315.                                             "id" => $currentId,
  2316.                                             "nameEn" => $phenomenaObj->getTitle("en"),
  2317.                                             "nameAr" => $phenomenaObj->getTitle("ar"),
  2318.                                             "criteria" => $phenomena->getCriteria()
  2319.                                         ];
  2320.                                     } else {
  2321.                                         $index array_search($currentId$existingIds);
  2322.                                         $response[$index]['criteria'] .= ', ' $phenomena->getCriteria();
  2323.                                     }
  2324.                                 }
  2325.                             }
  2326.                         }
  2327.                     } else {
  2328.                         return ["success" => false"message" => "No criteria is set"];
  2329.                     }
  2330.                 }
  2331.             }
  2332.             return ["success" => true"data" => $response];
  2333.         }
  2334.         return ["success" => false"message" => "Invalid alert type id"];
  2335.     }
  2336.     public function getAlertActions()
  2337.     {
  2338.         $response = [];
  2339.         $alertActions = new DataObject\AlertAction\Listing();
  2340.         $alertActions $alertActions->load();
  2341.         if ($alertActions) {
  2342.             foreach ($alertActions as $alertAction) {
  2343.                 $response[] = [
  2344.                     "id" => $alertAction->getAlertActionId(),
  2345.                     "nameEn" => $alertAction->getName('en'),
  2346.                     "nameAr" => $alertAction->getName('ar'),
  2347.                     "severity" => $alertAction->getSeverity()
  2348.                 ];
  2349.             }
  2350.         }
  2351.         return ["success" => true"data" => $response];
  2352.     }
  2353.     public function getAlertHazards()
  2354.     {
  2355.         $response = [];
  2356.         $alertHazards = new DataObject\AlertHazard\Listing();
  2357.         $alertHazards $alertHazards->load();
  2358.         if ($alertHazards) {
  2359.             foreach ($alertHazards as $alertHazard) {
  2360.                 $response[] = [
  2361.                     "id" => $alertHazard->getalertHazardId(),
  2362.                     "nameEn" => $alertHazard->getName('en'),
  2363.                     "nameAr" => $alertHazard->getName('ar'),
  2364.                     "OrderId" => $alertHazard->getOrderId()
  2365.                 ];
  2366.             }
  2367.         }
  2368.         return ["success" => true"data" => $response];
  2369.     }
  2370.     public function getAlertTypes()
  2371.     {
  2372.         $response = [];
  2373.         $alertTypes = new DataObject\AlertType\Listing();
  2374.         $alertTypes $alertTypes->load();
  2375.         if ($alertTypes) {
  2376.             foreach ($alertTypes as $alertType) {
  2377.                 $response[] = [
  2378.                     "id" => $alertType->getAlertTypeId(),
  2379.                     "color" => $alertType->getColor(),
  2380.                     "nameEn" => $alertType->getName('en'),
  2381.                     "nameAr" => $alertType->getName('ar')
  2382.                 ];
  2383.             }
  2384.         }
  2385.         return ["success" => true"data" => $response];
  2386.     }
  2387.     public function getEvents()
  2388.     {
  2389.         $response = [];
  2390.         $events = new DataObject\Event\Listing();
  2391.         $events $events->load();
  2392.         if ($events) {
  2393.             foreach ($events as $event) {
  2394.                 $response[] = [
  2395.                     "id" => $event->getEventId(),
  2396.                     "nameEn" => $event->getName('en'),
  2397.                     "nameAr" => $event->getName('ar')
  2398.                 ];
  2399.             }
  2400.         }
  2401.         return ["success" => true"data" => $response];
  2402.     }
  2403.     public function getWeatherPhenomenones()
  2404.     {
  2405.         $response = [];
  2406.         $weatherPhenomenons = new DataObject\PhenomenaList\Listing();
  2407.         $weatherPhenomenons $weatherPhenomenons->load();
  2408.         if ($weatherPhenomenons) {
  2409.             foreach ($weatherPhenomenons as $weatherPhenomenon) {
  2410.                 $response[] = [
  2411.                     "id" => $weatherPhenomenon->getPhenomenaListId(),
  2412.                     "nameEn" => $weatherPhenomenon->getTitle('en'),
  2413.                     "nameAr" => $weatherPhenomenon->getTitle('ar')
  2414.                 ];
  2415.             }
  2416.         }
  2417.         return ["success" => true"data" => $response];
  2418.     }
  2419.     public function getWeatherPhenomenonAffect()
  2420.     {
  2421.         $response = [];
  2422.         $getWeatherPhenomAffects = new DataObject\WeatherPhenomenonAffect\Listing();
  2423.         $getWeatherPhenomAffects $getWeatherPhenomAffects->load();
  2424.         if ($getWeatherPhenomAffects) {
  2425.             foreach ($getWeatherPhenomAffects as $getWeatherPhenomAffect) {
  2426.                 $response[] = [
  2427.                     "id" => $getWeatherPhenomAffect->getWeatherPhenomenonAffectId(),
  2428.                     "nameEn" => $getWeatherPhenomAffect->getName('en'),
  2429.                     "nameAr" => $getWeatherPhenomAffect->getName('ar')
  2430.                 ];
  2431.             }
  2432.         }
  2433.         return ["success" => true"data" => $response];
  2434.     }
  2435.     public function getAlertStatuses($params)
  2436.     {
  2437.         $response = [];
  2438.         $alertStatuss = new DataObject\AlertStatus\Listing();
  2439.         // Handle search by name
  2440.         if (isset($params['search']) && !empty($params['search'])) {
  2441.             $alertStatuss->addConditionParam("name LIKE ?""%" $params['search'] . "%");
  2442.         }
  2443.         // Handle alert type filter
  2444.         if (isset($params['alert_id']) && !empty($params['alert_id'])) {
  2445.             $alertTypeIds = [];
  2446.             $alertTypeList = new DataObject\AlertType\Listing();
  2447.             $alertTypeList->addConditionParam("alertTypeId IN (?)", [$params['alert_id']]);
  2448.             foreach ($alertTypeList as $alertType) {
  2449.                 $alertTypeIds[] = $alertType->getId();
  2450.             }
  2451.             $alertStatuss->addConditionParam("alertType__id IN (?)", [$alertTypeIds]);
  2452.         }
  2453.         // Check for regionId array and isLandLocked logic
  2454.         $excludeWavesRising false;
  2455.         if (isset($params['regionId']) && is_array($params['regionId']) && count($params['regionId']) > 0) {
  2456.             foreach ($params['regionId'] as $regionId) {
  2457.                 $region \Pimcore\Model\DataObject\Region::getByRegionId($regionIdtrue);
  2458.                 if ($region && $region->getIsLandLocked()) {
  2459.                     $excludeWavesRising true;
  2460.                     break;
  2461.                 }
  2462.             }
  2463.         }
  2464.         $alertStatuss $alertStatuss->load();
  2465.         if ($alertStatuss) {
  2466.             foreach ($alertStatuss as $alertStatus) {
  2467.                 // Exclude "Waves rising" if needed
  2468.                 if ($excludeWavesRising && ((strtolower(trim($alertStatus->getName('en'))) === 'waves rising'))) {
  2469.                     continue;
  2470.                 }
  2471.                 $alertType $alertStatus->getAlertType();
  2472.                 $alertData = [];
  2473.                 if (!empty($alertType)) {
  2474.                     $alertData = [
  2475.                         "id" => $alertType->getAlertTypeId(),
  2476.                         "nameEn" => $alertType->getName('en'),
  2477.                         "nameAr" => $alertType->getName('ar')
  2478.                     ];
  2479.                 }
  2480.                 $response[] = [
  2481.                     "id" => $alertStatus->getalertStatusId(),
  2482.                     "nameEn" => $alertStatus->getName('en'),
  2483.                     "nameAr" => $alertStatus->getName('ar'),
  2484.                     "imageTemplate" => $alertStatus->getimageTemplate(),
  2485.                     "alertType" => $alertData
  2486.                 ];
  2487.             }
  2488.         }
  2489.         return ["success" => true"data" => $response];
  2490.     }
  2491.     public function getGovernorates()
  2492.     {
  2493.         $response = [];
  2494.         $governorates = new DataObject\Governorate\Listing();
  2495.         $Governorates $governorates->load();
  2496.         if ($governorates) {
  2497.             foreach ($governorates as $governorate) {
  2498.                 $region $governorate->getregionId();
  2499.                 $regionData = [];
  2500.                 if (!empty($region)) {
  2501.                     $regionData = [
  2502.                         "id" => $region->getRegionId(),
  2503.                         "nameEn" => $region->getName('en'),
  2504.                         "nameAr" => $region->getName('ar'),
  2505.                         "longitude" => $region->getLongitude(),
  2506.                         "latitude" => $region->getLongitude()
  2507.                     ];
  2508.                 }
  2509.                 $response[] = [
  2510.                     "id" => $governorate->getgovernoteId(),
  2511.                     "nameEn" => $governorate->getName('en'),
  2512.                     "nameAr" => $governorate->getName('ar'),
  2513.                     "regionId" => $regionData,
  2514.                     "longitude" => $governorate->getLongitude(),
  2515.                     "latitude" => $governorate->getLatitude(),
  2516.                     "isHidden" => $governorate->getisHidden(),
  2517.                     "IsMunicipality" => $governorate->getIsMunicipality(),
  2518.                     "MunicipalityID" => $governorate->getMunicipalityID()
  2519.                 ];
  2520.             }
  2521.         }
  2522.         return ["success" => true"data" => $response];
  2523.     }
  2524.     public function getMunicipality($governorateId null$lang 'en')
  2525.     {
  2526.         $response = [];
  2527.         $municipalities = new DataObject\Municipality\Listing();
  2528.         if (is_array($governorateId)) {
  2529.             $govIdsArr = [];
  2530.             foreach ($governorateId as $govId) {
  2531.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($govIdtrue);
  2532.                 if ($governorate) {
  2533.                     array_push($govIdsArr$governorate->getId());
  2534.                 }
  2535.             }
  2536.             $municipalities->setCondition("governorate__id IN (" implode(", "$govIdsArr) . ")");
  2537.         } else {
  2538.             if ($governorateId) {
  2539.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($governorateIdtrue);
  2540.                 $municipalities->setCondition("governorate__id = ?", [$governorate->getId()]);
  2541.             }
  2542.         }
  2543.         $municipalities $municipalities->load();
  2544.         if ($municipalities) {
  2545.             foreach ($municipalities as $municipality) {
  2546.                 $response[] = [
  2547.                     "id" => $municipality->getMunicipalityid(),
  2548.                     "nameEn" => $municipality->getName('en'),
  2549.                     "nameAr" => $municipality->getName('ar'),
  2550.                     "longitude" => $municipality->getLongitude(),
  2551.                     "latitude" => $municipality->getLatitude(),
  2552.                     "governate" => $municipality->getGovernorate()->getGovernoteId()
  2553.                 ];
  2554.             }
  2555.         }
  2556.         // Determine sorting field based on language
  2557.         $sortField 'nameEn'// Default sorting by English
  2558.         if (isset($lang) && strtolower($lang) === 'ar') {
  2559.             $sortField 'nameAr'// Sorting by Arabic
  2560.         }
  2561.         // Sort manually using usort()
  2562.         usort($response, function ($a$b) use ($sortField) {
  2563.             return strcmp($a[$sortField], $b[$sortField]);
  2564.         });
  2565.         return ["success" => true"data" => $response];
  2566.     }
  2567.     public function getGovernoratesByRegion($params)
  2568.     {
  2569.         $response = [];
  2570.         $governorates = new DataObject\Governorate\Listing();
  2571.         if (isset($params['region_id'])) {
  2572.             $region \Pimcore\Model\DataObject\Region::getByRegionId($params['region_id'], true);
  2573.             if (!$region) {
  2574.                 throw new \Exception("Region not available");
  2575.             }
  2576.             $governorates->filterByRegionId($region);
  2577.         }
  2578.         if (isset($params['region_ids']) && !empty($params['region_ids'])) {
  2579.             $regionIds = [];
  2580.             $regionList = new DataObject\Region\Listing();
  2581.             $regionList->addConditionParam("regionId IN (?)", [$params['region_ids']]);
  2582.             foreach ($regionList as $region) {
  2583.                 $regionIds[] = $region->getId();
  2584.             }
  2585.             $governorates->addConditionParam("regionId__id IN (?)", [$regionIds]);
  2586.         }
  2587.         // Load governorates without sorting in Pimcore
  2588.         $governorates $governorates->load();
  2589.         // Convert to array for manual sorting
  2590.         $count 0;
  2591.         if ($governorates) {
  2592.             foreach ($governorates as $governorate) {
  2593.                 $region $governorate->getregionId();
  2594.                 $regionData = [];
  2595.                 if (!empty($region)) {
  2596.                     $regionData = [
  2597.                         "id" => $region->getRegionId(),
  2598.                         "nameEn" => $region->getName('en'),
  2599.                         "nameAr" => $region->getName('ar'),
  2600.                         "longitude" => $region->getLongitude(),
  2601.                         "latitude" => $region->getLatitude()
  2602.                     ];
  2603.                 }
  2604.                 $response[$count] = [
  2605.                     "id" => $governorate->getgovernoteId(),
  2606.                     "nameEn" => $governorate->getName('en'),
  2607.                     "nameAr" => $governorate->getName('ar'),
  2608.                     "regionId" => $regionData,
  2609.                     "longitude" => $governorate->getLongitude(),
  2610.                     "latitude" => $governorate->getLatitude(),
  2611.                     "isHidden" => $governorate->getisHidden(),
  2612.                     "IsMunicipality" => $governorate->getIsMunicipality(),
  2613.                     "MunicipalityID" => $governorate->getMunicipalityID(),
  2614.                     "bbox" => $governorate->getBbox()
  2615.                 ];
  2616.                 if ($governorate->getMunicipalityID() && $governorate->getIsMunicipality()) {
  2617.                     $municipality \Pimcore\Model\DataObject\Municipality::getBymunicipalityid($governorate->getMunicipalityID(), true);
  2618.                     if ($municipality) {
  2619.                         $muniGovernate $municipality->getgovernorate();
  2620.                         if ($muniGovernate) {
  2621.                             $response[$count]["parent_id"] = $muniGovernate->getgovernoteId();
  2622.                             $response[$count]["parent_longitude"] = $muniGovernate->getLongitude();
  2623.                             $response[$count]["parent_latitude"] = $muniGovernate->getLatitude();
  2624.                             $response[$count]["parent_name_en"] = $muniGovernate->getName("en");
  2625.                             $response[$count]["parent_name_ar"] = $muniGovernate->getName("ar");
  2626.                         }
  2627.                     }
  2628.                 }
  2629.                 $count++;
  2630.             }
  2631.         }
  2632.         // Determine sorting field based on language
  2633.         $sortField 'nameEn'// Default sorting by English
  2634.         if (isset($params['lang']) && strtolower($params['lang']) === 'ar') {
  2635.             $sortField 'nameAr'// Sorting by Arabic
  2636.         }
  2637.         // Sort manually using usort()
  2638.         usort($response, function ($a$b) use ($sortField) {
  2639.             return strcmp($a[$sortField], $b[$sortField]);
  2640.         });
  2641.         return ["success" => true"data" => $response];
  2642.     }
  2643.     public function getVersions($notificationObject$translator$decodedJwtToken null$userPermission null)
  2644.     {
  2645.         $snapshots $this->collectUpdateVersionSnapshots($notificationObject);
  2646.         $resultAscending = [];
  2647.         foreach ($snapshots as $index => $snap) {
  2648.             /** @var EwsNotification $viewNotification */
  2649.             $viewNotification $snap['obj'];
  2650.             $previous $index $snapshots[$index 1]['obj'] : null;
  2651.             $formatted $this->createNotificationFormat($viewNotification$translator$decodedJwtToken$userPermission);
  2652.             $formatted['updateChanges'] = $this->buildNotificationUpdateChanges(
  2653.                 $viewNotification,
  2654.                 $previous instanceof EwsNotification $previous null,
  2655.                 $translator
  2656.             );
  2657.             $resultAscending[] = $formatted;
  2658.         }
  2659.         // Newest first (matches Pimcore getVersions() iteration order).
  2660.         return array_reverse($resultAscending);
  2661.     }
  2662.     /**
  2663.      * Format live notification payload and attach updateChanges vs previous Update version.
  2664.      */
  2665.     private function formatNotificationWithUpdateChanges(
  2666.         EwsNotification $notification,
  2667.         $translator,
  2668.         $decodedJwtToken null,
  2669.         $userPermission null
  2670.     ): array {
  2671.         $formatted $this->createNotificationFormat($notification$translator$decodedJwtToken$userPermission);
  2672.         $formatted['updateChanges'] = $this->buildNotificationUpdateChanges(
  2673.             $notification,
  2674.             $this->resolvePreviousUpdateForCurrent($notification),
  2675.             $translator
  2676.         );
  2677.         return $formatted;
  2678.     }
  2679.     /**
  2680.      * Collect Update-note version snapshots oldest → newest.
  2681.      *
  2682.      * @return array<int, array{ts: int, obj: EwsNotification}>
  2683.      */
  2684.     private function collectUpdateVersionSnapshots(EwsNotification $notification): array
  2685.     {
  2686.         $snapshots = [];
  2687.         $versions $notification->getVersions() ?: [];
  2688.         foreach ($versions as $version) {
  2689.             if ($version->getNote() !== 'Update') {
  2690.                 continue;
  2691.             }
  2692.             $view $version->loadData();
  2693.             if (!$view instanceof EwsNotification) {
  2694.                 continue;
  2695.             }
  2696.             $snapshots[] = [
  2697.                 'ts' => (int) ($version->getDate() ?: $view->getModificationDate() ?: $view->getCreationDate()),
  2698.                 'obj' => $view,
  2699.             ];
  2700.         }
  2701.         usort($snapshots, static fn($a$b) => ($a['ts'] ?? 0) <=> ($b['ts'] ?? 0));
  2702.         return array_values($snapshots);
  2703.     }
  2704.     private function resolvePreviousUpdateForCurrent(EwsNotification $current): ?EwsNotification
  2705.     {
  2706.         $snapshots $this->collectUpdateVersionSnapshots($current);
  2707.         if ($snapshots === []) {
  2708.             return null;
  2709.         }
  2710.         $lastIndex count($snapshots) - 1;
  2711.         $last $snapshots[$lastIndex];
  2712.         $lastMod = (int) ($last['obj']->getModificationDate() ?: 0);
  2713.         $currentMod = (int) ($current->getModificationDate() ?: 0);
  2714.         // Latest Update version is usually the same state as the live object.
  2715.         if ($lastMod === $currentMod || (int) ($last['ts'] ?? 0) >= $currentMod) {
  2716.             return $lastIndex >= $snapshots[$lastIndex 1]['obj'] : null;
  2717.         }
  2718.         return $last['obj'];
  2719.     }
  2720.     /**
  2721.      * Text describing what changed, plus map images from polygonXAtachment.
  2722.      * - Location or alertType change → previous + after images
  2723.      * - Any other update → single current image
  2724.      * Text fields are always returned in both en and ar.
  2725.      *
  2726.      * @return array{
  2727.      *   textEn: string,
  2728.      *   textAr: string,
  2729.      *   items: array<int, array{key: string, titleEn: string, titleAr: string, detailEn: string, detailAr: string}>,
  2730.      *   images: array{previous: string, current: string}
  2731.      * }
  2732.      */
  2733.     private function buildNotificationUpdateChanges(
  2734.         EwsNotification $current,
  2735.         ?EwsNotification $previous,
  2736.         $translator
  2737.     ): array {
  2738.         $timezone = new \DateTimeZone(defined('TIMEZONE') ? TIMEZONE 'Asia/Riyadh');
  2739.         // Canonical action keys (language-independent), titles resolved per locale below.
  2740.         $actionKeys = [];
  2741.         $seenKeys = [];
  2742.         foreach ($this->extractRelevantHistoryActions($current$translator'en') as $actionMeta) {
  2743.             $key = (string) ($actionMeta['key'] ?? '');
  2744.             if ($key === '' || isset($seenKeys[$key])) {
  2745.                 continue;
  2746.             }
  2747.             $seenKeys[$key] = true;
  2748.             $actionKeys[] = $key;
  2749.         }
  2750.         $hasLocationUpdate in_array('Update Alert Location'$actionKeystrue);
  2751.         $hasAlertTypeUpdate in_array('Raise Alert'$actionKeystrue) || in_array('Lower Alert'$actionKeystrue);
  2752.         // Fallback when alertAction labels are missing but fields clearly changed.
  2753.         if ($previous) {
  2754.             if (!$hasAlertTypeUpdate && $this->hasSeverityChanged($current$previous)) {
  2755.                 $hasAlertTypeUpdate true;
  2756.                 $actionKeys[] = $this->resolveRaiseOrLowerKey($previous$current);
  2757.             }
  2758.             if (!$hasLocationUpdate && $this->hasLocationChanged($current$previous)) {
  2759.                 $hasLocationUpdate true;
  2760.                 $actionKeys[] = 'Update Alert Location';
  2761.             }
  2762.         }
  2763.         $items = [];
  2764.         $textPartsEn = [];
  2765.         $textPartsAr = [];
  2766.         foreach ($actionKeys as $key) {
  2767.             $titleEn $this->resolveUpdateChangeTitle($key$current$translator'en');
  2768.             $titleAr $this->resolveUpdateChangeTitle($key$current$translator'ar');
  2769.             $detailEn $this->buildAlertHistoryActionDetail(
  2770.                 $key,
  2771.                 $current,
  2772.                 $previous,
  2773.                 $timezone,
  2774.                 $translator,
  2775.                 'en'
  2776.             );
  2777.             $detailAr $this->buildAlertHistoryActionDetail(
  2778.                 $key,
  2779.                 $current,
  2780.                 $previous,
  2781.                 $timezone,
  2782.                 $translator,
  2783.                 'ar'
  2784.             );
  2785.             $items[] = [
  2786.                 'key' => $key,
  2787.                 'titleEn' => $titleEn,
  2788.                 'titleAr' => $titleAr,
  2789.                 'detailEn' => $detailEn,
  2790.                 'detailAr' => $detailAr,
  2791.             ];
  2792.             $textPartsEn[] = $detailEn !== '' ? ($titleEn ': ' $detailEn) : $titleEn;
  2793.             $textPartsAr[] = $detailAr !== '' ? ($titleAr ': ' $detailAr) : $titleAr;
  2794.         }
  2795.         $currentImage $this->getPolygonXAttachmentUrl($current) ?? '';
  2796.         if ($hasLocationUpdate || $hasAlertTypeUpdate) {
  2797.             // Location / alertType change → before & after map images
  2798.             $images = [
  2799.                 'previous' => $previous ? ($this->getPolygonXAttachmentUrl($previous) ?? '') : '',
  2800.                 'current' => $currentImage,
  2801.             ];
  2802.         } else {
  2803.             // All other updates → same shape, previous empty
  2804.             $images = [
  2805.                 'previous' => '',
  2806.                 'current' => $currentImage,
  2807.             ];
  2808.         }
  2809.         return [
  2810.             'textEn' => implode("\n"array_filter($textPartsEn, static fn($t) => $t !== '')),
  2811.             'textAr' => implode("\n"array_filter($textPartsAr, static fn($t) => $t !== '')),
  2812.             'items' => $items,
  2813.             'images' => $images,
  2814.         ];
  2815.     }
  2816.     private function resolveUpdateChangeTitle(
  2817.         string $key,
  2818.         EwsNotification $notification,
  2819.         $translator,
  2820.         string $lang
  2821.     ): string {
  2822.         $actions $notification->getAlertAction() ?: [];
  2823.         foreach ($actions as $action) {
  2824.             if (!$action) {
  2825.                 continue;
  2826.             }
  2827.             $mapped $this->mapAlertActionToHistoryTitle((string) $action->getName('en'));
  2828.             if ($mapped === $key) {
  2829.                 $localized = (string) ($action->getName($lang) ?: '');
  2830.                 if ($localized !== '') {
  2831.                     return $localized;
  2832.                 }
  2833.                 break;
  2834.             }
  2835.         }
  2836.         return $this->transAlertHistory($key$translator$lang);
  2837.     }
  2838.     private function getPolygonXAttachmentUrl(?EwsNotification $notification): ?string
  2839.     {
  2840.         if (!$notification) {
  2841.             return null;
  2842.         }
  2843.         $asset $notification->getPolygonXAtachment();
  2844.         if (!$asset || !method_exists($asset'getFullPath')) {
  2845.             return null;
  2846.         }
  2847.         $path = (string) $asset->getFullPath();
  2848.         if ($path === '') {
  2849.             return null;
  2850.         }
  2851.         return BASE_URL $path;
  2852.     }
  2853.     private function hasSeverityChanged(EwsNotification $currentEwsNotification $previous): bool
  2854.     {
  2855.         $from strtolower((string) ($previous->getAlertType()?->getColor() ?? ''));
  2856.         $to strtolower((string) ($current->getAlertType()?->getColor() ?? ''));
  2857.         return $from !== '' && $to !== '' && $from !== $to;
  2858.     }
  2859.     private function hasLocationChanged(EwsNotification $currentEwsNotification $previous): bool
  2860.     {
  2861.         $currentGov $this->extractGovernorateNamesForHistory($current'en');
  2862.         $previousGov $this->extractGovernorateNamesForHistory($previous'en');
  2863.         sort($currentGov);
  2864.         sort($previousGov);
  2865.         if ($currentGov !== $previousGov) {
  2866.             return true;
  2867.         }
  2868.         $currentCoords = (string) ($current->getCoordinates() ?? '');
  2869.         $previousCoords = (string) ($previous->getCoordinates() ?? '');
  2870.         if ($currentCoords !== $previousCoords) {
  2871.             return true;
  2872.         }
  2873.         $currentPolygonId $current->getPolygon()?->getId();
  2874.         $previousPolygonId $previous->getPolygon()?->getId();
  2875.         return $currentPolygonId !== $previousPolygonId;
  2876.     }
  2877.     private function resolveAlertSeverityLabel(EwsNotification $notificationstring $lang 'en'$translator null): string
  2878.     {
  2879.         $color = (string) ($notification->getAlertType()?->getColor() ?? '');
  2880.         if ($color === '') {
  2881.             return '';
  2882.         }
  2883.         $label ucwords(strtolower($color));
  2884.         return $this->transAlertHistory($label$translator$lang);
  2885.     }
  2886.     private function resolveRaiseOrLowerKey(EwsNotification $previousEwsNotification $current): string
  2887.     {
  2888.         $rank = ['green' => 1'yellow' => 2'orange' => 3'red' => 4];
  2889.         $from strtolower((string) ($previous->getAlertType()?->getColor() ?? ''));
  2890.         $to strtolower((string) ($current->getAlertType()?->getColor() ?? ''));
  2891.         $fromRank $rank[$from] ?? 0;
  2892.         $toRank $rank[$to] ?? 0;
  2893.         return $toRank $fromRank 'Lower Alert' 'Raise Alert';
  2894.     }
  2895.     private function resolveRaiseOrLowerTitle(
  2896.         EwsNotification $previous,
  2897.         EwsNotification $current,
  2898.         $translator,
  2899.         string $lang
  2900.     ): string {
  2901.         return $this->transAlertHistory(
  2902.             $this->resolveRaiseOrLowerKey($previous$current),
  2903.             $translator,
  2904.             $lang
  2905.         );
  2906.     }
  2907.     public function ewsNotificationHasVersionUpdateHistory(EwsNotification $notification): bool
  2908.     {
  2909.         $updateCount 0;
  2910.         foreach ($notification->getVersions() ?: [] as $version) {
  2911.             if ($version->getNote() === 'Update') {
  2912.                 $updateCount++;
  2913.                 if ($updateCount 1) {
  2914.                     return true;
  2915.                 }
  2916.             }
  2917.         }
  2918.         return false;
  2919.     }
  2920.     private function createNotificationFormat($notification$translator$decodedJwtToken null$userPermission null)
  2921.     {
  2922.         $response = [];
  2923.         $critetia '';
  2924.         $critetiaAr '';
  2925.         $status 'active';
  2926.         $alertType $notification->getAlertType();
  2927.         $phenomena $notification->getWeatherPhenomenon();
  2928.         // Set the timezone to Asia/Riyadh
  2929.         $timezone = new \DateTimeZone(TIMEZONE);
  2930.         // Convert start and end dates to Asia/Riyadh timezone
  2931.         $startDate $notification->getStartDate() ? $notification->getStartDate()->setTimezone($timezone) : null;
  2932.         $endDate $notification->getEndDate() ? $notification->getEndDate()->setTimezone($timezone) : null;
  2933.         $alertEndDate $notification->getAlertEndDate() ? $notification->getAlertEndDate()->setTimezone($timezone) : null;
  2934.         if ($alertType && $phenomena) {
  2935.             $phenomenaList $alertType->getPhenomenaList();
  2936.             if ($phenomenaList) {
  2937.                 $items $phenomenaList->getItems();
  2938.                 if ($items) {
  2939.                     foreach ($items as $item) {
  2940.                         if ($item->getPhenomena() == $phenomena->getId()) {
  2941.                             $critetia $item->getCriteria();
  2942.                             $critetiaAr $item->getCriteriaAr();
  2943.                             break;
  2944.                         }
  2945.                     }
  2946.                 }
  2947.             }
  2948.         }
  2949.         if ($notification->getStatus() != "ended") {
  2950.             if ($endDate && $endDate < new \DateTime('now'$timezone)) {
  2951.                 $status "expired";
  2952.             }
  2953.             $twentyFourHoursAgo = (new \DateTime('now'$timezone))->modify('-24 hours');
  2954.             if ($endDate && $endDate $twentyFourHoursAgo) {
  2955.                 $status "archived";
  2956.             }
  2957.         } else {
  2958.             $twentyFourHoursAgo = (new \DateTime('now'$timezone))->modify('-24 hours');
  2959.             if ($alertEndDate && $alertEndDate $twentyFourHoursAgo) {
  2960.                 $status "archived";
  2961.             } else {
  2962.                 $status "ended";
  2963.             }
  2964.         }
  2965.         $hideUnpublishedBackup DataObject::doHideUnpublished();
  2966.         DataObject::setHideUnpublished(false);
  2967.         $polygonId $notification->getPolygon()?->getId();
  2968.         $linkedAlertIds array_map(
  2969.             fn($obj) => $obj->getId(),
  2970.             $notification->getPolygon()?->getEwsAlerts(true) ?? []
  2971.         );
  2972.         $polCoords $notification->getPolygon() ? json_decode($notification->getPolygon()->getcoordinates()) : [];
  2973.         $polExpired $notification->getPolygon() && $notification->getPolygon()->getExpire() ? $notification->getPolygon()->getExpire() : null;
  2974.         $polPublished $notification->getPolygon() ? $notification->getPolygon()->getPublished() : false;
  2975.         DataObject::setHideUnpublished($hideUnpublishedBackup);
  2976.         // Initialize edit and delete flags
  2977.         $editFlag false;
  2978.         // Get the current report's type key
  2979.         $notificationType $notification->getNotificationType();
  2980.         // Map report type keys to permission names
  2981.         $permissionMapping = [
  2982.             'Default' => 'edit_ews_notification',
  2983.             'Polygon' => 'update_ews_notification_polygon',
  2984.             'Map' => 'update_ews_notification_map'
  2985.         ];
  2986.         // Check permissions only for the current report type
  2987.         // if ($notificationType && isset($permissionMapping[$notificationType]) && !empty($userPermission)) {
  2988.         //     $permissionName = $permissionMapping[$notificationType];
  2989.         //     $permissionStatus = $userPermission->getUserPermissions($decodedJwtToken, $translator);
  2990.         //     // Check edit permission
  2991.         //     if (isset($permissionStatus['grants']["$permissionName"]) && $permissionStatus['grants']["$permissionName"] === true) {
  2992.         //         $editFlag = true;
  2993.         //     }
  2994.         // }
  2995.         $response = [
  2996.             "id" => $notification->getId(),
  2997.             "searchEwsIdEn" => $notification->getEwsSearchId("en"),
  2998.             "searchEwsIdAr" => $notification->getEwsSearchId("ar"),
  2999.             "title" => $startDate $startDate->format("dmY") . '-' $notification->getId() : '',
  3000.             "alertType" => !empty($notification->getAlertType()) ? $notification->getAlertType()->getAlertTypeId() : "",
  3001.             "alertTypeAr" => !empty($notification->getAlertType()) ? $notification->getAlertType()->getName("ar") : "",
  3002.             "alertTypeEn" => !empty($notification->getAlertType()) ? ucwords($notification->getAlertType()->getColor()) : "",
  3003.             "fromDate" => $startDate $startDate->format("Y-m-d H:i:s") : "",
  3004.             "toDate" => $endDate $endDate->format("Y-m-d H:i:s") : "",
  3005.             "alertEndDate" => $alertEndDate && $status == "ended"  $alertEndDate->format("Y-m-d H:i:s") : null,
  3006.             "alertStatusID" => !empty($notification->getAlertStatus()) ? $notification->getAlertStatus()->getAlertStatusId() : "",
  3007.             "alertStatusAr" => !empty($notification->getAlertStatus()) ? $notification->getAlertStatus()->getName("ar") : "",
  3008.             "alertStatusEn" => !empty($notification->getAlertStatus()) ? $notification->getAlertStatus()->getName("en") : "",
  3009.             "alertStatusCategory" => "",
  3010.             "alertHazard" => !empty($notification->getAlertHazard()) ? $this->getAlertHazardArr($notification->getAlertHazard()) : [],
  3011.             "regionID" => !empty($notification->getRegion()) ? $notification->getRegion()->getRegionId() : "",
  3012.             "regionAR" => !empty($notification->getRegion()) ? $notification->getRegion()->getName("ar") : "",
  3013.             "regionEn" => !empty($notification->getRegion()) ? $notification->getRegion()->getName("en") : "",
  3014.             "governorates" => ($notification->getRegion()) ? $this->getGovernorateDetail($notification->getGovernorate()) : [],
  3015.             "mapGovernorates" => $notification->getMapGovernorate() ? $this->getGovernorateDetail($notification->getMapGovernorate()) : [],
  3016.             "ewsOtherLocations" => ($notification->getRegion()) ? $this->getGovernorateDetail($notification->getEwsOtherLocations()) : [],
  3017.             "otherLocationsAr" => $this->getOtherLocationsNames($notification->getEwsOtherLocations(), 'ar'),
  3018.             "otherLocationsEn" => $this->getOtherLocationsNames($notification->getEwsOtherLocations(), 'en'),
  3019.             "tweetID" => "",
  3020.             "enableTwitterNotification" => $notification->getEnableTwitterNotification(),
  3021.             "enableSMSNotification" => $notification->getEnableSMSNotification(),
  3022.             "enableEmailNotification" => $notification->getEnableEmailNotification(),
  3023.             "alertActions" => $this->getAlertActionsByArr($notification->getAlertAction()),
  3024.             "municipalities" => $this->getMunicipalityArr($notification->getMunicipality()),
  3025.             "centers" => $this->getCenterArr($notification->getCenter()),
  3026.             "districts" => $this->getDistrictArr($notification->getDistrict()),
  3027.             "lastModified" => ($notification->getModificationDate()) ? date("Y-m-d H:i:s"$notification->getModificationDate()) : "",
  3028.             "last_modified_date" => ($notification->getModificationDate()) ? date("Y-m-d"$notification->getModificationDate()) : "",
  3029.             'coordinates' => $notification->getCoordinates(),
  3030.             'message' => $notification->getMessage("en"),
  3031.             "message_en" => Bilingual::to($notification->getMessage("en"), 'en'),
  3032.             "message_ar" => Bilingual::to($notification->getMessage("en"), 'ar'),
  3033.             "file" => (!empty($notification->getAttachment())) ? API_BASE_URL $notification->getAttachment()->getFullPath() : [],
  3034.             'criteria' => $critetia,
  3035.             'criteriaAr' => $critetiaAr,
  3036.             'created_at' => ($notification->getCreationDate()) ? date("Y-m-d H:i:s"$notification->getCreationDate()) : "",
  3037.             'created_by' => ($notification->getUser()) ? $notification->getUser()->getName() : "",
  3038.             'edited_by' => ($notification->getEditor()) ? $notification->getEditor()->getName() : (($notification->getUser()) ? $notification->getUser()->getName() : ""),
  3039.             'status_en' => ucfirst($status),
  3040.             'status_ar' => $translator->trans(ucfirst($status), [], null'ar'),
  3041.             'previewText' => $notification->getPreviewText() ?? false,
  3042.             'isPolygon' => $notification->getIsPolygon(),
  3043.             'linkedAlerts' => $linkedAlertIds,
  3044.             'polygonId' => $polygonId,
  3045.             'polygonCoordinates' => $polCoords,
  3046.             'polygonExpired' => $polExpired,
  3047.             'polygonPublished' => $polPublished,
  3048.             'canEdit' => $editFlag,
  3049.             'notificationType' => $notification->getNotificationType(),
  3050.             'alertPage' => $notification->getAlertPage(),
  3051.             'x_post' => $notification->getXPost(),
  3052.             'user_group_ids' => array_map(
  3053.                 fn($group) => $group->getId(),
  3054.                 $notification->getUserGroup()
  3055.             ),
  3056.         ];
  3057.         return $response;
  3058.     }
  3059.     /**
  3060.      * Same payload shape as one element in device API search-ews-notification "data" (plus optional caller fields).
  3061.      */
  3062.     public function buildSearchEwsNotificationAlertPayload(
  3063.         EwsNotification $notification,
  3064.         $translator,
  3065.         $decodedJwtToken null,
  3066.         $userPermission null
  3067.     ): array {
  3068.         return $this->createNotificationFormat($notification$translator$decodedJwtToken$userPermission);
  3069.     }
  3070.     private function getAlertHazardArr($alertHazardArr)
  3071.     {
  3072.         $result = [];
  3073.         if ($alertHazardArr) {
  3074.             foreach ($alertHazardArr as $affect) {
  3075.                 $result[] = [
  3076.                     "pim_id" => $affect->getId(),
  3077.                     "id" => $affect->getAlertHazardId(),
  3078.                     "nameEn" => $affect->getName("en"),
  3079.                     "nameAr" => $affect->getName("ar"),
  3080.                 ];
  3081.             }
  3082.         }
  3083.         return $result;
  3084.     }
  3085.     private function getAlertActionsByArr($alertActions)
  3086.     {
  3087.         $result = [];
  3088.         if ($alertActions) {
  3089.             foreach ($alertActions as $alert) {
  3090.                 $result[] = [
  3091.                     "pim_id" => $alert->getId(),
  3092.                     "id" => $alert->getAlertActionId(),
  3093.                     "descriptionEn" => $alert->getName("en"),
  3094.                     "descriptionAr" => $alert->getName("ar"),
  3095.                 ];
  3096.             }
  3097.         }
  3098.         return $result;
  3099.     }
  3100.     private function getMunicipalityArr($municipalities)
  3101.     {
  3102.         $result = [];
  3103.         if ($municipalities) {
  3104.             foreach ($municipalities as $municipality) {
  3105.                 // p_R($municipality->getName());                
  3106.                 $result[] = [
  3107.                     "id" => $municipality->getMunicipalityId(),
  3108.                     "nameEn" => $municipality->getName("en"),
  3109.                     "nameAr" => $municipality->getName("ar"),
  3110.                     "governate" => $municipality->getGovernorate()->getGovernoteId()
  3111.                 ];
  3112.             }
  3113.         }
  3114.         return $result;
  3115.     }
  3116.     private function getDistrictArr($districts)
  3117.     {
  3118.         $result = [];
  3119.         if ($districts) {
  3120.             foreach ($districts as $district) {
  3121.                 $result[] = [
  3122.                     "id" => $district->getId(),
  3123.                     "nameEn" => $district->getName("en"),
  3124.                     "nameAr" => $district->getName("ar"),
  3125.                     "longitude" => $district->getLongitude(),
  3126.                     "latitude" => $district->getLatitude(),
  3127.                     "governate" => $district->getGovernorate()->getGovernoteId()
  3128.                 ];
  3129.             }
  3130.         }
  3131.         return $result;
  3132.     }
  3133.     private function getCenterArr($centers)
  3134.     {
  3135.         $result = [];
  3136.         if ($centers) {
  3137.             foreach ($centers as $center) {
  3138.                 $result[] = [
  3139.                     "id" => $center->getId(),
  3140.                     "nameEn" => $center->getName("en"),
  3141.                     "nameAr" => $center->getName("ar"),
  3142.                     "longitude" => $center->getLongitude(),
  3143.                     "latitude" => $center->getLatitude(),
  3144.                     "governate" => $center->getGovernorate()->getGovernoteId()
  3145.                 ];
  3146.             }
  3147.         }
  3148.         return $result;
  3149.     }
  3150.     private function getOtherLocationsNames($otherLocations$lang 'ar')
  3151.     {
  3152.         $names = []; // Step 1: Initialize an array to hold the names
  3153.         if ($otherLocations) {
  3154.             foreach ($otherLocations as $otherLocation) {
  3155.                 $names[] = $otherLocation->getName($lang); // Step 2 & 3: Extract and collect names
  3156.             }
  3157.         }
  3158.         if (empty($names)) { // Check if the names array is empty
  3159.             return ''// Return an empty string if there are no names
  3160.         }
  3161.         $namesString implode(', '$names); // Step 4: Convert the array to a comma-separated string
  3162.         return $namesString// Return or use the comma-separated string as needed
  3163.     }
  3164.     private function shouldDispatchPublicPortalEwsWebhook(EwsNotification $notification): bool
  3165.     {
  3166.         if ($notification->getPublished()) {
  3167.             return true;
  3168.         }
  3169.         $status = (string) $notification->getStatus();
  3170.         return $status === 'ended' || $status === 'archived';
  3171.     }
  3172.     private function dispatchPublicPortalEwsWebhookIfConfigured(int $notificationId, ?LoggerInterface $logger null): void
  3173.     {
  3174.         try {
  3175.             PublicPortalEwsWebhookProcessLauncher::dispatch($notificationId$logger);
  3176.         } catch (\Throwable $e) {
  3177.             if ($logger !== null) {
  3178.                 $logger->warning('Public portal EWS webhook dispatch failed: ' $e->getMessage());
  3179.             }
  3180.         }
  3181.     }
  3182.     public function publishEwsNotification($notificationId$published$userGroupIds$translator$emailService$templating$logger)
  3183.     {
  3184.         $result = [];
  3185.         $viewNotification DataObject\EwsNotification::getById($notificationIdfalse);
  3186.         if (!$viewNotification) {
  3187.             $viewNotification DataObject\EwsNotification::getById($notificationIdtrue);
  3188.         }
  3189.         if ($published) {
  3190.             $viewNotification->setStatus("active");
  3191.         } else {
  3192.             $viewNotification->setStatus("");
  3193.         }
  3194.         $viewNotification->setPublished($published);
  3195.         //set ews search Id
  3196.         $currentDate = new \DateTime();
  3197.         $formattedDate $currentDate->format('dmY') . '-' $viewNotification->getId();
  3198.         $searchIdEn 'Early Warning System | ' $formattedDate ' | ' ucfirst($viewNotification->getAlertType()?->getColor()) . ' Alert | ' $viewNotification->getWeatherPhenomenon()?->getTitle("en");
  3199.         $searchIdAr $translator->trans('Early Warning System', [], null"ar") . ' | ' $formattedDate ' | ' $translator->trans(ucfirst($viewNotification->getAlertType()?->getColor()) . ' Alert', [], null"ar") . ' | ' $viewNotification->getWeatherPhenomenon()?->getTitle("ar");
  3200.         $viewNotification->setEwsSearchId($searchIdEn"en");
  3201.         $viewNotification->setEwsSearchId($searchIdAr"ar");
  3202.         // // When publishing an updated alert to Twitter, also generate history on republish path.
  3203.         // if ($published && $viewNotification->getEnableTwitterNotification() && $templating) {
  3204.         //     try {
  3205.         //         if ($this->ewsNotificationHasVersionUpdateHistory($viewNotification)) {
  3206.         //             $historyLang = method_exists($translator, 'getLocale') ? (string) $translator->getLocale() : 'en';
  3207.         //             $historyAsset = $this->generateAlertHistoryTwitterImage(
  3208.         //                 $viewNotification,
  3209.         //                 $templating,
  3210.         //                 $logger,
  3211.         //                 false,
  3212.         //                 $translator,
  3213.         //                 $historyLang
  3214.         //             );
  3215.         //             if ($historyAsset) {
  3216.         //                 $viewNotification->setXHistoryAtachment($historyAsset);
  3217.         //             }
  3218.         //         }
  3219.         //     } catch (\Throwable $e) {
  3220.         //         $logger->error('Failed to generate EWS alert history Twitter image on publish: ' . $e->getMessage());
  3221.         //     }
  3222.         // }
  3223.         // Allow TwitterEventListner to post again on republish after an alert update.
  3224.         if ($published && $viewNotification->getEnableTwitterNotification()) {
  3225.             $viewNotification->setTwitterId('');
  3226.             $viewNotification->setTwitterLog('');
  3227.         }
  3228.         // Persist the selected user groups so the End alert can email the same groups later.
  3229.         if (!empty($userGroupIds)) {
  3230.             $groups = [];
  3231.             foreach ($userGroupIds as $gid) {
  3232.                 $group EwsAndReportUserGroup::getById($gidtrue);
  3233.                 if ($group instanceof EwsAndReportUserGroup) {
  3234.                     $groups[] = $group;
  3235.                 }
  3236.             }
  3237.             $viewNotification->setUserGroup($groups);
  3238.         }
  3239.         $viewNotification->save(["versionNote" => "Update"]);
  3240.         // $alert = $this->createNotificationFormat($viewNotification, $translator);
  3241.         // Commenting out the public portal EWS webhook dispatch as not working correctly 2026-08-17.
  3242.         // $this->dispatchPublicPortalEwsWebhookIfConfigured((int) $viewNotification->getId(), $logger);
  3243.         if ($published) {
  3244.             // Emails stay on the same console command; run it in the background so this API is not blocked.
  3245.             $jsonUserGroupIds json_encode($userGroupIds);
  3246.             try {
  3247.                 ConsoleBackgroundProcess::start([
  3248.                     'php',
  3249.                     'bin/console',
  3250.                     'app:send-early-warning-alert-email',
  3251.                     '--alertId=' $viewNotification->getId(),
  3252.                     '--userGroupIds=' $jsonUserGroupIds,
  3253.                 ]);
  3254.                 $logger->info('Started send-early-warning-alert-email in background for EwsNotification ' $viewNotification->getId());
  3255.             } catch (\Throwable $exception) {
  3256.                 $logger->error("published EwsNotification command failed to start: " $exception->getMessage());
  3257.                 return ['success' => false'message' => $exception->getMessage()];
  3258.             }
  3259.         }
  3260.         return ["success" => true"message" => $translator->trans("ews_notification_published")];
  3261.     }
  3262.     public function previewEwsNotification($params$user$translator$templating)
  3263.     {
  3264.         $regionId $params['regionId'] ?? null;
  3265.         $governorateIds $params['governateIds'] ?? null;
  3266.         $startDate $params['startDate'] ?? null;
  3267.         $endDate $params['endDate'] ?? null;
  3268.         $startTime $params['startTime'] ?? null;
  3269.         $endTime $params['endTime'] ?? null;
  3270.         $alertActionId $params['alertActionId'] ?? null;
  3271.         $alertTypeId $params['alertTypeId'] ?? null;
  3272.         $alertStatusId $params['alertStatusId'] ?? null;
  3273.         $alertHazardId $params['alertHazardId'] ?? null;
  3274.         $alertItem AlertType::getByAlertTypeId($this->scalarId($alertTypeId), 1);
  3275.         $regionItem Region::getByRegionId($this->scalarId($regionId), 1);
  3276.         $alertStatusItem AlertStatus::getByAlertStatusId($this->scalarId($alertStatusId), 1);
  3277.         $govIds $this->parseCommaSeparatedIds($governorateIds);
  3278.         $govById = [];
  3279.         if (!empty($govIds)) {
  3280.             $governorateArr = new Governorate\Listing();
  3281.             $governorateArr->setCondition('governoteId IN (?)', [$govIds]);
  3282.             foreach ($governorateArr as $item) {
  3283.                 $govById[(string) $item->getgovernoteId()] = $item;
  3284.             }
  3285.         }
  3286.         $govItems = [];
  3287.         foreach ($govIds as $govId) {
  3288.             if (!isset($govById[(string) $govId])) {
  3289.                 continue;
  3290.             }
  3291.             $item $govById[(string) $govId];
  3292.             $govItems[] = [
  3293.                 "id" => $item->getgovernoteId(),
  3294.                 "nameEn" => $item->getName('en'),
  3295.                 "nameAr" => $item->getName('ar'),
  3296.                 "longitude" => (float)$item->getLongitude(),
  3297.                 "latitude" => (float)$item->getLatitude(),
  3298.             ];
  3299.         }
  3300.         $hazardIds $this->parseCommaSeparatedIds($alertHazardId);
  3301.         $hazardById = [];
  3302.         if (!empty($hazardIds)) {
  3303.             $hazardArr = new AlertHazard\Listing();
  3304.             $hazardArr->setCondition('alertHazardId IN (?)', [$hazardIds]);
  3305.             foreach ($hazardArr as $item) {
  3306.                 $hazardById[(string) $item->getAlertHazardId()] = $item;
  3307.             }
  3308.         }
  3309.         $hazardItems = [];
  3310.         foreach ($hazardIds as $hazardId) {
  3311.             if (!isset($hazardById[(string) $hazardId])) {
  3312.                 continue;
  3313.             }
  3314.             $item $hazardById[(string) $hazardId];
  3315.             $hazardItems[] = [
  3316.                 "pim_id" => $item->getId(),
  3317.                 "id" => $item->getAlertHazardId(),
  3318.                 "nameEn" => $item->getName('en'),
  3319.                 "nameAr" => $item->getName('ar'),
  3320.             ];
  3321.         }
  3322.         $actionIds $this->parseCommaSeparatedIds($alertActionId);
  3323.         $actionById = [];
  3324.         if (!empty($actionIds)) {
  3325.             $actionArr = new AlertAction\Listing();
  3326.             $actionArr->setCondition('alertActionId IN (?)', [$actionIds]);
  3327.             foreach ($actionArr as $item) {
  3328.                 $actionById[(string) $item->getAlertActionId()] = $item;
  3329.             }
  3330.         }
  3331.         $actionItems = [];
  3332.         foreach ($actionIds as $actionId) {
  3333.             if (!isset($actionById[(string) $actionId])) {
  3334.                 continue;
  3335.             }
  3336.             $item $actionById[(string) $actionId];
  3337.             $actionItems[] = [
  3338.                 "pim_id" => $item->getId(),
  3339.                 "id" => $item->getAlertActionId(),
  3340.                 "descriptionEn" => $item->getName('en'),
  3341.                 "descriptionAr" => $item->getName('ar'),
  3342.             ];
  3343.         }
  3344.         // Preview only: reuse an existing token or mint a display token. Do not persist
  3345.         // a new unsubscribe token on the user (that was a write on every preview).
  3346.         $tokenURL '';
  3347.         $token '';
  3348.         if (is_object($user) && method_exists($user'getEwsNotificationToken')) {
  3349.             $token = (string) $user->getEwsNotificationToken();
  3350.         }
  3351.         if ($token === '') {
  3352.             $token base64_encode($user->getEmail() . time() . uniqid());
  3353.         }
  3354.         if ($token !== '') {
  3355.             $tokenURL BASE_URL '/unsubscribe/notification?ews=true&token=' $token;
  3356.         }
  3357.         $currentDate = new \DateTime();
  3358.         $dummyId "ID";
  3359.         $dummyIdAr "الهوية";
  3360.         $formattedDate $currentDate->format('dmY') . '-' $dummyId;
  3361.         $formattedDateAr $currentDate->format('dmY') . '-' $dummyIdAr;
  3362.         $searchIdEn 'Early Warning System | ' $formattedDate ' | ' ucfirst($alertItem->getColor()) . ' Alert | ' ;
  3363.         $searchIdAr $translator->trans('Early Warning System', [], null"ar") . ' | ' $formattedDateAr ' | ' $translator->trans(ucfirst($alertItem->getColor()) . ' Alert', [], null"ar") . ' | ' ;
  3364.         $fromDate "{$startDate} " . ($startTime $startTime '00:00:00');
  3365.         $toDate "{$endDate} " . ($endTime $endTime '00:00:00');
  3366.         $message = [
  3367.             "en" => "There is {$alertStatusItem->getName('en')} alert in {$regionItem->get('name''en')} Start Time : {$fromDate} End Time : {$toDate}",
  3368.             "ar" => "يوجد تنبيه {$alertStatusItem->getName('ar')} في {$regionItem->get('name''ar')} وقت البداية: {$fromDate} وقت الانتهاء: {$toDate}"
  3369.         ];
  3370.         $alert = [
  3371.             "user_name" => $user->getName(),
  3372.             "host" => API_BASE_URL,
  3373.             "tokenURL" => $tokenURL,
  3374.             "title" => $formattedDate,
  3375.             "alertColor" => $alertItem->getColor(),
  3376.             "alertType" => $alertItem->getAlertTypeId(),
  3377.             "alertTypeAr" => $alertItem->getName('ar'),
  3378.             "alertTypeEn" => $alertItem->getColor(),
  3379.             "fromDate" => "{$startDate} {$startTime}",
  3380.             "toDate" => "{$endDate} {$endTime}",
  3381.             "mannedAlertDatailUrl" => null,
  3382.             "searchEwsIdEn" => $searchIdEn,
  3383.             "searchEwsIdAr" => $searchIdAr,
  3384.             "alertHazard" => $hazardItems,
  3385.             "alertActions" => $actionItems,
  3386.             "alertStatusID" => $alertStatusItem->getalertStatusId(),
  3387.             "alertStatusEn" => $alertStatusItem->getName('en'),
  3388.             "alertStatusAr" => $alertStatusItem->getName('ar'),
  3389.             "regionID" => $regionId,
  3390.             "regionEn" => $regionItem->get('name''en'),
  3391.             "regionAR" => $regionItem->get('name''ar'),
  3392.             "governorates" => $govItems,
  3393.             "last_modified_date" => date("Y-m-d"),
  3394.         ];
  3395.         $html $templating->render('web2print/_manned_alert_notification_ar.html.twig'$alert);
  3396.         return ["success" => true"emailHtml" => $html"smsMsg" => $message['ar']];
  3397.     }
  3398.     public function reportEwsNotification($param$translator)
  3399.     {
  3400.         $result = [];
  3401.         $region null;
  3402.         try {
  3403.             $listing = new EwsNotification\Listing();
  3404.             $reportType = isset($param['report_type']) ? $param['report_type'] : null;
  3405.             $regionId = isset($param['region_id']) ? $param['region_id'] : null;
  3406.             if ($regionId) {
  3407.                 $region \Pimcore\Model\DataObject\Region::getByRegionId($regionIdtrue);
  3408.                 if (!$region) {
  3409.                     throw new \Exception("Region not exists");
  3410.                 }
  3411.             }
  3412.             switch ($reportType) {
  3413.                 case 'published_in_week':
  3414.                     $listing->setCondition("o_published = true AND o_creationDate >= " strtotime("-1 week"));
  3415.                     break;
  3416.                 case 'published_in_day':
  3417.                     $listing->setCondition("o_published = true AND o_creationDate >= " strtotime("-1 day"));
  3418.                     break;
  3419.                 case 'published_in_month':
  3420.                     $listing->setCondition("o_published = true AND o_creationDate >= " strtotime("-1 month"));
  3421.                     break;
  3422.                 case 'published_in_year':
  3423.                     $listing->setCondition("o_published = true AND o_creationDate >= " strtotime("-1 year"));
  3424.                     break;
  3425.                 case 'published_in_region':
  3426.                     $listing->setCondition("o_published = true AND region__id = ?", [$region->getId()]);
  3427.                     break;
  3428.                 case 'published_in_draft':
  3429.                     $listing->setCondition("o_published = false");
  3430.                     break;
  3431.                 default:
  3432.                     throw new \Exception("Invalid request");
  3433.                     break;
  3434.             }
  3435.             $result['count'] = $listing->getCount();
  3436.             $notifications $listing->load();
  3437.             if ($notifications) {
  3438.                 foreach ($notifications as $notification) {
  3439.                     $result['data'][] = $this->createNotificationFormat($notification$translator);
  3440.                 }
  3441.             }
  3442.             return $result;
  3443.         } catch (\Exception $ex) {
  3444.             return new \Exception($ex->getMessage());
  3445.         }
  3446.         return $result;
  3447.     }
  3448.     private function sendEmailNotification($users$alert$emailService$templating)
  3449.     {
  3450.         $mailSent null;
  3451.         if ($alert) {
  3452.             $governatesArr $alert['governorates'];
  3453.             $governates = [];
  3454.             if ($governatesArr) {
  3455.                 foreach ($governatesArr as $gov) {
  3456.                     $governates[] = $gov['nameEn'];
  3457.                 }
  3458.             }
  3459.             $data $alert;
  3460.             $data['sender'] = 'National Center for Meteorology';
  3461.             //$subject = $alert['searchEwsIdAr'] ??'Severe Weather Alert - ' . $gov['nameEn'];
  3462.             // $subject = $alert['alertStatusAr'] . ' - ' . $alert['regionAR'];
  3463.             // $subject = 'النظام الالي للإنذار المبكر :' . ' - ' . $alert['searchEwsIdAr'];
  3464.             $parts explode('|'$alert['searchEwsIdAr']);
  3465.             $excluded implode('|'array_slice($parts1));
  3466.             $subject 'النظام الالي للإنذار المبكر :'  $excluded;
  3467.             // $html = $templating->render('web2print/_manned_alert_notification_ar.html.twig', $data);
  3468.             //sending an email document (pimcore document)
  3469.             // $emailAlertTemplate = '/email/alert_notification';
  3470.             if ($users) {
  3471.                 foreach ($users as $currentUser) {
  3472.                     if (isset($alert['enableEmailNotification']) && $alert['enableEmailNotification']) {
  3473.                         if ($currentUser->getSendEwsEmail() || is_null($currentUser->getSendEwsEmail())) {
  3474.                             # code...
  3475.                             // $email = $currentUser->getEmail();
  3476.                             // $param = ['user' => $currentUser, 'message' => $html, 'url' => null];
  3477.                             // $mailSent = $emailService->sendMail($param, $email, $emailAlertTemplate, $subject);
  3478.                             // $data['name'] = $user->getName();
  3479.                             // extra param js
  3480.                             $alertObj \Pimcore\Model\DataObject\AlertType::getByAlertTypeId($alert['alertType'], true);
  3481.                             $alertName $alertObj->getColor() ? $alertObj->getColor() : '';
  3482.                             $alert['user_name'] = $currentUser->getName();
  3483.                             $alert['host'] = API_BASE_URL;
  3484.                             $alert['alertColor'] = $alertName;
  3485.                             $backgroundColor '#fcb82526';
  3486.                             $borderColor '#000000';
  3487.                             $textColor '#000000';
  3488.                             // unsubscribe ews notificaiton token
  3489.                             $tokenURL '';
  3490.                             $token $this->userModel->unSubscribeEwsGenerateToken($currentUser->getEmail());
  3491.                             if (!empty($token)) {
  3492.                                 $tokenURL BASE_URL '/unsubscribe/notification?ews=true&token=' $token;
  3493.                             }
  3494.                             if (isset($alert['alertColor']) && !empty(trim($alert['alertColor']))) {
  3495.                                 $alertColorLower strtolower(trim($alert['alertColor']));
  3496.                                 if ($alertColorLower === 'red') {
  3497.                                     $backgroundColor '#f6000017';
  3498.                                     $borderColor '#F60000';
  3499.                                     $textColor '#F60000';
  3500.                                 } elseif ($alertColorLower === 'orange') {
  3501.                                     $backgroundColor '#ff66001f';
  3502.                                     $borderColor '#FF6600';
  3503.                                     $textColor '#FF6600';
  3504.                                 } elseif ($alertColorLower === 'yellow') {
  3505.                                     $backgroundColor '#fcb82526';
  3506.                                     $borderColor '#FCB825';
  3507.                                     $textColor '#FCB825';
  3508.                                 }
  3509.                             }
  3510.                             $purpose EWS_MESSAGE;
  3511.                             $alert['backgroundColor'] = $backgroundColor;
  3512.                             $alert['borderColor'] = $borderColor;
  3513.                             $alert['textColor'] = $textColor;
  3514.                             $alert['tokenURL'] = $tokenURL;
  3515.                             $alert['mannedAlertDatailUrl'] = null;
  3516.                             $html $templating->render('web2print/_manned_alert_notification_ar.html.twig'$alert);
  3517.                             $mailSent $this->c2Service->sendNotificationEmail($_ENV['EWS_MAIL_TEMPLATE'], $alert['id'], $currentUser->getId(), $html$subject$purpose);
  3518.                             // $this->c2Service->sendMannedAlertEmails($html,$currentUser->getId(),$alert['id']);
  3519.                             // $mailSent=$this->c2Service->sendMannedAlertEmails($html,$currentUser->getId(),$alert['id'],$subject);
  3520.                             if ($mailSent) {
  3521.                                 $status "sent";
  3522.                                 $this->saveEmailStatus($currentUser->getEmail(), $currentUser->getName(), $alert$status);
  3523.                             } else {
  3524.                                 $status "not sent";
  3525.                                 $this->saveEmailStatus($currentUser->getEmail(), $currentUser->getName(), $alert$status);
  3526.                             }
  3527.                         }
  3528.                     }
  3529.                 }
  3530.             }
  3531.         }
  3532.         return $mailSent;
  3533.     }
  3534.     private function sendExcelEmailNotification($userGroupIds$alert$emailService$templating)
  3535.     {
  3536.         $mailSent null;
  3537.         if ($alert) {
  3538.             $governatesArr $alert['governorates'];
  3539.             $governates = [];
  3540.             if ($governatesArr) {
  3541.                 foreach ($governatesArr as $gov) {
  3542.                     $governates[] = $gov['nameEn'];
  3543.                 }
  3544.             }
  3545.             $data $alert;
  3546.             $data['sender'] = 'National Center for Meteorology';
  3547.             // $subject = $alert['searchEwsIdAr'] ??'Severe Weather Alert - ' . $gov['nameEn'];
  3548.             //$subject = $alert['alertStatusAr'] . ' - ' . $alert['regionAR'];
  3549.             // $subject = 'النظام الالي للإنذار المبكر :' . ' - ' . $alert['searchEwsIdAr'];
  3550.             $parts explode('|'$alert['searchEwsIdAr']);
  3551.             $excluded implode('|'array_slice($parts1));
  3552.             $subject 'النظام الالي للإنذار المبكر :'  $excluded;
  3553.             if ($userGroupIds) {
  3554.                 foreach ($userGroupIds as $userGroupId) {
  3555.                     if ($userGroupId) {
  3556.                         $userGroup \Pimcore\Model\DataObject\EwsAndReportUserGroup::getById($userGroupIdtrue);
  3557.                         if ($userGroup instanceof \Pimcore\Model\DataObject\EwsAndReportUserGroup) {
  3558.                             foreach (json_decode($userGroup->getJsonData()) as $currentUser) {
  3559.                                 $entityUserSubscription $this->userModel->getEntitySubscription($currentUser);
  3560.                                 $packageExpired $entityUserSubscription ? (new \DateTime() > $entityUserSubscription->getEndDate() ? true false) : false;
  3561.                                 $subActive $entityUserSubscription $entityUserSubscription->getIsActive() : false;
  3562.                                 $nonSubUser $subActive && !$packageExpired;
  3563.                                 if (isset($currentUser->firstName) && isset($currentUser->lastName) && isset($currentUser->email) && $nonSubUser) {
  3564.                                     // extra param js
  3565.                                     $alertObj \Pimcore\Model\DataObject\AlertType::getByAlertTypeId($alert['alertType'], true);
  3566.                                     $alertName $alertObj->getColor() ? $alertObj->getColor() : '';
  3567.                                     $alert['user_name'] = $currentUser->firstName ' ' $currentUser->lastName;
  3568.                                     $alert['host'] = API_BASE_URL;
  3569.                                     $alert['alertColor'] = $alertName;
  3570.                                     $backgroundColor '#fcb82526';
  3571.                                     $borderColor '#000000';
  3572.                                     $textColor '#000000';
  3573.                                     if (isset($alert['alertColor']) && !empty(trim($alert['alertColor']))) {
  3574.                                         $alertColorLower strtolower(trim($alert['alertColor']));
  3575.                                         if ($alertColorLower === 'red') {
  3576.                                             $backgroundColor '#f6000017';
  3577.                                             $borderColor '#F60000';
  3578.                                             $textColor '#F60000';
  3579.                                         } elseif ($alertColorLower === 'orange') {
  3580.                                             $backgroundColor '#ff66001f';
  3581.                                             $borderColor '#FF6600';
  3582.                                             $textColor '#FF6600';
  3583.                                         } elseif ($alertColorLower === 'yellow') {
  3584.                                             $backgroundColor '#fcb82526';
  3585.                                             $borderColor '#FCB825';
  3586.                                             $textColor '#FCB825';
  3587.                                         }
  3588.                                     }
  3589.                                     $alert['backgroundColor'] = $backgroundColor;
  3590.                                     $alert['borderColor'] = $borderColor;
  3591.                                     $alert['textColor'] = $textColor;
  3592.                                     $alert['tokenURL'] = null;
  3593.                                     $alert['mannedAlertDatailUrl'] = null;
  3594.                                     $html $templating->render('web2print/_manned_alert_notification_ar.html.twig'$alert);
  3595.                                     // $email = $currentUser->email;
  3596.                                     // $param = ['user' => $currentUser, 'message' => $html, 'url' => null];
  3597.                                     // $mailSent = $emailService->sendMail($param, "abdul.muqeet@centric.ae", '/email/alert_notification', $subject);
  3598.                                     $purpose EWS_MESSAGE;
  3599.                                     $mailSent $this->c2Service->sendDefaultEmail($_ENV['EWS_MAIL_TEMPLATE'], $alert['id'], $currentUser->email$html$subject$purpose);
  3600.                                     if ($mailSent) {
  3601.                                         $status "sent";
  3602.                                         $this->saveEmailStatus($currentUser->email$currentUser->firstName ' ' $currentUser->lastName$alert$status);
  3603.                                     } else {
  3604.                                         $status "not sent";
  3605.                                         $this->saveEmailStatus($currentUser->email$currentUser->firstName ' ' $currentUser->lastName$alert$status);
  3606.                                     }
  3607.                                 }
  3608.                             }
  3609.                         }
  3610.                     }
  3611.                 }
  3612.             }
  3613.         }
  3614.         return $mailSent;
  3615.     }
  3616.     private function saveEmailStatus($userEmail$userName$alert$emailStatus)
  3617.     {
  3618.         // Get all governorates names in string seprated by ,
  3619.         $nameEnArray array_column($alert["governorates"], "nameEn");
  3620.         $nameEnString implode(", "$nameEnArray);
  3621.         $nameArArray array_column($alert["governorates"], "nameAr");
  3622.         $nameArString implode(", "$nameArArray);
  3623.         $ewsEmailStatus = new FetchSentEwsEmail();
  3624.         $ewsEmailStatus->setParent(\Pimcore\Model\DataObject\Service::createFolderByPath("/Other/FetchSentEwsEmail/"));
  3625.         $ewsEmailStatus->setKey(\Pimcore\Model\Element\Service::getValidKey($userEmail "-" strtotime("now") . "-" rand(110000), 'object'));
  3626.         $ewsEmailStatus->setEwsId($alert['id']);
  3627.         $ewsEmailStatus->setUserName($userName);
  3628.         $ewsEmailStatus->setEmail($userEmail);
  3629.         //$ewsEmailStatus->setLocationName($locationName);
  3630.         $ewsEmailStatus->setAlertType($alert['alertTypeEn'], 'en');
  3631.         $ewsEmailStatus->setAlertType($alert['alertTypeAr'], 'ar');
  3632.         $ewsEmailStatus->setAlertStatus($alert['alertStatusEn'], 'en');
  3633.         $ewsEmailStatus->setAlertStatus($alert['alertStatusAr'], 'ar');
  3634.         $ewsEmailStatus->setRegionName($alert['regionEn'], 'en');
  3635.         $ewsEmailStatus->setRegionName($alert['regionAR'], 'ar');
  3636.         $ewsEmailStatus->setGovernorateNames($nameEnString'en');
  3637.         $ewsEmailStatus->setGovernorateNames($nameArString'ar');
  3638.         $ewsEmailStatus->setStatus($emailStatus);
  3639.         $ewsEmailStatus->setPublished(true);
  3640.         $ewsEmailStatus->save();
  3641.         return $ewsEmailStatus;
  3642.     }
  3643.     public function getEwsNotificationEmailStatus($params$paginator$translator)
  3644.     {
  3645.         $result = [];
  3646.         try {
  3647.             $pageSize = isset($params['page_size']) ? $params['page_size'] : LIMIT_PER_PAGE;
  3648.             $page = isset($params['page']) ? $params['page'] : 1;
  3649.             $notificationId $params["id"] ? $params["id"] : null;
  3650.             $listing = new FetchSentEwsEmail\Listing();
  3651.             $listing->setCondition("ewsId = ? ", [$notificationId]);
  3652.             $paginator $paginator->paginate(
  3653.                 $listing,
  3654.                 $page,
  3655.                 $pageSize
  3656.             );
  3657.             if ($paginator->count() > 0) {
  3658.                 foreach ($paginator as $EwsNotificationEmail) {
  3659.                     $result[] = [
  3660.                         "ewsId" => $EwsNotificationEmail->getEwsId(),
  3661.                         "userName" => $EwsNotificationEmail->getUserName(),
  3662.                         "Email" => $EwsNotificationEmail->getEmail(),
  3663.                         "locationName" => $EwsNotificationEmail->getLocationName(),
  3664.                         "alertType" => $EwsNotificationEmail->getAlertType('en'),
  3665.                         "alertTypeAr" => $EwsNotificationEmail->getAlertType('ar'),
  3666.                         "alertStatus" => $EwsNotificationEmail->getAlertStatus('en'),
  3667.                         "alertStatusAr" => $EwsNotificationEmail->getAlertStatus('ar'),
  3668.                         "regionName" => $EwsNotificationEmail->getRegionName('en'),
  3669.                         "regionNameAr" => $EwsNotificationEmail->getRegionName('ar'),
  3670.                         "governorateNames" => explode(","$EwsNotificationEmail->getGovernorateNames('en')),
  3671.                         "governorateNamesAr" => explode(","$EwsNotificationEmail->getGovernorateNames('ar')),
  3672.                         "emailStatus" => $EwsNotificationEmail->getStatus(),
  3673.                         "emailStatusAr" => $translator->trans($EwsNotificationEmail->getStatus(), [], null"ar"),
  3674.                     ];
  3675.                 }
  3676.             }
  3677.             return ["success" => TRUE"data" => $result"paginationVariables" => $paginator->getPaginationData()];
  3678.         } catch (\Exception $ex) {
  3679.             return new \Exception($ex->getMessage());
  3680.         }
  3681.         return $result;
  3682.     }
  3683.     public function getOtherLocations()
  3684.     {
  3685.         $response = [];
  3686.         $otherLocations = new DataObject\OtherLocation\Listing();
  3687.         $otherLocations $otherLocations->load();
  3688.         if ($otherLocations) {
  3689.             foreach ($otherLocations as $otherLocation) {
  3690.                 $response[] = [
  3691.                     "id" => $otherLocation->getOtherLocationId(),
  3692.                     "nameEn" => $otherLocation->getName('en'),
  3693.                     "nameAr" => $otherLocation->getName('ar')
  3694.                 ];
  3695.             }
  3696.         }
  3697.         return ["success" => true"data" => $response];
  3698.     }
  3699.     public function getEwsNotificationByByRegionName($regionName)
  3700.     {
  3701.         $db Db::get();
  3702.         $selectedLocalities $db->fetchAll("SELECT * FROM `object_collection_AddressComponents_location` WHERE (`addressKey` = 'locality' || `addressKey` = 'administrative_area_level_1') and (TRIM(REPLACE(`addressValue`,'Province','')) = '" $regionName "')");
  3703.         return $selectedLocalities;
  3704.     }
  3705.     public function ewsAnalytics($params$connection$lang): array
  3706.     {
  3707.         $response = [];
  3708.         $status = isset($params['status']) ? $params['status'] : "";
  3709.         $isManned null;
  3710.         if (isset($params['isManned']) && $params['isManned'] == true) {
  3711.             $isManned true;
  3712.         } elseif (isset($params['isManned']) && $params['isManned'] == false) {
  3713.             $isManned false;
  3714.         }
  3715.         if (isset($params['region']) && !empty($params['region'])) {
  3716.             $region Region::getByRegionId($params['region'], true);
  3717.             $response[] = $this->ewsAnalyticsCount($region$status$connection$lang$isManned);
  3718.         } else {
  3719.             $regions = new Region\Listing();
  3720.             foreach ($regions as $region) {
  3721.                 $response[] = $this->ewsAnalyticsCount($region$status$connection$lang$isManned);
  3722.             }
  3723.         }
  3724.         return $response;
  3725.     }
  3726.     public function ewsAnalyticsCount($region$status$connection$lang$isManned)
  3727.     {
  3728.         $regionNameEn $region->getName("en");
  3729.         $regionNameAr $region->getName("ar");
  3730.         $statuses = ["active""archived""expired"];
  3731.         $count = ['regionEn' => $regionNameEn"regionAr" => $regionNameAr];
  3732.         if (in_array($status$statuses)) {
  3733.             $count[$status] = $this->getStatusData($region$status$connection$isManned);
  3734.         } else {
  3735.             foreach ($statuses as $s) {
  3736.                 $count[$s] = $this->getStatusData($region$s$connection$isManned);
  3737.             }
  3738.         }
  3739.         return $count;
  3740.     }
  3741.     public function getStatusData($region$status$connection$isManned)
  3742.     {
  3743.         $totalNotifications $this->statusCount($region$status$isManned);
  3744.         $alertTypeCount $this->alertCount($region$status$connection$isManned);
  3745.         return [
  3746.             'total_notifications' => $totalNotifications,
  3747.             'alert_type_count' => $alertTypeCount,
  3748.         ];
  3749.     }
  3750.     public function alertCount($region$status$connection$isManned)
  3751.     {
  3752.         $currentTimestamp time(); // Current UNIX timestamp
  3753.         $twentyFourHoursAgoTimestamp strtotime('-24 hours'); // 24 hours ago timestamp
  3754.         $claim_report_sql $connection->createQueryBuilder();
  3755.         $claim_report_sql
  3756.             ->select('oa.Color AS color''COUNT(oq.oo_id) AS count')
  3757.             ->from('object_ews_notification''oq')
  3758.             ->join('oq''object_query_alert_type''oa''oq.alertType__id = oa.oo_id')
  3759.             ->andWhere('oq.region__id = :regionId')
  3760.             ->andWhere('oq.status = :status');
  3761.         if ($isManned !== null && $isManned == true) {
  3762.             $claim_report_sql->andWhere('oq.isManned = 1');
  3763.         } elseif ($isManned !== null && $isManned == false) {
  3764.             $claim_report_sql->andWhere('oq.isManned = 0');
  3765.         }
  3766.         $claim_report_sql->andWhere('oq.o_published = 1'); // added new line
  3767.         // Initialize parameters array
  3768.         $params = [
  3769.             'regionId' => $region->getId(),
  3770.             'status' => $status,
  3771.         ];
  3772.         // Add conditions based on status
  3773.         if ($status == "active") {
  3774.             $claim_report_sql->andWhere('oq.endDate > :currentTimestamp');
  3775.             $params['currentTimestamp'] = $currentTimestamp;
  3776.         } else if ($status == "expired") {
  3777.             $claim_report_sql->andWhere('oq.endDate < :currentTimestamp AND oq.endDate > :twentyFourHoursAgoTimestamp');
  3778.             $params['currentTimestamp'] = $currentTimestamp;
  3779.             $params['twentyFourHoursAgoTimestamp'] = $twentyFourHoursAgoTimestamp;
  3780.         } else if ($status == "archived") {
  3781.             $claim_report_sql->andWhere('oq.endDate < :twentyFourHoursAgoTimestamp');
  3782.             $params['twentyFourHoursAgoTimestamp'] = $twentyFourHoursAgoTimestamp;
  3783.         }
  3784.         // Set all parameters at once
  3785.         $claim_report_sql->setParameters($params);
  3786.         $claim_report_sql->groupBy('color');
  3787.         $claimReportstmt $claim_report_sql->execute();
  3788.         $results $claimReportstmt->fetchAllAssociative();
  3789.         // Fetch all possible colors
  3790.         $alertTypes = new AlertType\Listing();
  3791.         $allColors = [];
  3792.         foreach ($alertTypes as $alertType) {
  3793.             if ($alertType) {
  3794.                 $allColors[] = $alertType->getColor();
  3795.             }
  3796.         }
  3797.         if (empty($allColors)) {
  3798.             $allColors = ["yellow""orange""red"];
  3799.         }
  3800.         // Create an associative array to store counts by color
  3801.         $countsByColor = [];
  3802.         foreach ($results as $result) {
  3803.             $countsByColor[$result['color']] = $result['count'];
  3804.         }
  3805.         // Merge counts for existing colors with counts for all colors, including those with count 0
  3806.         $finalResult = [];
  3807.         foreach ($allColors as $color) {
  3808.             $finalResult[] = [
  3809.                 "color" => $color,
  3810.                 "count" => isset($countsByColor[$color]) ? $countsByColor[$color] : 0
  3811.             ];
  3812.         }
  3813.         return $finalResult;
  3814.     }
  3815.     public function statusCount($region$status$isManned)
  3816.     {
  3817.         $notifications = new EwsNotification\Listing();
  3818.         $currentTimestamp time(); // Get the current UNIX timestamp
  3819.         $twentyFourHoursAgoTimestamp strtotime('-24 hours'); // 24 hours ago timestamp
  3820.         $conditions "region__id = ? AND status = ?";
  3821.         $params = [$region->getId(), $status];
  3822.         if ($status == "active") {
  3823.             $conditions .= " AND endDate > ?";
  3824.             $params[] = $currentTimestamp;
  3825.         } else if ($status == "expired") {
  3826.             $conditions .= " AND endDate < ? AND endDate > ?";
  3827.             $params[] = $currentTimestamp;
  3828.             $params[] = $twentyFourHoursAgoTimestamp;
  3829.         } else if ($status == "archived") {
  3830.             $conditions .= " AND endDate < ?";
  3831.             $params[] = $twentyFourHoursAgoTimestamp;
  3832.         }
  3833.         $notifications->addConditionParam($conditions$params);
  3834.         // Add condition for manned notifications
  3835.         if ($isManned !== null) {
  3836.             $notifications->addConditionParam("isManned = ?", [$isManned]);
  3837.         }
  3838.         return $notifications->count();
  3839.     }
  3840.     public function searchEwsNotification($params$lang$paginator$translator): array
  3841.     {
  3842.         $response = [];
  3843.         $pageSize = isset($params['page_size']) ? $params['page_size'] : LIMIT_PER_PAGE;
  3844.         $page = isset($params['page']) ? $params['page'] : 1;
  3845.         $notificationList = new DataObject\EwsNotification\Listing();
  3846.         // If unpublished parameter exists and is true, show unpublished items
  3847.         // Handle boolean true, string "true", or integer 1
  3848.         $isUnpublished = isset($params['unpublished']) && (
  3849.             $params['unpublished'] === true ||
  3850.             $params['unpublished'] === 'true' ||
  3851.             $params['unpublished'] === ||
  3852.             $params['unpublished'] === '1'
  3853.         );
  3854.         if ($isUnpublished) {
  3855.             $notificationList->setUnpublished(true);
  3856.             $notificationList->filterByPublished(false);
  3857.         }
  3858.         // No else needed - default behavior already shows published only
  3859.         if (isset($params['allowedNotificationTypes'])) {
  3860.             $notificationList->addConditionParam("notificationType IN (?)", [$params["allowedNotificationTypes"]]);
  3861.         }
  3862.         if (isset($params['isManned']) && $params['isManned'] == true) {
  3863.             $notificationList->addConditionParam("isManned = 1");
  3864.         } elseif (isset($params['isManned']) && $params['isManned'] == false) {
  3865.             $notificationList->addConditionParam("isManned = 0");
  3866.         }
  3867.         if (isset($params['id']) && !empty($params['id'])) {
  3868.             $idz $params['id'];
  3869.             $notificationList->addConditionParam("oo_id IN (?)", [$idz]);
  3870.         }
  3871.         if (isset($params['alertAction']) && !empty($params['alertAction'])) {
  3872.             $alertActionIds = [];
  3873.             $alertActionList = new DataObject\AlertAction\Listing();
  3874.             $alertActionList->addConditionParam("alertActionId IN (?)", [$params['alertAction']]);
  3875.             foreach ($alertActionList as $alertAction) {
  3876.                 $alertActionIds[] = $alertAction->getId();
  3877.             }
  3878.             $alertActionids implode(","$alertActionIds);
  3879.             $notificationList->addConditionParam("alertAction REGEXP CONCAT('(^|,)(', REPLACE('$alertActionids',',', '|'), ')(,|$)')");
  3880.         }
  3881.         if (isset($params['alertType']) && !empty($params['alertType'])) {
  3882.             $alertTypeIds = [];
  3883.             $alertTypeList = new DataObject\AlertType\Listing();
  3884.             $alertTypeList->addConditionParam("alertTypeId IN (?)", [$params['alertType']]);
  3885.             foreach ($alertTypeList as $alertType) {
  3886.                 $alertTypeIds[] = $alertType->getId();
  3887.             }
  3888.             $notificationList->addConditionParam("alertType__id IN (?)", [$alertTypeIds]);
  3889.         }
  3890.         // if (isset($params['phenomena']) && !empty($params['phenomena'])) {
  3891.         //     $phenomenaIds = [];
  3892.         //     $phenomenaList = new DataObject\PhenomenaList\Listing();
  3893.         //     $phenomenaList->addConditionParam("phenomenalistid IN (?)", [$params['phenomena']]);
  3894.         //     foreach ($phenomenaList as $phenomena) {
  3895.         //         $phenomenaIds[] = $phenomena->getId();
  3896.         //     }
  3897.         //     $notificationList->addConditionParam("weatherPhenomenon__id IN (?)", [$phenomenaIds]);
  3898.         // }
  3899.         if (!empty($params['phenomena'])) {
  3900.             $alertStatusSql null;
  3901.             foreach ($params['phenomena'] as $alertStatus) {
  3902.                 $alertStatus \Pimcore\Model\DataObject\AlertStatus::getByAlertStatusId($alertStatustrue);
  3903.                 if ($alertStatus) {
  3904.                     $alertStatusSql .= "alertStatus__id = " $alertStatus->getId() . " OR ";
  3905.                 }
  3906.             }
  3907.             $notificationList->addConditionParam("(" substr($alertStatusSql0, -3) . ")");
  3908.         }
  3909.         if (isset($params['region']) && !empty($params['region'])) {
  3910.             $regionIds = [];
  3911.             $regionList = new DataObject\Region\Listing();
  3912.             $regionList->addConditionParam("regionId IN (?)", [$params['region']]);
  3913.             foreach ($regionList as $region) {
  3914.                 $regionIds[] = $region->getId();
  3915.             }
  3916.             $notificationList->addConditionParam("region__id IN (?)", [$regionIds]);
  3917.         }
  3918.         if (isset($params['governate']) && !empty($params['governate'])) {
  3919.             $governatesIds = [];
  3920.             $governateList = new DataObject\Governorate\Listing();
  3921.             $governateList->addConditionParam("governoteId IN (?)", [$params['governate']]);
  3922.             foreach ($governateList as $governate) {
  3923.                 $governatesIds[] = $governate->getId();
  3924.             }
  3925.             $governatesIds implode(","$governatesIds);
  3926.             $notificationList->addConditionParam("governorate REGEXP CONCAT('(^|,)(', REPLACE('$governatesIds',',', '|'), ')(,|$)')");
  3927.         }
  3928.         if (isset($params['status'][0]) && !empty($params['status'][0])) {
  3929.             $currentTimestamp time();
  3930.             $twentyFourHoursAgo strtotime('-24 hours');
  3931.             if ($params['status'][0] == "active" && (!isset($params['status'][1]) || empty($params['status'][1]))) {
  3932.                 // Filter notifications with an end date in the future or equal to current time
  3933.                 $notificationList->addConditionParam("status != ?", ["ended"]);
  3934.                 $notificationList->filterByEndDate($currentTimestamp">=");
  3935.             } else if ($params['status'][0] == "expired" && (!isset($params['status'][1]) || empty($params['status'][1]))) {
  3936.                 // Filter notifications with an end date in the past, but not older than 24 hours
  3937.                 $notificationList->addConditionParam("status != ?", ["ended"]);
  3938.                 $notificationList->filterByEndDate($currentTimestamp"<");
  3939.                 $notificationList->filterByEndDate($twentyFourHoursAgo">=");
  3940.             } else if ($params['status'][0] == "archived" && (!isset($params['status'][1]) || empty($params['status'][1]))) {
  3941.                 // Filter notifications with an end date older than 24 hours
  3942.                 $notificationList->addConditionParam("endDate < ? OR alertEndDate < ? ", [$twentyFourHoursAgo$twentyFourHoursAgo]);
  3943.                 // $notificationList->filterByEndDate($twentyFourHoursAgo, "<");
  3944.             } else if ($params['status'][0] == "ended" && (!isset($params['status'][1]) || empty($params['status'][1]))) {
  3945.                 $notificationList->filterByAlertEndDate($twentyFourHoursAgo">");
  3946.                 $notificationList->filterByStatus("ended");
  3947.                 // $notificationList->addConditionParam("status = ?",["ended"]);
  3948.             } else if (isset($params['status'][1]) && !empty($params['status'][1])) {
  3949.                 $conditions = [];
  3950.                 if (in_array('ended'$params['status'])) {
  3951.                     $conditions[] = "(status = 'ended' AND AlertEndDate > {$twentyFourHoursAgo})";
  3952.                 }
  3953.                 if (in_array('active'$params['status'])) {
  3954.                     $conditions[] = "(status != 'ended' AND endDate >= {$currentTimestamp})";
  3955.                 }
  3956.                 if (in_array('expired'$params['status'])) {
  3957.                     $conditions[] = "(status != 'ended' AND endDate < {$currentTimestamp} AND endDate >= {$twentyFourHoursAgo})";
  3958.                 }
  3959.                 if (in_array('archived'$params['status'])) {
  3960.                     $conditions[] = "(status != 'ended' AND (endDate < {$twentyFourHoursAgo} OR alertEndDate < {$twentyFourHoursAgo}))";
  3961.                 }
  3962.                 if (!empty($conditions)) {
  3963.                     $notificationList->addConditionParam('(' implode(' OR '$conditions) . ')');
  3964.                 }
  3965.             }
  3966.         } else {
  3967.             // If no status is provided, return 0 records (only for published records)
  3968.             // For unpublished records, don't require status filter
  3969.             if (!$isUnpublished) {
  3970.                 $notificationList->addConditionParam("1 = 0");
  3971.             }
  3972.         }
  3973.         $notificationList->setLocale($lang);
  3974.         if (isset($params['searchId']) && !empty($params['searchId'])) {
  3975.             $notificationList->addConditionParam(
  3976.                 "(ewsSearchId LIKE ?)",
  3977.                 [
  3978.                     '%' $params['searchId'] . '%'
  3979.                 ]
  3980.             );
  3981.         }
  3982.         if (isset($params['alertHazard']) && !empty($params['alertHazard'])) {
  3983.             $alertHazardIds = [];
  3984.             $alertHazardList = new DataObject\AlertHazard\Listing();
  3985.             $alertHazardList->addConditionParam("alertHazardId IN (?)", [$params['alertHazard']]);
  3986.             foreach ($alertHazardList as $alertHazard) {
  3987.                 $alertHazardIds[] = $alertHazard->getId();
  3988.             }
  3989.             $alertHazardIds implode(","$alertHazardIds);
  3990.             $notificationList->addConditionParam("alertHazard REGEXP CONCAT('(^|,)(', REPLACE('$alertHazardIds',',', '|'), ')(,|$)')");
  3991.         }
  3992.         if (isset($params['municipality']) && !empty($params['municipality'])) {
  3993.             $municipalityIds = [];
  3994.             $municipalityList = new DataObject\Municipality\Listing();
  3995.             $municipalityList->addConditionParam("municipalityid IN (?)", [$params['municipality']]);
  3996.             foreach ($municipalityList as $municipality) {
  3997.                 $municipalityIds[] = $municipality->getId();
  3998.             }
  3999.             $municipalityIds implode(","$municipalityIds);
  4000.             $notificationList->addConditionParam("municipality REGEXP CONCAT('(^|,)(', REPLACE('$municipalityIds',',', '|'), ')(,|$)')");
  4001.         }
  4002.         if (isset($params['startDate']) && !empty($params['startDate'])) {
  4003.             $notificationList->filterByStartDate(strtotime($params['startDate']), ">=");
  4004.         }
  4005.         if (isset($params['endDate']) && !empty($params['endDate'])) {
  4006.             $notificationList->filterByEndDate(strtotime($params['endDate']), "<=");
  4007.         }
  4008.         if (isset($params['fromDate']) && isset($params['toDate']) && !empty($params['fromDate']) && !empty($params['toDate'])) {
  4009.             $fromDate = new \DateTime($params['fromDate']);
  4010.             $toDate = new \DateTime($params['toDate']);
  4011.             // Ensure the dates are in the correct format
  4012.             $fromDateStr strtotime($fromDate->format('Y-m-d H:i:s'));
  4013.             $toDateStr strtotime($toDate->format('Y-m-d') . ' 23:59:59');
  4014.             $notificationList->addConditionParam(
  4015.                 "(o_creationDate >= ? AND o_creationDate <= ?)",
  4016.                 [$fromDateStr$toDateStr]
  4017.             );
  4018.         }
  4019.         $notifications $notificationList->load();
  4020.         $sortedData = [];
  4021.         foreach ($notifications as $notification) {
  4022.             $regionEn $notification->getRegion() ? $notification->getRegion()->getName("en") : "";
  4023.             $startDate $notification->getStartDate() ? $notification->getStartDate()->format('Y-m-d H:i:s') : "";
  4024.             $endDate $notification->getEndDate() ? $notification->getEndDate()->format('Y-m-d H:i:s') : "";
  4025.             $alertStatusEn $notification->getWeatherPhenomenon()?->getTitle("en");
  4026.             $creationDate $notification->getCreationDate();
  4027.             $searchIdEn $notification->getEwsSearchId("en");
  4028.             $searchIdAr $notification->getEwsSearchId("ar");
  4029.             $sortedData[] = [
  4030.                 'notification' => $notification,
  4031.                 'regionName' => $regionEn,
  4032.                 'alertStatus' => $alertStatusEn,
  4033.                 'startDate' => $startDate,
  4034.                 'endDate' => $endDate,
  4035.                 'creationDate' => $creationDate// Add creation date to sorting criteria
  4036.                 'searchIdEn' => $searchIdEn,
  4037.                 'searchIdAr' => $searchIdAr
  4038.             ];
  4039.         }
  4040.         // Define default sort orders and detect if any sort parameter is provided
  4041.         $sortOrderRegion $params['sortByRegion'] ?? null;
  4042.         $sortOrderStartDate $params['sortByStartDate'] ?? null;
  4043.         $sortOrderEndDate $params['sortByEndDate'] ?? null;
  4044.         $sortOrderAlertStatus $params['sortByAlertStatus'] ?? null;
  4045.         $sortOrderSearchIdEn $params['sortBySearchIdEn'] ?? null;
  4046.         $sortOrderSearchIdAr $params['sortBySearchIdAr'] ?? null;
  4047.         $sortOrderCreatedAt $params['sortByCreated'] ?? null;
  4048.         // Check if any sort parameter is provided
  4049.         $anySortParamProvided $sortOrderRegion || $sortOrderStartDate || $sortOrderEndDate || $sortOrderAlertStatus || $sortOrderSearchIdEn || $sortOrderSearchIdAr;
  4050.         usort($sortedData, function ($a$b) use ($sortOrderRegion$sortOrderStartDate$sortOrderEndDate$sortOrderAlertStatus$sortOrderSearchIdEn$sortOrderSearchIdAr$sortOrderCreatedAt$anySortParamProvided) {
  4051.             if (!$anySortParamProvided || $sortOrderCreatedAt) {
  4052.                 if ($sortOrderCreatedAt == null) {
  4053.                     $sortOrderCreatedAt "desc";
  4054.                 }
  4055.                 // Default to sorting by creation date if no sort parameter is provided
  4056.                 $compareCreatedAt strcmp($a['creationDate'], $b['creationDate']) * ($sortOrderCreatedAt === 'desc' ? -1);
  4057.                 if ($compareCreatedAt !== 0) return $compareCreatedAt// Assuming 'asc' as default sort order for creation date
  4058.             }
  4059.             // Else, sort based on the provided parameters
  4060.             if ($sortOrderRegion) {
  4061.                 $compareRegion strcmp($a['regionName'], $b['regionName']) * ($sortOrderRegion === 'desc' ? -1);
  4062.                 if ($compareRegion !== 0) return $compareRegion;
  4063.             }
  4064.             if ($sortOrderSearchIdEn) {
  4065.                 $compareSearchIdEn strcmp($a['searchIdEn'], $b['searchIdEn']) * ($sortOrderSearchIdEn === 'desc' ? -1);
  4066.                 if ($compareSearchIdEn !== 0) return $compareSearchIdEn;
  4067.             }
  4068.             if ($sortOrderSearchIdAr) {
  4069.                 $compareSearchIdAr strcmp($a['searchIdAr'], $b['searchIdAr']) * ($sortOrderSearchIdAr === 'desc' ? -1);
  4070.                 if ($compareSearchIdAr !== 0) return $compareSearchIdAr;
  4071.             }
  4072.             if ($sortOrderAlertStatus) {
  4073.                 $compareAlertStatus strcmp($a['alertStatus'], $b['alertStatus']) * ($sortOrderAlertStatus === 'desc' ? -1);
  4074.                 if ($compareAlertStatus !== 0) return $compareAlertStatus;
  4075.             }
  4076.             if ($sortOrderStartDate) {
  4077.                 $compareStartDate strcmp($a['startDate'], $b['startDate']) * ($sortOrderStartDate === 'desc' ? -1);
  4078.                 if ($compareStartDate !== 0) return $compareStartDate;
  4079.             }
  4080.             if ($sortOrderEndDate) {
  4081.                 return strcmp($a['endDate'], $b['endDate']) * ($sortOrderEndDate === 'desc' ? -1);
  4082.             }
  4083.             // If all provided parameters are equal, or no specific sort orders are provided, default to creation date
  4084.             return ($b['creationDate'] ?? 0) - ($a['creationDate'] ?? 0);
  4085.         });
  4086.         $sortedNotifications array_map(function ($item) {
  4087.             return $item['notification'];
  4088.         }, $sortedData);
  4089.         if (isset($params['dashboard']) && $params['dashboard'] === 'dashboard') {
  4090.             return [
  4091.                 'success' => true,
  4092.                 'data' => array_values(array_map(
  4093.                     static fn ($notification) => (int) $notification->getId(),
  4094.                     $sortedNotifications
  4095.                 )),
  4096.             ];
  4097.         }
  4098.         // Extract the sorted notifications
  4099.         if ($paginator == null) {
  4100.             // if ($sortedNotifications->count()) {
  4101.             foreach ($sortedNotifications as $notification) {
  4102.                 // if($notification->getPublished())
  4103.                 $item $this->createNotificationFormat($notification$translator);
  4104.                 $item['history'] = false;
  4105.                 if ($this->ewsNotificationHasVersionUpdateHistory($notification)) {
  4106.                     $item['history'] = true;
  4107.                 }
  4108.                 
  4109.                 $response[] = $item;
  4110.             }
  4111.             // }
  4112.             return ["success" => TRUE"data" => $response];
  4113.         }
  4114.         $paginator $paginator->paginate(
  4115.             $sortedNotifications,
  4116.             $page,
  4117.             $pageSize
  4118.         );
  4119.         $decodedJwtToken  = isset($params['decodedJwtToken']) ?? null;
  4120.         $userPermission = isset($params['userPermission']) ?? null;
  4121.         // if ($sortedNotifications->count()) {
  4122.         foreach ($paginator as $notification) {
  4123.             // if($notification->getPublished())
  4124.             $item $this->createNotificationFormat($notification$translator$decodedJwtToken$userPermission);
  4125.             $item['history'] = false;
  4126.             if ($this->ewsNotificationHasVersionUpdateHistory($notification)) {
  4127.                 $item['history'] = true;
  4128.             }
  4129.             $response[] = $item;
  4130.         }
  4131.         // }
  4132.         return ["data" => $response"paginationVariables" => $paginator->getPaginationData()];
  4133.     }
  4134.     public function archiveNotification($params$translator): array
  4135.     {
  4136.         $id $params['id'] ?? null;
  4137.         if (!$id) {
  4138.             throw new \Exception("EWS id is required");
  4139.         }
  4140.         $ewsNotification \Pimcore\Model\DataObject::getById($id);
  4141.         if (!$ewsNotification instanceof \Pimcore\Model\DataObject\EwsNotification) {
  4142.             // throw new \Exception("Ews notification not found");
  4143.             return ['success' => false'message' => $translator->trans('ews_notification_not_found')];
  4144.         }
  4145.         if (isset($params['status']) && $params['status']) {
  4146.             $ewsNotification->setStatus("archived");
  4147.         }
  4148.         $ewsNotification->save(["versionNote" => "Update"]);
  4149.         if ($this->shouldDispatchPublicPortalEwsWebhook($ewsNotification)) {
  4150.             $this->dispatchPublicPortalEwsWebhookIfConfigured((int) $ewsNotification->getId());
  4151.         }
  4152.         return ['success' => true'message' => $translator->trans('archive_ews_notification'), "notification_id" => $ewsNotification->getId()];
  4153.     }
  4154.     public function getId(): ?UuidV4
  4155.     {
  4156.         $uuid UuidV4::v4();
  4157.         return $uuid;
  4158.     }
  4159.     public function getPhenomenaList()
  4160.     {
  4161.         $phenomenas = new \Pimcore\Model\DataObject\PhenomenaList\Listing();
  4162.         $phenomenas->setOrderKey("o_creationDate");
  4163.         $phenomenas->setOrder("desc");
  4164.         $phenomenas->load();
  4165.         $response = [];
  4166.         if (count($phenomenas) > 0) {
  4167.             foreach ($phenomenas as $phenomena) {
  4168.                 $response[] = [
  4169.                     "id" => $phenomena->getPhenomenaListId(),
  4170.                     "nameEN" => $phenomena->getTitle('en'),
  4171.                     "nameAr" => $phenomena->getTitle('ar'),
  4172.                 ];
  4173.             }
  4174.             return ["success" => true"data" => $response];
  4175.         }
  4176.         return ["success" => false"data" => $response];
  4177.     }
  4178.     public function getHazardListByAlertStatusId($statusId,  $regionIds = [], $lang "en")
  4179.     {
  4180.         $response = [];
  4181.         $excludeWavesRising false;
  4182.         // Check if any region is landlocked
  4183.         if (!empty($regionIds) && is_array($regionIds)) {
  4184.             foreach ($regionIds as $regionId) {
  4185.                 $region \Pimcore\Model\DataObject\Region::getByRegionId($regionIdtrue);
  4186.                 if ($region && $region->getIsLandLocked()) {
  4187.                     $excludeWavesRising true;
  4188.                     break;
  4189.                 }
  4190.             }
  4191.         }
  4192.         $alertStatus \Pimcore\Model\DataObject\AlertStatus::getByAlertStatusId($statusIdtrue);
  4193.         if ($alertStatus instanceof \Pimcore\Model\DataObject\AlertStatus) {
  4194.             if ($alertStatus && $alertStatus->getAlertHazards()) {
  4195.                 foreach ($alertStatus->getAlertHazards() as $value) {
  4196.                     $alertHazardObj $value->getLocalizedfields()->getItems();
  4197.                     // Exclude "Waves rising" if needed
  4198.                     if (
  4199.                         $excludeWavesRising &&
  4200.                         isset($alertHazardObj['en']['name']) &&
  4201.                         strtolower(trim($alertHazardObj['en']['name'])) === 'waves rising'
  4202.                     ) {
  4203.                         continue;
  4204.                     }
  4205.                     $response[] = [
  4206.                         "id" => (int)$value->getAlertHazardId(),
  4207.                         "nameEn" => $alertHazardObj['en']['name'],
  4208.                         "nameAr" => $alertHazardObj['ar']['name']
  4209.                     ];
  4210.                 }
  4211.             } else {
  4212.                 return ["success" => false"message" => "No Hazard is set"];
  4213.             }
  4214.         } else {
  4215.             return ["success" => false"message" => "Invalid alert status id"];
  4216.         }
  4217.         return ["success" => true"data" => $response];
  4218.     }
  4219.     // public function getHazardListByAlertStatusId($statusId, $lang = "en")
  4220.     // {
  4221.     //     $response = [];
  4222.     //     $alertPhenomena = \Pimcore\Model\DataObject\PhenomenaList::getByPhenomenaListId($statusId, true);
  4223.     //     if ($alertPhenomena instanceof \Pimcore\Model\DataObject\PhenomenaList) {
  4224.     //         if ($alertPhenomena && $alertPhenomena->getAlertHazards()) {
  4225.     //             foreach ($alertPhenomena->getAlertHazards() as  $value) {
  4226.     //                 $alertHazardObj = $value->getLocalizedfields()->getItems();
  4227.     //                 $response[] = ["id" => (int)$value->getAlertHazardId(), "nameEn" => $alertHazardObj['en']['name'], "nameAr" => $alertHazardObj['ar']['name']];
  4228.     //             }
  4229.     //         } else {
  4230.     //             return ["success" => false, "message" => "No Hazard is set"];
  4231.     //         }
  4232.     //     } else {
  4233.     //         return ["success" => false, "message" => "Invalid alert status id"];
  4234.     //     }
  4235.     //     return ["success" => true, "data" => $response];
  4236.     // }
  4237.     public function getOtherGovernoratesByRegion($regionId)
  4238.     {
  4239.         $response = [];
  4240.         $region \Pimcore\Model\DataObject\Region::getByRegionId($regionIdtrue);
  4241.         if (!$region) {
  4242.             throw new \Exception("Region not available");
  4243.         }
  4244.         $otherGovernorates = new DataObject\EwsOtherLocation\Listing();
  4245.         //$otherGovernorates->filterByRegionId($region);
  4246.         $otherGovernorates $otherGovernorates->load();
  4247.         if ($otherGovernorates) {
  4248.             foreach ($otherGovernorates as $governorate) {
  4249.                 $region $governorate->getregionId();
  4250.                 $regionData = [];
  4251.                 if (!empty($region)) {
  4252.                     $regionData = [
  4253.                         "id" => $region->getRegionId(),
  4254.                         "nameEn" => $region->getName('en'),
  4255.                         "nameAr" => $region->getName('ar'),
  4256.                         "longitude" => $region->getLongitude(),
  4257.                         "latitude" => $region->getLongitude()
  4258.                     ];
  4259.                 }
  4260.                 $response[] = [
  4261.                     "id" => $governorate->getgovernoteId(),
  4262.                     "nameEn" => $governorate->getName('en'),
  4263.                     "nameAr" => $governorate->getName('ar'),
  4264.                     "regionId" => $regionData,
  4265.                     "longitude" => $governorate->getLongitude(),
  4266.                     "latitude" => $governorate->getLatitude(),
  4267.                     "isHidden" => $governorate->getisHidden(),
  4268.                     "IsMunicipality" => $governorate->getIsMunicipality(),
  4269.                     "MunicipalityID" => $governorate->getMunicipalityID()
  4270.                 ];
  4271.             }
  4272.         }
  4273.         return ["success" => true"data" => $response];
  4274.     }
  4275.     public function createEwsReportUserGroup($params$translator)
  4276.     {
  4277.         $response = [];
  4278.         $checkGroup EwsAndReportUserGroup::getByName($params['name'], true);
  4279.         if ($checkGroup instanceof EwsAndReportUserGroup) {
  4280.             return ["success" => false"message" => $translator->trans("group_name_already_exists"), "name" => $params['name']];
  4281.         }
  4282.         $ewsReportUserGroup = new DataObject\EwsAndReportUserGroup();
  4283.         $ewsReportUserGroup->setParent(\Pimcore\Model\DataObject\Service::createFolderByPath("/Notification/userGroups/" $params['type'] . "/"));
  4284.         $ewsReportUserGroup->setKey(\Pimcore\Model\Element\Service::getValidKey($params['name'] . "-" strtotime("now") . "-" rand(110000), 'object''object'));
  4285.         $ewsReportUserGroup->setName(strip_tags($params['name']));
  4286.         $ewsReportUserGroup->setJsonData(json_encode($params['data']));
  4287.         $ewsReportUserGroup->setGroupType($params['type']);
  4288.         $ewsReportUserGroup->setPublished(true);
  4289.         $ewsReportUserGroup->save();
  4290.         return ["success" => true"message" => $translator->trans("ews_report_user_group_created")];
  4291.     }
  4292.     public function updateEwsReportUserGroup($params$translator)
  4293.     {
  4294.         $response = [];
  4295.         $updateGroup EwsAndReportUserGroup::getById($params['id']);
  4296.         if (!$updateGroup instanceof EwsAndReportUserGroup) {
  4297.             return ["success" => false"message" => $translator->trans('ews_report_user_group_not_found'), "id" => $params['id']];
  4298.         }
  4299.         if (isset($params['name']) && !empty($params['name']) && $updateGroup->getName() != $params['name']) {
  4300.             $checkGroup EwsAndReportUserGroup::getByName($params['name'], true);
  4301.             if ($checkGroup instanceof EwsAndReportUserGroup) {
  4302.                 return ["success" => false"message" => $translator->trans("group_name_already_exists"), "name" => $params['name']];
  4303.             }
  4304.             $updateGroup->setName(strip_tags($params['name']));
  4305.             $updateGroup->setKey(\Pimcore\Model\Element\Service::getValidKey($params['name'] . "-" strtotime("now") . "-" rand(110000), 'object''object'));
  4306.         }
  4307.         if (isset($params['data']) && !empty($params['data'])) {
  4308.             $updateGroup->setJsonData(json_encode($params['data']));
  4309.         }
  4310.         if (isset($params['type']) && !empty($params['type'])) {
  4311.             $updateGroup->setGroupType($params['type']);
  4312.             $updateGroup->setParent(\Pimcore\Model\DataObject\Service::createFolderByPath("/Notification/userGroups/" $params['type'] . "/"));
  4313.         }
  4314.         $updateGroup->setJsonData(json_encode($params['data']));
  4315.         $updateGroup->setGroupType($params['type']);
  4316.         $updateGroup->setPublished(true);
  4317.         $updateGroup->save();
  4318.         return ["success" => true"message" => $translator->trans("ews_report_user_group_updated")];
  4319.     }
  4320.     public function deleteEwsReportUserGroup($params$translator)
  4321.     {
  4322.         $response = [];
  4323.         $deleteGroup EwsAndReportUserGroup::getById($params['id']);
  4324.         if (!$deleteGroup instanceof EwsAndReportUserGroup) {
  4325.             return ["success" => false"message" => $translator->trans('ews_report_user_group_not_found'), "id" => $params['id']];
  4326.         }
  4327.         $deleteGroup->delete();
  4328.         return ["success" => true"message" => $translator->trans("ews_report_user_group_deleted")];
  4329.     }
  4330.     public function listEwsReportUserGroup($params$paginator$translator)
  4331.     {
  4332.         $data = [];
  4333.         $listing = new EwsAndReportUserGroup\Listing();
  4334.         $listing->setOrderKey("oo_id");
  4335.         $listing->setOrder("DESC");
  4336.         if (isset($params['type']) && !empty($params['type'])) {
  4337.             $listing->addConditionParam("groupType = ?", [$params['type']]);
  4338.         }
  4339.         if (isset($params['search']) && !empty($params['search'])) {
  4340.             $listing->addConditionParam("name LIKE ? ", ['%' $params['search'] . '%']);
  4341.         }
  4342.         $pageSize = isset($params['page_size']) ? $params['page_size'] : LIMIT_PER_PAGE;
  4343.         $page = isset($params['page']) ? $params['page'] : 1;
  4344.         $paginator $paginator->paginate(
  4345.             $listing,
  4346.             $page,
  4347.             $pageSize
  4348.         );
  4349.         foreach ($paginator as $group) {
  4350.             $data[] = [
  4351.                 "id" => $group->getId(),
  4352.                 "name" => $group->getName(),
  4353.                 "type" => $group->getGroupType(),
  4354.                 "data" => json_decode($group->getJsonData(), true)
  4355.             ];
  4356.         }
  4357.         if (count($data) > 0) {
  4358.             return ["success" => true"data" => $data"paginationVariables" => $paginator->getPaginationData()];
  4359.         } else {
  4360.             return ["success" => false"message" => $translator->trans('no_ews_report_user_group_found')];
  4361.         }
  4362.     }
  4363.     public function getSortCriteria()
  4364.     {
  4365.         // This is a placeholder; you'll need to adapt this based on how you can access the region's name or other criteria
  4366.         return $this->getRegion()->getName();
  4367.     }
  4368.     public function getCenters($governorateId null$lang 'en')
  4369.     {
  4370.         $response = [];
  4371.         $centers = new DataObject\Centers\Listing();
  4372.         if (is_array($governorateId)) {
  4373.             $govIdsArr = [];
  4374.             foreach ($governorateId as $govId) {
  4375.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($govIdtrue);
  4376.                 if ($governorate) {
  4377.                     array_push($govIdsArr$governorate->getId());
  4378.                 }
  4379.             }
  4380.             $centers->setCondition("governorate__id IN (" implode(", "$govIdsArr) . ")");
  4381.         } else {
  4382.             if ($governorateId) {
  4383.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($governorateIdtrue);
  4384.                 $centers->setCondition("governorate__id = ?", [$governorate->getId()]);
  4385.             }
  4386.         }
  4387.         $centers $centers->load();
  4388.         if ($centers) {
  4389.             foreach ($centers as $center) {
  4390.                 $response[] = [
  4391.                     'id' => $center->getId(),
  4392.                     "object_id" => $center->getObjectId(),
  4393.                     "nameEn" => $center->getName('en'),
  4394.                     "nameAr" => $center->getName('ar'),
  4395.                     "longitude" => $center->getLongitude(),
  4396.                     "latitude" => $center->getLatitude(),
  4397.                     "governate" => $center->getGovernorate()->getGovernoteId()
  4398.                 ];
  4399.             }
  4400.         }
  4401.         // Determine sorting field based on language
  4402.         $sortField 'nameEn'// Default sorting by English
  4403.         if (isset($lang) && strtolower($lang) === 'ar') {
  4404.             $sortField 'nameAr'// Sorting by Arabic
  4405.         }
  4406.         // Sort manually using usort()
  4407.         usort($response, function ($a$b) use ($sortField) {
  4408.             return strcmp($a[$sortField], $b[$sortField]);
  4409.         });
  4410.         return ["success" => true"data" => $response];
  4411.     }
  4412.     public function getDistricts($governorateId null$lang 'en')
  4413.     {
  4414.         $response = [];
  4415.         $districts = new DataObject\District\Listing();
  4416.         if (is_array($governorateId)) {
  4417.             $govIdsArr = [];
  4418.             foreach ($governorateId as $govId) {
  4419.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($govIdtrue);
  4420.                 if ($governorate) {
  4421.                     array_push($govIdsArr$governorate->getId());
  4422.                 }
  4423.             }
  4424.             $districts->setCondition("governorate__id IN (" implode(", "$govIdsArr) . ")");
  4425.         } else {
  4426.             if ($governorateId) {
  4427.                 $governorate \Pimcore\Model\DataObject\Governorate::getByGovernoteId($governorateIdtrue);
  4428.                 $districts->setCondition("governorate__id = ?", [$governorate->getId()]);
  4429.             }
  4430.         }
  4431.         $districts $districts->load();
  4432.         if ($districts) {
  4433.             foreach ($districts as $district) {
  4434.                 $response[] = [
  4435.                     "id" => $district->getId(),
  4436.                     "object_id" => $district->getObjectId(),
  4437.                     "nameEn" => $district->getName('en'),
  4438.                     "nameAr" => $district->getName('ar'),
  4439.                     "longitude" => $district->getLongitude(),
  4440.                     "latitude" => $district->getLatitude(),
  4441.                     "governate" => $district->getGovernorate()->getGovernoteId()
  4442.                 ];
  4443.             }
  4444.         }
  4445.         // Determine sorting field based on language
  4446.         $sortField 'nameEn'// Default sorting by English
  4447.         if (isset($lang) && strtolower($lang) === 'ar') {
  4448.             $sortField 'nameAr'// Sorting by Arabic
  4449.         }
  4450.         // Sort manually using usort()
  4451.         usort($response, function ($a$b) use ($sortField) {
  4452.             return strcmp($a[$sortField], $b[$sortField]);
  4453.         });
  4454.         return ["success" => true"data" => $response];
  4455.     }
  4456.     public function createEwsPolygon($params$translator)
  4457.     {
  4458.         $coordinates $params['coordinates'] ?? null;
  4459.         $alertTypeId $params['alertTypeId'] ?? null;
  4460.         $expiryDate $params['expiryDate'] ?? null;
  4461.         $type $params['type'] ?? null;
  4462.         // if (!is_array($coordinates) || empty($coordinates)) {
  4463.         //     return ['success' => false, 'message' => $translator->trans('invalid_coordinates')];
  4464.         // }
  4465.         $ewsPolygon = new DataObject\EwsPolygon();
  4466.         $ewsPolygon->setParent(DataObject\Service::createFolderByPath('/Notification/EWSPolygon'));
  4467.         $ewsPolygon->setKey(uniqid("polygon-"));
  4468.         $ewsPolygon->setCoordinates(json_encode($coordinates));
  4469.         if (!empty($type)) {
  4470.             $ewsPolygon->setEwsAlertType($type);
  4471.         }
  4472.         if ($alertTypeId && !empty($alertTypeId)) {
  4473.             $alertType DataObject\AlertType::getByAlertTypeId($alertTypeId1);
  4474.             if (!$alertType) {
  4475.                 return ['success' => false'message' => $translator->trans('invalid_alert_type')];
  4476.             }
  4477.             $ewsPolygon->setAlertType($alertType);
  4478.         }
  4479.         if ($expiryDate && !empty($expiryDate)) {
  4480.             $ewsPolygon->setExpire(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$expiryDate));
  4481.         }
  4482.         // $ewsPolygon->setPublished(true);
  4483.         try {
  4484.             $ewsPolygon->save();
  4485.         } catch (\Exception $e) {
  4486.             return ['success' => false'message' => $translator->trans('error_saving_polygon'), 'error' => $e->getMessage()];
  4487.         }
  4488.         return [
  4489.             'success' => true,
  4490.             'message' => $translator->trans('ews_polygon_added_successfully'),
  4491.             'polygonId' => $ewsPolygon->getId(),
  4492.             'type' => $ewsPolygon->getEwsAlertType()
  4493.         ];
  4494.     }
  4495.     public function updateEwsPolygon($params$translator)
  4496.     {
  4497.         $ewsPolygon EwsPolygon::getById($params['id'] ?? null);
  4498.         if (!$ewsPolygon instanceof EwsPolygon) {
  4499.             return [
  4500.                 "success" => false,
  4501.                 "message" => $translator->trans('ews_polygon_not_found'),
  4502.                 "id" => $params['id'] ?? null
  4503.             ];
  4504.         }
  4505.         // Update coordinates if provided and valid
  4506.         if (isset($params['coordinates'])) {
  4507.             $coordinates $params['coordinates'];
  4508.             // if (!is_array($coordinates) || empty($coordinates)) {
  4509.             //     return ['success' => false, 'message' => $translator->trans('invalid_coordinates')];
  4510.             // }
  4511.             $ewsPolygon->setCoordinates(json_encode($coordinates));
  4512.         }
  4513.         if (!empty($params['type'])) {
  4514.             $ewsPolygon->setEwsAlertType($params['type']);
  4515.         }
  4516.         // Update alert type if provided
  4517.         if (!empty($params['alertTypeId'])) {
  4518.             $alertTypeId $params['alertTypeId'];
  4519.             $alertType DataObject\AlertType::getByAlertTypeId($alertTypeId1);
  4520.             if (!$alertType) {
  4521.                 return ['success' => false'message' => $translator->trans('invalid_alert_type')];
  4522.             }
  4523.             $ewsPolygon->setAlertType($alertType);
  4524.         }
  4525.         // Update EWS alerts if provided
  4526.         if (!empty($params['alertIds']) && is_array($params['alertIds'])) {
  4527.             $alertIds $params['alertIds'];
  4528.             $ewsNotifications = [];
  4529.             foreach ($alertIds as $alertId) {
  4530.                 $ewsNotification \Pimcore\Model\DataObject\EwsNotification::getById($alertIdtrue);
  4531.                 if (!$ewsNotification instanceof \Pimcore\Model\DataObject\EwsNotification) {
  4532.                     return [
  4533.                         'success' => false,
  4534.                         'message' => $translator->trans('ews_notification_not_found'),
  4535.                         'invalid_alert_id' => $alertId
  4536.                     ];
  4537.                 }
  4538.                 $ewsNotification->setPolygon($ewsPolygon);
  4539.                 $ewsNotification->save();
  4540.                 $ewsNotifications[] = $ewsNotification;
  4541.             }
  4542.             $ewsPolygon->setEwsAlerts(array_unique($ewsNotifications));
  4543.         }
  4544.         if (!empty($params['expiryDate'])) {
  4545.             $ewsPolygon->setExpire(\Carbon\Carbon::createFromFormat('Y-m-d H:i:s'$params['expiryDate']));
  4546.         }
  4547.         if (!empty($params['isPublished']) && $params['isPublished'] == true) {
  4548.             $ewsPolygon->setPublished(true);
  4549.         }
  4550.         // Try saving
  4551.         try {
  4552.             $ewsPolygon->save();
  4553.         } catch (\Exception $e) {
  4554.             return [
  4555.                 'success' => false,
  4556.                 'message' => $translator->trans('error_saving_polygon'),
  4557.                 'error' => $e->getMessage()
  4558.             ];
  4559.         }
  4560.         return [
  4561.             'success' => true,
  4562.             'message' => $translator->trans('ews_polygon_updated_successfully'),
  4563.             'polygonId' => $ewsPolygon->getId()
  4564.         ];
  4565.     }
  4566.     public function listEwsPolygon($params$paginator$translator)
  4567.     {
  4568.         $ewsId $params['id'] ?? null;
  4569.         $isPublished array_key_exists('isPublished'$params) ? (bool) $params['isPublished'] : true;
  4570.         \Pimcore\Model\DataObject::setHideUnpublished(false);
  4571.         $listing = new EwsPolygon\Listing();
  4572.         // $listing->addConditionParam('expire >= ?', [time()]);
  4573.         if ($ewsId) {
  4574.             $listing->addConditionParam('o_id = ?', [(int)$ewsId]);
  4575.         }
  4576.         $listing->setOrderKey("oo_id");
  4577.         $listing->setOrder("DESC");
  4578.         $data = [];
  4579.         foreach ($listing as $polygon) {
  4580.             if ($polygon->getPublished() !== $isPublished) {
  4581.                 continue; // skip if it doesn't match the required published status
  4582.             }
  4583.             $alertIds = [];
  4584.             $isEnded false;
  4585.             $alerts $polygon->getEwsAlerts();
  4586.             if (is_array($alerts)) {
  4587.                 foreach ($alerts as $alert) {
  4588.                     if ($alert instanceof \Pimcore\Model\DataObject\EwsNotification) {
  4589.                         $alertIds[] = $alert->getId();
  4590.                         $isEnded $alert->getStatus() == "ended" true false;
  4591.                     }
  4592.                 }
  4593.             }
  4594.             $alertTypeObj $polygon->getAlertType();
  4595.             $alertTypeId = ($alertTypeObj instanceof \Pimcore\Model\DataObject\AlertType)
  4596.                 ? $alertTypeObj->getAlertTypeId()
  4597.                 : null;
  4598.             $data[] = [
  4599.                 "id" => $polygon->getId(),
  4600.                 "coordinates" => json_decode($polygon->getCoordinates(), true),
  4601.                 "alertType" => $alertTypeId,
  4602.                 "type" => $polygon->getEwsAlertType(),
  4603.                 "alerts" => $alertIds,
  4604.                 "expiryDate" => $polygon->getExpire(),
  4605.                 "isExpired" => $polygon->getExpire() ? $polygon->getExpire()->getTimestamp() < time() : null,
  4606.                 "isEnded" => $isEnded
  4607.             ];
  4608.         }
  4609.         if (!empty($data)) {
  4610.             return [
  4611.                 "success" => true,
  4612.                 "data" => $data,
  4613.             ];
  4614.         }
  4615.         return [
  4616.             "success" => false,
  4617.             "message" => $translator->trans('polygons_not_found'),
  4618.         ];
  4619.     }
  4620.     public function endEwsNotification($params$translator$logger)
  4621.     {
  4622.         $result = [];
  4623.         $viewNotification DataObject\EwsNotification::getById($params['id'], true);
  4624.         if (!$viewNotification instanceof EwsNotification) {
  4625.             return [
  4626.                 "success" => false,
  4627.                 "message" => $translator->trans('ews_notification_not_found'),
  4628.                 "id" => $params['id'] ?? null
  4629.             ];
  4630.         }
  4631.         //set ews search Id
  4632.         $viewNotification->setStatus("ended");
  4633.         $viewNotification->setAlertEndDate(\Carbon\Carbon::now());
  4634.         // Persist the browser-rendered image (x_img) as an asset on the alert so the Twitter end
  4635.         // listener posts that instead of generating one with wkhtmltoimage. The save() below
  4636.         // fires the listener, which reads endXAttachment.
  4637.         if (!empty($params['x_img'])) {
  4638.             $dataUri $params['x_img'];
  4639.             if (($commaPos strpos($dataUri',')) !== false && stripos($dataUri'base64') !== false) {
  4640.                 $dataUri substr($dataUri$commaPos 1);
  4641.             }
  4642.             $binary base64_decode(strtr($dataUri' ''+'), true);
  4643.             if ($binary !== false && $binary !== '') {
  4644.                 $endAsset \App\Lib\Utility::createAsset(
  4645.                     $binary,
  4646.                     $viewNotification->getId() . '_' time() . '_end_ews_alert.png',
  4647.                     'END EWS Twitter Images'
  4648.                 );
  4649.                 if ($endAsset) {
  4650.                     $viewNotification->setEndXAttachment($endAsset);
  4651.                 }
  4652.             }
  4653.         }
  4654.         // DIAGNOSTIC (temporary): confirm the end method runs and log the values the
  4655.         // TwitterEwsEndEventListner gates on. Written to Pimcore ApplicationLogger so it
  4656.         // lands in the application_logs table. Remove once the End-tweet issue is resolved.
  4657.         \Pimcore\Log\ApplicationLogger::getInstance('END-DIAG'true)->info(sprintf(
  4658.             '[END-DIAG] endEwsNotification saving EWS ID %s | published=%s | enableTwitter=%s | status=%s',
  4659.             $viewNotification->getId(),
  4660.             var_export($viewNotification->isPublished(true), true),
  4661.             var_export($viewNotification->getEnableTwitterNotification(), true),
  4662.             var_export($viewNotification->getStatus(), true)
  4663.         ));
  4664.         $viewNotification->save(["versionNote" => "End alert"]);
  4665.         $viewNotification->save();
  4666.         if ($this->shouldDispatchPublicPortalEwsWebhook($viewNotification)) {
  4667.             $this->dispatchPublicPortalEwsWebhookIfConfigured((int) $viewNotification->getId(), $logger);
  4668.         }
  4669.         $alert $this->createNotificationFormat($viewNotification$translator$params['decodedJwtToken'], $params['userPermission']);
  4670.         if ($viewNotification) {
  4671.             // Email the same user groups that were selected when the alert was published/edited.
  4672.             $userGroupIds array_map(
  4673.                 fn($group) => $group->getId(),
  4674.                 $viewNotification->getUserGroup()
  4675.             );
  4676.             // Ensure you use 'php' to execute the command.
  4677.             $command = ['php''bin/console''app:send-early-warning-alert-email''--alertId=' $viewNotification->getId(), '--endAlert=1'];
  4678.             if (!empty($userGroupIds)) {
  4679.                 $command[] = '--userGroupIds=' json_encode($userGroupIds);
  4680.             }
  4681.             $process = new Process($command);
  4682.             $process->setWorkingDirectory(PIMCORE_PROJECT_ROOT);
  4683.             // Set timeout to 5 minutes (300 seconds) or null for no timeout
  4684.             $process->setTimeout(300); // or use null for unlimited time
  4685.             try {
  4686.                 $process->mustRun();
  4687.                 $result['success'] = true;
  4688.                 $logger->info("End EwsNotification command executed successfully: " $process->getOutput());
  4689.                 $result['message'] = $process->getOutput();
  4690.             } catch (ProcessFailedException $exception) {
  4691.                 $logger->error("End EwsNotification command failed: " $exception->getMessage());
  4692.                 return ['success' => false'message' => $exception->getMessage()];
  4693.             }
  4694.         }
  4695.         return ["success" => true"message" => $translator->trans("ews_notification_end")];
  4696.     }
  4697.     /**
  4698.      * Persist distinct message texts for a user so create/update history is preserved.
  4699.      * Re-using the same message bumps last_used_at; new text inserts a new row.
  4700.      *
  4701.      * @param array<string, string> $messages keyed by locale (e.g. en/ar)
  4702.      */
  4703.     public function saveUserMessageSuggestions(int $userId, array $messages): void
  4704.     {
  4705.         if ($userId <= || empty($messages)) {
  4706.             return;
  4707.         }
  4708.         $db Db::get();
  4709.         $now = (new \DateTime())->format('Y-m-d H:i:s');
  4710.         foreach ($messages as $language => $text) {
  4711.             $text trim((string) $text);
  4712.             if ($text === '') {
  4713.                 continue;
  4714.             }
  4715.             $hash hash('sha256'$text);
  4716.             $existingId $db->fetchOne(
  4717.                 'SELECT id FROM ews_user_message_suggestions WHERE user_id = ? AND message_hash = ?',
  4718.                 [$userId$hash]
  4719.             );
  4720.             if ($existingId) {
  4721.                 $db->executeStatement(
  4722.                     'UPDATE ews_user_message_suggestions SET last_used_at = ?, language = COALESCE(?, language) WHERE id = ?',
  4723.                     [$nowis_string($language) ? $language null$existingId]
  4724.                 );
  4725.             } else {
  4726.                 $db->insert('ews_user_message_suggestions', [
  4727.                     'user_id' => $userId,
  4728.                     'message' => $text,
  4729.                     'message_hash' => $hash,
  4730.                     'language' => is_string($language) ? $language null,
  4731.                     'last_used_at' => $now,
  4732.                     'created_at' => $now,
  4733.                 ]);
  4734.             }
  4735.         }
  4736.     }
  4737.     /**
  4738.      * Search previously used EWS notification messages for an authenticated user.
  4739.      * Only searches when $q has at least 2 characters.
  4740.      * Reads from ews_user_message_suggestions so updates keep prior message history.
  4741.      *
  4742.      * @return array{success: bool, data: list<array{message: string, last_used_at: string|null, created_at: string|null}>}
  4743.      */
  4744.     public function getUserMessageSuggestions(int $userIdstring $qint $limit 10): array
  4745.     {
  4746.         $q trim($q);
  4747.         $limit max(1min(10$limit));
  4748.         if (mb_strlen($q) < 2) {
  4749.             return ['success' => true'data' => []];
  4750.         }
  4751.         $db Db::get();
  4752.         $qb $db->createQueryBuilder();
  4753.         $qb->select([
  4754.                 'message',
  4755.                 'last_used_at',
  4756.                 'created_at',
  4757.             ])
  4758.             ->from('ews_user_message_suggestions')
  4759.             ->where('user_id = :userId')
  4760.             ->andWhere('message LIKE :search')
  4761.             ->orderBy('last_used_at''DESC')
  4762.             ->addOrderBy('created_at''DESC')
  4763.             ->setMaxResults($limit)
  4764.             ->setParameter('userId'$userId)
  4765.             ->setParameter('search''%' $q '%');
  4766.         $rows $qb->execute()->fetchAllAssociative();
  4767.         $data = [];
  4768.         foreach ($rows as $row) {
  4769.             $data[] = [
  4770.                 'message' => (string) $row['message'],
  4771.                 'last_used_at' => $row['last_used_at'] ?? null,
  4772.                 'created_at' => $row['created_at'] ?? null,
  4773.             ];
  4774.         }
  4775.         return ['success' => true'data' => $data];
  4776.     }
  4777.     /**
  4778.      * Build Alert History rows for Twitter image from Update versions.
  4779.      * Multiple alert actions on the same version are clubbed into one row.
  4780.      *
  4781.      * Mapping:
  4782.      * Change Location → Update Alert Location
  4783.      * Change Type → Lower Alert / Raise Alert
  4784.      * Change Date or Time → Update Alert Period
  4785.      * Change Status → Update Alert Status
  4786.      */
  4787.     public function buildAlertHistoryImageEntries(
  4788.         EwsNotification $notification,
  4789.         bool $includeCurrentState true,
  4790.         $translator null,
  4791.         string $lang 'en'
  4792.     ): array {
  4793.         $lang strtolower($lang) === 'ar' 'ar' 'en';
  4794.         $timezone = new \DateTimeZone(defined('TIMEZONE') ? TIMEZONE 'Asia/Riyadh');
  4795.         $snapshots = [];
  4796.         $versions $notification->getVersions() ?: [];
  4797.         foreach ($versions as $version) {
  4798.             if ($version->getNote() !== 'Update') {
  4799.                 continue;
  4800.             }
  4801.             $view $version->loadData();
  4802.             if (!$view instanceof EwsNotification) {
  4803.                 continue;
  4804.             }
  4805.             $actions $this->extractRelevantHistoryActions($view$translator$lang);
  4806.             if ($actions === []) {
  4807.                 continue;
  4808.             }
  4809.             $snapshots[] = [
  4810.                 'ts' => (int) ($version->getDate() ?: $view->getModificationDate() ?: $view->getCreationDate()),
  4811.                 'obj' => $view,
  4812.                 'actions' => $actions,
  4813.             ];
  4814.         }
  4815.         // Include the in-memory updated state (not yet saved as a version).
  4816.         if ($includeCurrentState) {
  4817.             $currentActions $this->extractRelevantHistoryActions($notification$translator$lang);
  4818.             if ($currentActions !== []) {
  4819.                 $snapshots[] = [
  4820.                     'ts' => time(),
  4821.                     'obj' => $notification,
  4822.                     'actions' => $currentActions,
  4823.                 ];
  4824.             }
  4825.         }
  4826.         usort($snapshots, static fn($a$b) => ($a['ts'] ?? 0) <=> ($b['ts'] ?? 0));
  4827.         $entries = [];
  4828.         $hasNewAlert false;
  4829.         $titleSeparator $lang === 'ar' ' | ' ' | ';
  4830.         foreach ($snapshots as $index => $snap) {
  4831.             /** @var EwsNotification $obj */
  4832.             $obj $snap['obj'];
  4833.             $previous $index $snapshots[$index 1]['obj'] : null;
  4834.             $timestamp $this->formatAlertHistoryTimestamp((int) $snap['ts'], $timezone);
  4835.             $color $this->resolveAlertHistoryDotColor($obj);
  4836.             $clubbedActions = [];
  4837.             $titleParts = [];
  4838.             $detailParts = [];
  4839.             $primaryKey '';
  4840.             foreach ($snap['actions'] as $actionMeta) {
  4841.                 $key $actionMeta['key'];
  4842.                 $title $actionMeta['title'];
  4843.                 if ($key === 'New Alert') {
  4844.                     $hasNewAlert true;
  4845.                 }
  4846.                 if ($primaryKey === '') {
  4847.                     $primaryKey $key;
  4848.                 }
  4849.                 $detail $this->buildAlertHistoryActionDetail(
  4850.                     $key,
  4851.                     $obj,
  4852.                     $previous instanceof EwsNotification $previous null,
  4853.                     $timezone,
  4854.                     $translator,
  4855.                     $lang
  4856.                 );
  4857.                 $titleParts[] = $title;
  4858.                 if ($detail !== '') {
  4859.                     $detailParts[] = $detail;
  4860.                 }
  4861.                 $clubbedActions[] = [
  4862.                     'title' => $title,
  4863.                     'detail' => $detail,
  4864.                     'key' => $key,
  4865.                 ];
  4866.             }
  4867.             $entries[] = [
  4868.                 'timestamp' => $timestamp,
  4869.                 'title' => implode($titleSeparator$titleParts),
  4870.                 'titleKey' => $primaryKey,
  4871.                 'detail' => implode("\n"$detailParts),
  4872.                 'actions' => $clubbedActions,
  4873.                 'color' => $color,
  4874.                 'ts' => (int) $snap['ts'],
  4875.             ];
  4876.         }
  4877.         if (!$hasNewAlert) {
  4878.             $createdTs = (int) ($notification->getCreationDate() ?: time());
  4879.             $newAlertTitle $this->transAlertHistory('New Alert'$translator$lang);
  4880.             array_unshift($entries, [
  4881.                 'timestamp' => $this->formatAlertHistoryTimestamp($createdTs$timezone),
  4882.                 'title' => $newAlertTitle,
  4883.                 'titleKey' => 'New Alert',
  4884.                 'detail' => '',
  4885.                 'actions' => [
  4886.                     [
  4887.                         'title' => $newAlertTitle,
  4888.                         'detail' => '',
  4889.                         'key' => 'New Alert',
  4890.                     ],
  4891.                 ],
  4892.                 'color' => $this->resolveAlertHistoryDotColor($notification),
  4893.                 'ts' => $createdTs,
  4894.             ]);
  4895.         }
  4896.         usort($entries, static fn($a$b) => ($b['ts'] ?? 0) <=> ($a['ts'] ?? 0));
  4897.         return array_values($entries);
  4898.     }
  4899.     /**
  4900.      * Render Alert History HTML and convert to a PNG asset with wkhtmltoimage.
  4901.      */
  4902.     public function generateAlertHistoryTwitterImage(
  4903.         EwsNotification $notification,
  4904.         $templating,
  4905.         $logger null,
  4906.         bool $includeCurrentState true,
  4907.         $translator null,
  4908.         string $lang 'en'
  4909.     ): ?\Pimcore\Model\Asset {
  4910.         $lang strtolower($lang) === 'ar' 'ar' 'en';
  4911.         $entries $this->buildAlertHistoryImageEntries($notification$includeCurrentState$translator$lang);
  4912.         if ($entries === []) {
  4913.             return null;
  4914.         }
  4915.         $timezone = new \DateTimeZone(defined('TIMEZONE') ? TIMEZONE 'Asia/Riyadh');
  4916.         $startedTs = (int) ($notification->getCreationDate() ?: time());
  4917.         foreach ($entries as $entry) {
  4918.             $isNewAlert = ($entry['titleKey'] ?? '') === 'New Alert';
  4919.             if (!$isNewAlert && !empty($entry['actions']) && is_array($entry['actions'])) {
  4920.                 foreach ($entry['actions'] as $action) {
  4921.                     if (($action['key'] ?? '') === 'New Alert') {
  4922.                         $isNewAlert true;
  4923.                         break;
  4924.                     }
  4925.                 }
  4926.             }
  4927.             if ($isNewAlert && !empty($entry['ts'])) {
  4928.                 $startedTs min($startedTs, (int) $entry['ts']);
  4929.             }
  4930.         }
  4931.         $html $templating->render('image_templates/alert-history.html.twig', [
  4932.             'lang' => $lang,
  4933.             'isRtl' => $lang === 'ar',
  4934.             'labels' => [
  4935.                 'alertHistory' => $this->transAlertHistory('Alert History'$translator$lang),
  4936.             ],
  4937.             'startedOn' => $this->formatAlertHistoryStartedOn($startedTs$timezone$translator$lang),
  4938.             'entries' => $entries,
  4939.         ]);
  4940.         $container \Pimcore::getContainer();
  4941.         /** @var SnappyImage $snappyImage */
  4942.         $snappyImage $container->has('knp_snappy.image')
  4943.             ? $container->get('knp_snappy.image')
  4944.             : $container->get(SnappyImage::class);
  4945.         $snappyImage->setOption('enable-local-file-access'true);
  4946.         $snappyImage->setOption('width''776');
  4947.         $snappyImage->setOption('format''png');
  4948.         $snappyImage->setOption('quality'100);
  4949.         $imageBinary $snappyImage->getOutputFromHtml($html);
  4950.         if (!$imageBinary) {
  4951.             if ($logger) {
  4952.                 $logger->error('wkhtmltoimage returned empty output for EWS alert history image');
  4953.             }
  4954.             return null;
  4955.         }
  4956.         return \App\Lib\Utility::createAsset(
  4957.             $imageBinary,
  4958.             uniqid(''true) . '_ews_alert_history_' $lang '.png',
  4959.             'EWS Twitter Images'
  4960.         );
  4961.     }
  4962.     /**
  4963.      * @return array<int, array{key: string, title: string}>
  4964.      */
  4965.     private function extractRelevantHistoryActions(EwsNotification $notification$translator nullstring $lang 'en'): array
  4966.     {
  4967.         $titles = [];
  4968.         $seenKeys = [];
  4969.         $actions $notification->getAlertAction() ?: [];
  4970.         foreach ($actions as $action) {
  4971.             if (!$action) {
  4972.                 continue;
  4973.             }
  4974.             $key $this->mapAlertActionToHistoryTitle((string) $action->getName('en'));
  4975.             if ($key === null || isset($seenKeys[$key])) {
  4976.                 continue;
  4977.             }
  4978.             $seenKeys[$key] = true;
  4979.             $localized = (string) ($action->getName($lang) ?: '');
  4980.             $titles[] = [
  4981.                 'key' => $key,
  4982.                 'title' => $localized !== '' $localized $this->transAlertHistory($key$translator$lang),
  4983.             ];
  4984.         }
  4985.         return $titles;
  4986.     }
  4987.     /**
  4988.      * Map stored alert-action / change labels to canonical history keys.
  4989.      */
  4990.     private function mapAlertActionToHistoryTitle(string $nameEn): ?string
  4991.     {
  4992.         $haystack strtolower(trim($nameEn));
  4993.         if ($haystack === '') {
  4994.             return null;
  4995.         }
  4996.         if (str_contains($haystack'new alert')) {
  4997.             return 'New Alert';
  4998.         }
  4999.         if (str_contains($haystack'raise')) {
  5000.             return 'Raise Alert';
  5001.         }
  5002.         if (str_contains($haystack'lower')) {
  5003.             return 'Lower Alert';
  5004.         }
  5005.         if (str_contains($haystack'location') || $haystack === 'change location') {
  5006.             return 'Update Alert Location';
  5007.         }
  5008.         if (
  5009.             str_contains($haystack'period')
  5010.             || str_contains($haystack'change date')
  5011.             || str_contains($haystack'change time')
  5012.             || str_contains($haystack'date or time')
  5013.             || (str_contains($haystack'time') && str_contains($haystack'update'))
  5014.         ) {
  5015.             return 'Update Alert Period';
  5016.         }
  5017.         if (str_contains($haystack'status') || $haystack === 'change status') {
  5018.             return 'Update Alert Status';
  5019.         }
  5020.         return null;
  5021.     }
  5022.     private function buildAlertHistoryActionDetail(
  5023.         string $titleKey,
  5024.         EwsNotification $current,
  5025.         ?EwsNotification $previous,
  5026.         \DateTimeZone $timezone,
  5027.         $translator null,
  5028.         string $lang 'en'
  5029.     ): string {
  5030.         if (!$previous) {
  5031.             return '';
  5032.         }
  5033.         if ($titleKey === 'Raise Alert' || $titleKey === 'Lower Alert') {
  5034.             $from $this->resolveAlertSeverityLabel($previous$lang$translator);
  5035.             $to $this->resolveAlertSeverityLabel($current$lang$translator);
  5036.             if ($from !== '' && $to !== '' && $from !== $to) {
  5037.                 return $this->transAlertHistory(
  5038.                     'Severity changed from %from% to %to%',
  5039.                     $translator,
  5040.                     $lang,
  5041.                     ['%from%' => $from'%to%' => $to]
  5042.                 );
  5043.             }
  5044.         }
  5045.         if ($titleKey === 'Update Alert Location') {
  5046.             $currentNames $this->extractGovernorateNamesForHistory($current$lang);
  5047.             $previousNames $this->extractGovernorateNamesForHistory($previous$lang);
  5048.             $added array_values(array_diff($currentNames$previousNames));
  5049.             $removed array_values(array_diff($previousNames$currentNames));
  5050.             $nameSeparator $lang === 'ar' '، ' ', ';
  5051.             $parts = [];
  5052.             if ($added) {
  5053.                 $parts[] = $this->transAlertHistory(
  5054.                     'Added governorates: %names%',
  5055.                     $translator,
  5056.                     $lang,
  5057.                     ['%names%' => implode($nameSeparator$added)]
  5058.                 );
  5059.             }
  5060.             if ($removed) {
  5061.                 $parts[] = $this->transAlertHistory(
  5062.                     'Removed governorates: %names%',
  5063.                     $translator,
  5064.                     $lang,
  5065.                     ['%names%' => implode($nameSeparator$removed)]
  5066.                 );
  5067.             }
  5068.             return implode('. '$parts);
  5069.         }
  5070.         if ($titleKey === 'Update Alert Period') {
  5071.             $from $this->formatAlertHistoryPeriodRange($previous$timezone);
  5072.             $to $this->formatAlertHistoryPeriodRange($current$timezone);
  5073.             if ($from !== $to) {
  5074.                 return $this->transAlertHistory(
  5075.                     'Time changed from %from% to %to%',
  5076.                     $translator,
  5077.                     $lang,
  5078.                     ['%from%' => $from'%to%' => $to]
  5079.                 );
  5080.             }
  5081.         }
  5082.         if ($titleKey === 'Update Alert Status') {
  5083.             $from = (string) ($previous->getAlertStatus()?->getName($lang) ?? $previous->getAlertStatus()?->getName('en') ?? '');
  5084.             $to = (string) ($current->getAlertStatus()?->getName($lang) ?? $current->getAlertStatus()?->getName('en') ?? '');
  5085.             if ($from !== '' && $to !== '' && $from !== $to) {
  5086.                 return $this->transAlertHistory(
  5087.                     'Status changed from %from% to %to%',
  5088.                     $translator,
  5089.                     $lang,
  5090.                     ['%from%' => $from'%to%' => $to]
  5091.                 );
  5092.             }
  5093.         }
  5094.         return '';
  5095.     }
  5096.     /**
  5097.      * @return string[]
  5098.      */
  5099.     private function extractGovernorateNamesForHistory(EwsNotification $notificationstring $lang 'en'): array
  5100.     {
  5101.         $names = [];
  5102.         $fallback $lang === 'ar' 'en' 'ar';
  5103.         foreach ($notification->getGovernorate() ?: [] as $gov) {
  5104.             if ($gov) {
  5105.                 $name = (string) ($gov->getName($lang) ?: $gov->getName($fallback));
  5106.                 if ($name !== '') {
  5107.                     $names[] = $name;
  5108.                 }
  5109.             }
  5110.         }
  5111.         foreach ($notification->getMapGovernorate() ?: [] as $gov) {
  5112.             if ($gov) {
  5113.                 $name = (string) ($gov->getName($lang) ?: $gov->getName($fallback));
  5114.                 if ($name !== '' && !in_array($name$namestrue)) {
  5115.                     $names[] = $name;
  5116.                 }
  5117.             }
  5118.         }
  5119.         return $names;
  5120.     }
  5121.     private function formatAlertHistoryPeriodRange(EwsNotification $notification\DateTimeZone $timezone): string
  5122.     {
  5123.         $startDate $notification->getStartDate();
  5124.         $endDate $notification->getEndDate();
  5125.         $start $startDate
  5126.             ? ($startDate instanceof \Carbon\Carbon $startDate->copy()->setTimezone($timezone) : (clone $startDate)->setTimezone($timezone))
  5127.             : null;
  5128.         $end $endDate
  5129.             ? ($endDate instanceof \Carbon\Carbon $endDate->copy()->setTimezone($timezone) : (clone $endDate)->setTimezone($timezone))
  5130.             : null;
  5131.         $startStr $start $start->format('d/m/Y H:i') : '';
  5132.         $endStr $end $end->format('d/m/Y H:i') : '';
  5133.         if ($startStr === '' && $endStr === '') {
  5134.             return '';
  5135.         }
  5136.         return $startStr '–' $endStr;
  5137.     }
  5138.     private function formatAlertHistoryTimestamp(int $timestamp\DateTimeZone $timezone): string
  5139.     {
  5140.         if ($timestamp <= 0) {
  5141.             return '';
  5142.         }
  5143.         $dt = (new \DateTime('@' $timestamp))->setTimezone($timezone);
  5144.         return $dt->format('d/m/Y') . ' | ' $dt->format('H:i');
  5145.     }
  5146.     private function formatAlertHistoryStartedOn(int $timestamp\DateTimeZone $timezone$translator nullstring $lang 'en'): string
  5147.     {
  5148.         if ($timestamp <= 0) {
  5149.             return '';
  5150.         }
  5151.         $dt = (new \DateTime('@' $timestamp))->setTimezone($timezone);
  5152.         return $this->transAlertHistory(
  5153.             'Started on %date% at %time%',
  5154.             $translator,
  5155.             $lang,
  5156.             [
  5157.                 '%date%' => $dt->format('d/m/Y'),
  5158.                 '%time%' => $dt->format('H:i'),
  5159.             ]
  5160.         );
  5161.     }
  5162.     private function transAlertHistory(string $key$translator nullstring $lang 'en', array $params = []): string
  5163.     {
  5164.         $fallback = [
  5165.             'en' => [
  5166.                 'Alert History' => 'Alert History',
  5167.                 'New Alert' => 'New Alert',
  5168.                 'Raise Alert' => 'Raise Alert',
  5169.                 'Lower Alert' => 'Lower Alert',
  5170.                 'Update Alert Location' => 'Update Alert Location',
  5171.                 'Update Alert Period' => 'Update Alert Period',
  5172.                 'Update Alert Status' => 'Update Alert Status',
  5173.                 'Started on %date% at %time%' => 'Started on %date% at %time%',
  5174.                 'Added governorates: %names%' => 'Added governorates: %names%',
  5175.                 'Removed governorates: %names%' => 'Removed governorates: %names%',
  5176.                 'Time changed from %from% to %to%' => 'Time changed from %from% to %to%',
  5177.                 'Status changed from %from% to %to%' => 'Status changed from %from% to %to%',
  5178.                 'Severity changed from %from% to %to%' => 'Severity changed from %from% to %to%',
  5179.                 'Red' => 'Red',
  5180.                 'Orange' => 'Orange',
  5181.                 'Yellow' => 'Yellow',
  5182.                 'Green' => 'Green',
  5183.             ],
  5184.             'ar' => [
  5185.                 'Alert History' => 'سجل التنبيه',
  5186.                 'New Alert' => 'تنبيه جديد',
  5187.                 'Raise Alert' => 'رفع مستوى التنبيه',
  5188.                 'Lower Alert' => 'خفض مستوى التنبيه',
  5189.                 'Update Alert Location' => 'تحديث موقع التنبيه',
  5190.                 'Update Alert Period' => 'تحديث فترة التنبيه',
  5191.                 'Update Alert Status' => 'تحديث حالة التنبيه',
  5192.                 'Started on %date% at %time%' => 'بدأ في %date% الساعة %time%',
  5193.                 'Added governorates: %names%' => 'المحافظات المضافة: %names%',
  5194.                 'Removed governorates: %names%' => 'المحافظات المحذوفة: %names%',
  5195.                 'Time changed from %from% to %to%' => 'تم تغيير الوقت من %from% إلى %to%',
  5196.                 'Status changed from %from% to %to%' => 'تم تغيير الحالة من %from% إلى %to%',
  5197.                 'Severity changed from %from% to %to%' => 'تم تغيير الشدة من %from% إلى %to%',
  5198.                 'Red' => 'أحمر',
  5199.                 'Orange' => 'برتقالي',
  5200.                 'Yellow' => 'أصفر',
  5201.                 'Green' => 'أخضر',
  5202.             ],
  5203.         ];
  5204.         $applyParams = static function (string $text) use ($params): string {
  5205.             foreach ($params as $search => $replace) {
  5206.                 $text str_replace((string) $search, (string) $replace$text);
  5207.             }
  5208.             return $text;
  5209.         };
  5210.         if ($translator && method_exists($translator'trans')) {
  5211.             $translated $translator->trans($key$paramsnull$lang);
  5212.             if (is_string($translated) && $translated !== '') {
  5213.                 // Symfony returns the message id when missing; prefer our bilingual fallback.
  5214.                 $untranslated $applyParams($key);
  5215.                 if (!($lang !== 'en' && $translated === $untranslated && isset($fallback[$lang][$key]))) {
  5216.                     return $translated;
  5217.                 }
  5218.             }
  5219.         }
  5220.         $text $fallback[$lang][$key] ?? ($fallback['en'][$key] ?? $key);
  5221.         return $applyParams($text);
  5222.     }
  5223.     private function resolveAlertHistoryDotColor(EwsNotification $notification): string
  5224.     {
  5225.         $color strtolower((string) ($notification->getAlertType()?->getColor() ?? 'red'));
  5226.         if (in_array($color, ['red''orange''yellow''green'], true)) {
  5227.             return $color;
  5228.         }
  5229.         return 'red';
  5230.     }
  5231. }