%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/loslex/demo/app/Workers/
Upload File :
Create Path :
Current File : /www/loslex/demo/app/Workers/ContestImporter.php

<?php

namespace App\Workers;

use App\Enums\ContestLevelEnum;
use App\Http\Controllers\CupResultsController;
use App\Models\Contest;
use App\Models\ContestDivision;
use App\Models\Registration;
use App\Models\ShooterStage;
use App\Models\ShooterStageTarget;
use App\Models\ShooterStageTime;
use App\Models\Stage;
use App\Models\User;
use App\Rules\ForbiddenUsernames;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use Ramsey\Uuid\Uuid;

use function Psy\sh;
use stdClass;
use ZipArchive;

class ContestImporter
{
    public array $errors = array();
    public array $warnings = array();

    public function __construct(Contest $contest, string $filename, string $extension)
    {
        switch ($extension){
            case 'xls':
                $this->importExcel($contest, $filename);
                break;

            case 'xlsx':
                $this->importExcel($contest, $filename, true);
                break;

            case 'psc':
                $this->importPractiScore($contest, $filename);
                break;

            default:
                $this->errors[] = __("File with extension :ext is not valid PractiScore or Excel file. Please import only .psc/.xls/.xlsx files.", ['ext' => $extension]);
                return;
        }

        $this->updateMaterializedViews($contest);
        $this->clearContestCaches($contest);
        $this->deleteContestPDFs($contest);
        $this->clearCupCache($contest);
    }

    public static function updateMaterializedViews(Contest $contest)
    {
        // Update materialized views
        DB::statement("DELETE FROM `shooter_stage_results` WHERE `contest_id`=?", [$contest->id]);
        DB::statement("DELETE FROM `contest_results` WHERE `contest_id`=?", [$contest->id]);
        DB::statement("INSERT INTO `shooter_stage_results` (SELECT * FROM `w_shooter_stage_results` WHERE `contest_id`=?)", [$contest->id]);
        DB::statement("INSERT INTO `contest_results` (SELECT * FROM `w_contest_results` WHERE `contest_id`=?)", [$contest->id]);
    }

    public static function clearContestCaches(Contest $contest)
    {
        // Remove results cache
        Cache::forget($contest->getCacheKeyPrefix("results") . "detail");
        Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "detail"]);
        Cache::forget($contest->getCacheKeyPrefix("results") . "overview");
        Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "overview"]);
        Cache::forget($contest->getCacheKeyPrefix("results") . "ceremony");
        Log::info("Cache clear", ['key' => $contest->getCacheKeyPrefix("results") . "ceremony"]);
    }

    public static function clearCupCache(Contest $contest)
    {
        if ($contest->contest_level_id == ContestLevelEnum::CUP->value) {
            $cacheKey = $contest->date->gte(Carbon::parse("2024-09-01")) ? "cup/handgun/2025" : "cup/handgun/2024";

            // Remove cup cache
            Cache::forget($cacheKey);
            Log::info("Cache clear", ['key' => $cacheKey]);
        }
    }

    public static function deleteContestPDFs(Contest $contest)
    {
        // Delete PDFs from storage
        $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))));
        Storage::disk('contests')->delete(
            [
                $contest->id . "/" . $pdfname . "-overview.pdf",
                $contest->id . "/" . $pdfname . "-detail.pdf",
                $contest->id . "/" . $pdfname . "-awards.pdf"
            ]);
    }

    protected function importExcel(Contest $contest, string $filename, bool $isXlsx = false): void
    {
        $numStages = $contest->stages;
        $tmpName = tempnam(sys_get_temp_dir(), 'practiscore.xls');
        file_put_contents($tmpName, Storage::disk('contests')->get($filename));
        $reader = $isXlsx ? new Xlsx() : new Xls();
        $spreadsheet = $reader->load($tmpName);
        $ss = $spreadsheet->setActiveSheetIndex(0);

        DB::beginTransaction();

        $importData = array();
        $rowIterator = $ss->getRowIterator(3);
        $divisionId = 0;
        while ($rowIterator->valid())
        {
            $rowIterator->next();
            $row = $rowIterator->current();
            $position = $row->getRowIndex();

            $cCell = trim($ss->getCell("C" . $position)->getValue());
            if(!$cCell || $cCell == "Jméno") { continue; }
            if(isset($this->textDivisions[$cCell]))
            {
                // Prepare division ID
                $divisionId = ContestDivision::where(DB::raw('UPPER(bgdivision)'), $this->textDivisions[$cCell])->first()->id;
                continue;
            }

            // Get mail and alias
            $bCellExploded = explode("/", trim($ss->getCell("B" . $position)->getValue()));
            $email = isset($bCellExploded[0]) ? trim($bCellExploded[0]) : "";
            $alias = isset($bCellExploded[1]) ? trim($bCellExploded[1]) : "";
            $regGuid = isset($bCellExploded[2]) ? trim($bCellExploded[2]) : "";
            $notCompeting = strtoupper(trim($ss->getCell("A" . $position)->getValue())) == "MZ";
            $dq = strtoupper(trim($ss->getCell("A" . $position)->getValue())) == "DQ";

            $registration = $contest->registrations()->where('canceltoken', $regGuid)->first();
            if(!$registration)
            {
                $alias = strtoupper($alias);
                list($lastName, $firstName) = explode(" ", $ss->getCell("C" . $position)->getValue());
                $user = $this->getOrCreateUserByAliasOrEmail($alias, $email, $firstName, $lastName);
                if(!$user) { continue; }

                $registration = new Registration([
                    'contest_id' => $contest->id,
                    'user_id' => $user->id,
                    'contest_division_id' => $divisionId,
                    'squad' => 1,
                    'notcomp' => $notCompeting,
                    'dq' => $dq
                ]);
                $registration->canceltoken = Str::orderedUuid();
            }
            $registration->contest_division_id = $divisionId;
            $registration->dq = $dq;
            $registration->notcomp = $notCompeting;

            $shooter = new stdClass();
            $shooter->registration = $registration;
            $shooter->stages = array();

            // Get stage data
            $col = 4;
            for ($i = 1; $i <= $numStages; $i++)
            {
                $stage = new stdClass();
                $stage->order = $i;
                $stage->proc = $ss->getCell([$col + 8, $position])->getValue() ?? 0;
                $stage->noshoot = $ss->getCell([$col + 7, $position])->getValue() ?? 0;

                $target = new stdClass();
                $target->order = 1;
                $target->popper = $ss->getCell([$col + 1, $position])->getValue() ?? 0;
                $target->alpha = $ss->getCell([$col + 2, $position])->getValue() ?? 0;
                $target->charlie = $ss->getCell([$col + 3, $position])->getValue() ?? 0;
                $target->delta = $ss->getCell([$col + 4, $position])->getValue() ?? 0;
                $target->miss = $ss->getCell([$col + 5, $position])->getValue() ?? 0;
                $target->misspopper = $ss->getCell([$col + 6, $position])->getValue() ?? 0;

                $time = new stdClass();
                $time->time = $ss->getCell([$col, $position])->getValue();
                $time->order = 1;
                $time->string = null;

                $stage->targets = [$target];
                $stage->times = [$time];

                $shooter->stages[] = $stage;
                $col += 10;
            }
            $importData[] = $shooter;
        }

        $json = json_encode($importData, JSON_PRETTY_PRINT);
        Storage::disk('contests')->put($filename . ".json", $json);

        if(count($this->errors) > 0) { // Do not proceed if there are errors in the imported file
            return;
        }

        // Delete old data before import
        DB::statement("DELETE FROM shooter_stage_times WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]);
        DB::statement("DELETE FROM shooter_stage_targets WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]);
        DB::statement("DELETE FROM shooter_stages WHERE stage_id in (SELECT id FROM stages WHERE contest_id = ?)", [$contest->id]);
        DB::statement("DELETE FROM stages WHERE contest_id = ?", [$contest->id]);

        // Create stages
        $stageIds = array();
        for($i = 1; $i <= $numStages; $i++)
        {
            $st = new Stage([
                'contest_id' => $contest->id,
                'name' => "Stage $i",
                'order' => $i,
                'strings' => 1
            ]);
            $st->save();
            $stageIds[$i] = $st->id;
        }

        foreach ($importData as $shooter)
        {
            $shooter->registration->save();

            foreach ($shooter->stages as $stage)
            {
                $shooterStage = new ShooterStage([
                    'registration_id' => $shooter->registration->id,
                    'stage_id' => $stageIds[$stage->order],
                    'proc' => $stage->proc,
                    'noshoot' => $stage->noshoot
                ]);
                $shooterStage->save();

                foreach ($stage->times as $time)
                {
                    $shooterStageTime = new ShooterStageTime([
                        'shooter_stage_id' => $shooterStage->id,
                        'time' => $time->time,
                        'order' => $time->order,
                        'string' => $time->string
                    ]);
                    $shooterStageTime->save();
                }

                foreach ($stage->targets as $target)
                {
                    $shooterStageTarget = new ShooterStageTarget([
                        'shooter_stage_id' => $shooterStage->id,
                        'order' => $target->order,
                        'popper' => $target->popper,
                        'alpha' => $target->alpha,
                        'charlie' => $target->charlie,
                        'delta' => $target->delta,
                        'miss' => $target->miss,
                        'misspopper' => $target->misspopper
                    ]);
                    $shooterStageTarget->save();
                }
            }
        }

        DB::commit();
    }

    protected function importPractiScore(Contest $contest, string $filename): void
    {
        $tmpName = tempnam(sys_get_temp_dir(), 'practiscore.zip');
        file_put_contents($tmpName, Storage::disk('contests')->get($filename));
        $zip = new ZipArchive;
        $zip->open($tmpName);
        $txt = $zip->getFromName('match_def.json');
        $matchDef = json_decode($txt);
        $txt = $zip->getFromName('match_scores.json');
        $scores = json_decode($txt);
        unset($txt);
        $zip->close();
        unlink($tmpName);

        // Prepare stages
        $stages = array();
        $stageOrderMap = array();
        $numStages = 0;
        foreach ($matchDef->match_stages as $match_stage)
        {
            $stage = new stdClass();
            $stage->guid = $match_stage->stage_uuid;
            $stage->order = $match_stage->stage_number;
            $stage->name = $match_stage->stage_name;
            $stage->deleted = $match_stage->stage_deleted ?? false;
            $stage->strings = $match_stage->stage_strings;
            $stages[$stage->guid] = $stage;
            if (!$stage->deleted)
            {
                $numStages++;
                $stageOrderMap[$stage->order] = $stage->order;
            }
        }
        if($contest->stages != $numStages)
        {
            $this->errors[] = __('Contest has :cStages but imported PractiScore file contains :psStages stages', ['cStages' => $contest->stages, 'psStages' => $numStages]);
            return;
        }

        // reorder stages (to remap indexes of missing stages)
        $stageOrderMap = array_values($stageOrderMap);
        array_unshift($stageOrderMap, 0);

        DB::beginTransaction();
        $shooters = $this->psProcessShooters($matchDef, $contest);
        if($shooters == null)
        {
            DB::rollBack();
            return;
        }

        DB::statement("DELETE FROM shooter_stage_times WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]);
        DB::statement("DELETE FROM shooter_stage_targets WHERE shooter_stage_id in (SELECT id FROM shooter_stages where stage_id in (SELECT id FROM stages WHERE contest_id = ?))", [$contest->id]);
        DB::statement("DELETE FROM shooter_stages WHERE stage_id in (SELECT id FROM stages WHERE contest_id = ?)", [$contest->id]);
        DB::statement("DELETE FROM stages WHERE contest_id = ?", [$contest->id]);

        // Process scores
        foreach ($scores->match_scores as $stage)
        {
            $defStage = $stages[$stage->stage_uuid];
            if ($defStage->deleted) { continue; }
            $st = new Stage([
                'contest_id' => $contest->id,
                'name' => $defStage->name,
                'order' => array_search($defStage->order, $stageOrderMap, true),
                'strings' => $defStage->strings
            ]);
            $st->save();
            $stageId = $st->id;
            foreach ($stage->stage_stagescores as $score) {
                if (!isset($shooters[$score->shtr])) { continue; }

                $shooterStage = new ShooterStage([
                    'stage_id' => $stageId,
                    'registration_id' => $shooters[$score->shtr]->id,
                    'proc' => $score->proc ?? 0,
                    'noshoot' => 0
                ]);
                $shooterStage->save();
                $stageNo = $shooterStage->id;
                $noshoots = $score->popns ?? 0;

                if (!isset($score->dnf) || !$score->dnf) {
                    // Proces times
                    $order = 1;
                    foreach ($score->str as $time) {
                        $strings = null;
                        // parse string splits
                        if (isset($score->meta)) {
                            foreach ($score->meta as $meta) {
                                if (strstr($meta->k, "string" . ($order - 1))) {
                                    $strings = array();
                                    $times = explode(",", $meta->v);
                                    $curr = 0.0;
                                    foreach ($times as $t) {
                                        $strings[] = number_format((double)$t - $curr, 2, '.', '');
                                        $curr = (double)$t;
                                    }
                                }
                            }
                        }

                        $shooterStageTime = new ShooterStageTime([
                            'shooter_stage_id' => $stageNo,
                            'time' => $time,
                            'order' => $order++,
                            'string' => $strings != null ? implode(",", $strings) : null
                        ]);
                        $shooterStageTime->save();
                    }

                    $dataTpl = array(
                        "shooter_stage_id" => $stageNo,
                        "alpha" => 0,
                        "charlie" => 0,
                        "delta" => 0,
                        "miss" => 0,
                        "popper" => 0,
                        "misspopper" => 0
                    );
                    $order = 1;
                    $data = $this->arrayCopy($dataTpl);
                    if ($score->popm > 0 || $score->poph > 0) {
                        $data["order"] = $order++;
                        $data["popper"] = $score->poph;
                        $data["misspopper"] = $score->popm;
                        $target = new ShooterStageTarget($data);
                        $target->save();
                        $data = $this->arrayCopy($dataTpl);
                    }
                    if (isset($score->ts)) {
                        foreach ($score->ts as $t) {
                            $data["order"] = $order++;
                            $data["alpha"] = ($t & 0xF);
                            $data["charlie"] = ($t & 0xF00) >> 8;
                            $data["delta"] = ($t & 0xF000) >> 12;
                            $data["miss"] = ($t & 0xF00000) >> 20;
                            $noshoots += ($t & 0xF0000) >> 16;

                            $target = new ShooterStageTarget($data);
                            $target->save();
                            $data = $this->arrayCopy($dataTpl);
                        }
                    }
                }
                else
                {
                    $shooterStageTime = new ShooterStageTime([
                        'shooter_stage_id' => $stageNo,
                        'time' => 9999,
                        'order' => 1,
                        'string' => null
                    ]);
                    $shooterStageTime->save();

                    $target = new ShooterStageTarget(array(
                        "shooter_stage_id" => $stageNo,
                        "alpha" => 0,
                        "charlie" => 0,
                        "delta" => 0,
                        "miss" => 0,
                        "popper" => 0,
                        "misspopper" => 0,
                        "order" => 1
                    ));
                    $target->save();
                }

                $shooterStage->noshoot = $noshoots;
                $shooterStage->save();
            }
        }

        DB::commit();
    }

    private function getUserByAlias(string $alias)
    {
        return User::where(DB::raw('UPPER(username)'), strtoupper($alias))->first();
    }

    private function getUserByEmail(string $email)
    {
        return User::where(DB::raw('UPPER(email)'), strtoupper($email))->first();
    }

    /**
     * @param mixed $matchDef
     * @param Contest $contest
     * @return void
     */
    public function psProcessShooters(mixed $matchDef, Contest $contest): ?array
    {
        $failed = false;
        $shooters = array();
        foreach ($matchDef->match_shooters as $match_shooter) {
            if (isset($match_shooter->sh_del) && $match_shooter->sh_del) { continue; }
            $shooter = new stdClass();
            $shooter->id = null;
            $shooter->dq = (isset($match_shooter->sh_dq) && $match_shooter->sh_dq) ? $match_shooter->sh_dq : false;
            $shooter->firstName = $match_shooter->sh_fn;
            $shooter->lastName = $match_shooter->sh_ln;
            $shooter->mail = (isset($match_shooter->sh_eml) && $match_shooter->sh_eml) ? $match_shooter->sh_eml : false;
            $shooter->uid = $match_shooter->sh_uid;
            $shooter->division = strtoupper($match_shooter->sh_dvp);
            $shooter->squad = $match_shooter->sh_sqd;
            $shooter->alias = (isset($match_shooter->sh_id) && $match_shooter->sh_id) ? strtoupper($match_shooter->sh_id) : false;
            $shooter->notCompeting = isset($match_shooter->sh_chkins) && in_array("MZ", $match_shooter->sh_chkins);
            $registration = $contest->registrations()->where('canceltoken', $shooter->uid)->first();
            if (!$registration) {
                // try to create registration
                $user = $this->getOrCreateUserByAliasOrEmail($shooter->alias, $shooter->mail, $shooter->firstName, $shooter->lastName);

                if($user == null)
                {
                    $failed = true;
                    continue;
                }

                $registration = new Registration([
                    'contest_id' => $contest->id,
                    'user_id' => $user->id,
                    'contest_division_id' => 1,
                    'squad' => $shooter->squad,
                    'notcomp' => $shooter->notCompeting ? 1 : 0,
                    'dq' => $shooter->dq
                ]);
                $registration->canceltoken = $shooter->uid;
                $registration->save();
            }
            if (strtoupper($registration->division->bgdivision) != $shooter->division || $shooter->dq || $registration->notcomp != $shooter->notCompeting) {
                // update division
                $registration->contest_division_id = ContestDivision::where(DB::raw('UPPER(bgdivision)'), $shooter->division)->first()->id;
                // DQ
                $registration->dq = $shooter->dq;
                // NotCompeting
                $registration->notcomp = $shooter->notCompeting;
                $registration->save();
            }
            $shooter->id = $registration->id;
            if (!$shooter->dq) { $shooters[$shooter->uid] = $shooter; }
        }
        return !$failed ? $shooters : null;
    }

    public function hasMessages()
    {
        return count($this->errors) > 0 || count($this->warnings) > 0;
    }

    private function arrayCopy($arr) {
        $newArray = array();
        foreach($arr as $key => $value) {
            if (is_array($value)) { $newArray[$key] = $this->arrayCopy($value); }
            elseif (is_object($value)) { $newArray[$key] = clone $value; }
            else { $newArray[$key] = $value; }
        }
        return $newArray;
    }

    /**
     * @param stdClass $shooter
     * @return User|null
     */
    public function getOrCreateUserByAliasOrEmail($alias, $email, $firstName, $lastName): ?User
    {
        if (!$alias && !$email)
        {
            $this->errors[] = __('User :firstname :lastname does not have LOS alias or email', ['firstname' => $firstName, 'lastname' => $lastName]);
            return null;
        }

        if (!$alias) { $alias = (string)Uuid::uuid4(); }
        if (!$email) { $email = Uuid::uuid4() . "@loslex.cz"; }

        $user = $this->getUserByAlias($alias) ?? $this->getUserByEmail($email);

        if ($user) {
            $userLastNameWithSuffix = $user->lastname . ($user->suffix ? " " . $user->suffix : "");
            if (!str_starts_with(strtoupper(trim($firstName)), strtoupper($user->firstname)) || !str_starts_with(strtoupper(trim($lastName)), strtoupper($userLastNameWithSuffix))) {
                $this->errors[] = __('LOS Alias conflict :alias, System: :origfirst :origlast, PractiScore: :psFirst :psLast',
                    ['alias' => $alias, 'origfirst' => $user->firstname, 'origlast' => $user->lastname, 'psFirst' => $firstName, 'psLast' => $lastName]);
                return null;
            }
        }
        else {
            // Create new user
            $this->warnings[] = __('User with name :name and alias :alias does not exist, creating new one.', ['name' => $firstName . " " . $lastName, 'alias' => $alias]);
            $userdata = [
                'username' => $alias,
                'firstname' => $firstName,
                'lastname' => $lastName,
                'email' => $email,
                'password' => ""
            ];
            $validator = Validator::make(array_map("strtolower", $userdata), ['username' => ['required', 'string', 'max:255', 'alpha_num', 'ascii', 'unique:'.User::class, new ForbiddenUsernames ]]);
            if ($validator->fails()) {
                $this->warnings = array_merge($this->warnings, $validator->errors()->get('username'));
                $userdata['username'] = (string)Uuid::uuid4();
            }

            $user = new User($userdata);
            $user->personaltoken = Str::orderedUuid();
            $user->save();
        }
        return $user;
    }

    private array $textDivisions = [
        'PISTOLE' => 'PI',
        'KOMPAKTNÍ PISTOLE' => 'KPI',
        'REVOLVER' => 'RE',
        'MALÁ PISTOLE' => 'MPI',
        'MALÝ REVOLVER' => 'MRE',
        'OPTIK' => 'OPT',
        'PUŠKA SAMONABÍJECÍ' => 'PSA',
        'OSOBNÍ OBRANNÁ ZBRAŇ' => 'PDW',
        'PUŠKA OPAKOVACÍ' => 'POP',
        'BROKOVNICE SAMONABÍJECÍ' => 'BSA',
        'BROKOVNICE OPAKOVACÍ' => 'BOP',
        'BROKOVNICE OSTATNÍ' => 'BOS',
    ];
}

Zerion Mini Shell 1.0