%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/loslex/demo/app/Http/Controllers/
Upload File :
Create Path :
Current File : //www/loslex/demo/app/Http/Controllers/ContestController.php

<?php

namespace App\Http\Controllers;

use App\Enums\ContestLevelEnum;
use App\Enums\ContestPublicationStatusEnum;
use App\Http\Requests\ContestUpdateRequest;
use App\Models\Contest;
use App\Models\ContestCategory;
use App\Models\ContestLevel;
use App\Models\OrganizerGroup;
use App\Models\Range;
use App\Models\Registration;
use App\Models\User;
use App\Workers\ContestImporter;
use App\Workers\RegistrationExporter;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;
use Illuminate\View\View;

if (!defined('RESULTS_CACHE_TTL')) {
    define('RESULTS_CACHE_TTL', 7 * 24 * 3600);
}

class ContestController extends Controller
{
    public function __construct() {
        $this->middleware('auth')->except(['index', 'show']);
    }

    /* Display a listing of the resource. */
    public function index(Request $request) : View
    {
        $this->authorize('viewAny', Contest::class);
        return view('contests.list');
    }

    /* Display the specified resource. */
    public function show(Contest $contest) : View
    {
        $this->authorize('view', $contest);

        $contest->load(['registrations.user','registrations.division']);

        return view('contests.show', [
            'contest' => $contest,
            'user' => Auth::user(),
            'forcenotcompete' => $this->forceNotCompete($contest),
            'results' => $this->getResults($contest, $this->needDetailedResults($contest))
        ]);
    }

    /**
     * Determines whether the NotCompete flag should be forced in registration form
     * @param $contest
     * @return bool
     * */
    private function forceNotCompete(Contest $contest) : bool
    {
        $enforcedContestLevels = array(
            ContestLevelEnum::CUP->value,
            ContestLevelEnum::CHAMPIONSHIP->value,
            ContestLevelEnum::SZSCLASSIFICATION->value
        );

        if (!in_array($contest->contest_level_id, $enforcedContestLevels)) { return false; }

        if (Auth::user()?->is_admin) { return false; }

        return $contest->registrations
            ->where('user_id', Auth::user()?->id)
            ->where('notcomp', 0)
            ->count() > 0;
    }

    /** Show the form for creating a new resource. */
    public function create(Request $request) : View
    {
        $this->authorize('create', Contest::class);

        return view('contests.edit', [
            'contest' => null,
            'ranges' => Range::where('is_active', 1)->orderBy('name')->get(),
            'orggroups' => Auth::user()->is_admin ? OrganizerGroup::orderBy('name')->get() : Auth::user()->organizer_groups->sortBy('name'),
            'organizators' => $this->getOrganizerUsers(),
            'contestlevels' => ContestLevel::where('is_active', 1)->get(),
            'contestcategories' => ContestCategory::where('is_active', 1)->get(),
        ]);
    }

    /** Show the form for editing the specified resource. */
    public function edit(Contest $contest): View
    {
        $this->authorize('update', $contest);

        $contestlevels = ContestLevel::where('is_active', 1)->get();
        $remdays = $contest->date->diff(Carbon::now());

        $contestlevels = $contestlevels->filter(function(ContestLevel $lvl) use ($contest, $remdays) {
            return $lvl->advance <= $remdays->days || $contest->contest_level_id == $lvl->id;
        });

        return view('contests.edit', [
            'contest' => $contest,
            'ranges' => Range::orderBy('name')->get(),
            'orggroups' => Auth::user()->is_admin ? OrganizerGroup::orderBy('name')->get() : Auth::user()->organizer_groups->sortBy('name'),
            'organizators' => $this->getOrganizerUsers(),
            'contestlevels' => $contestlevels,
            'contestcategories' => ContestCategory::where('is_active', 1)->get(),
        ]);
    }

    /** Display contest maintenance page */
    public function maintain(Contest $contest): View
    {
        $this->authorize('update', $contest);

        $contest->load(['registrations.user','registrations.division', 'registrations.user.registrations']);
        $contestfiles = LazyCollection::make(function() use ($contest) {
            $allfiles = Storage::disk('contests')->files($contest->id);
            foreach ($allfiles as $file) {
                yield array(
                    'file' => $file,
                    'size' => round(Storage::disk('contests')->size($file) / 1024, 2),
                    'modified' => Carbon::parse(Storage::disk('contests')->lastModified($file))
                );
            }
        });

        return view('contests.maintain', [
            'contest' => $contest,
            'users' => User::get()->sortBy('displayname'),
            'contestfiles' => $contestfiles,
        ]);
    }

    /** Toggle publish status of the contest */
    public function publish(Contest $contest, int $status): RedirectResponse
    {
        $this->authorize('update', $contest);

        $contest->published = $status;
        $contest->save();

        Log::info("User " . Auth::user()->username . " updated contest publish status {$contest->contestname}", ['changed' => $contest->getChanges()]);
        return back();
    }

    /** publish contest results */
    public function toggleResultPublication(Contest $contest)
    {
        $this->authorize('update', $contest);

        $contest->results_published = !$contest->results_published;
        $contest->save();

        $cacheKey = $contest->date->gte(Carbon::parse("2024-09-01")) ? "cup/handgun/2025" : "cup/handgun/2024";

        Cache::forget($cacheKey);
        Log::info("Cache clear", ['key' => $cacheKey]);

        Log::info("User " . Auth::user()->username . " updated contest results status {$contest->contestname}", ['changed' => $contest->getChanges()]);
        return back();
    }

    /** Store a newly created resource in storage. */
    public function store(ContestUpdateRequest $request): RedirectResponse
    {
        $this->authorize('create', Contest::class);

        $contestData = new Contest;
        $contestData->fill($request->validated());
        $contestData->fillSettings($request);
        $contestData->psmatchguid = Str::uuid();
        $contestData->save();
        Cache::forget('icsContests');
        Log::info("Cache clear", ['key' => 'icsContests']);

        Log::info("User " . Auth::user()->username . " created contest {$contestData->contestname}");

        return redirect(route('contest.show', $contestData->id));
    }

    /** Update the specified resource in storage. */
    public function update(ContestUpdateRequest $request, Contest $contest): RedirectResponse
    {
        $this->authorize('update', $contest);

        $oldSquadCount = $contest->squads;  // store old number of squads

        $contest->fill($request->validated());
        $contest->fillSettings($request);
        $contest->version++;

        if ($contest->squads < $oldSquadCount) {
            Registration::where('contest_id', $contest->id)
                        ->where('squad', '>', $contest->squads)
                        ->update(['squad' => 1]);
        }

        $contest->save();
        Cache::forget('icsContests');
        Log::info("Cache clear", ['key' => 'icsContests']);

        Log::info("User " . Auth::user()->username . " updated contest {$contest->contestname}", ['contest'=> $contest->id, 'changed' => $contest->getChanges()]);

        return redirect(route('contest.show', $contest->id));
    }

    /** Remove the specified resource from storage. */
    public function destroy(Contest $contest)
    {
    }

    /** Update squad for selected registrations. This is used by organizers when equlizing squads before contest. */
    public function changesquad(Contest $contest, Request $request) : RedirectResponse
    {
        $this->authorize('update', $contest);

        if ( is_array($request->selection) && count($request->selection) ) {
            Registration::whereIn('id', $request->selection)->update(['squad' => $request->squad]);
        }

        return back();
    }

    /** Exports TXT files with registreeses email addresses. Scope of the export is determined by $type value. Addresses are formated so it could be copied directly to email address lines.
     * @param $contest
     * @param $type Parameter defines the scope of the exported addresses - recognized values are 'all' (all registered), 'contestants' (contestant squads), 'squadr' (referrees/helpers squad), 'builders' (builders), 'paid' (registrees with paid flag).
     */
    public function email(Contest $contest, string $type)
    {
        $this->authorize('update', $contest);

        switch ($type) {
            default:
            case "all":
                $contacts = $contest->registrations->pluck('user.displayname', 'user.email');
                break;

            case "contestants":
                $contacts = $contest->registrations->where('squad', '>', 0)->pluck('user.displayname', 'user.email');
                break;

            case "squadr":
                $contacts = $contest->registrations->where('squad', 0)->pluck('user.displayname', 'user.email');
                break;

            case "builders":
                $contacts = $contest->registrations->where('builder', 1)->pluck('user.displayname', 'user.email');
                break;

            case "notpaid":
                $contacts = $contest->registrations->where('paid', 0)->pluck('user.displayname', 'user.email');
        }

        $data = "\xEF\xBB\xBF"; //BOM
        if ($contacts->isNotEmpty()) {
            foreach ($contacts as $email => $name) {
                $data .= "$name <$email>; ";
            }
        } else {
            $data .= __('No registrations of this type');
        }

        return response()->streamDownload(function() use ($data) { echo $data; }, "emails_{$contest->id}_$type.txt");
    }

    /** Calls ExportRegistration model class to create export and returns finalized file */
    public function export(Contest $contest, string $type)
    {
        $this->authorize('update', $contest);

        $regExport = new RegistrationExporter($contest, $type, request()->has('all'));

        $headers = array("Content-Type" => $this->getContentType($regExport->filename));
        return Storage::disk('contests')->download($regExport->filename, basename($regExport->filename), $headers);
    }

    /** Detemines proper ContentType for the exported file.*/
    private function getContentType(string $filename)
    {
        $pathinfo = pathinfo($filename);
        switch ($pathinfo['extension']) {
            case 'csv':     return 'application/csv';
            case 'json':    return 'application/json';
            case 'pdf':     return 'application/pdf';
            case 'psc':     return 'application/psc';
            case 'xlsx':    return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
            default:        return '';
        }
    }

    /** Downloads contest-related file from Laravel disk storage */
    public function fileDownload(Contest $contest, string $fname)
    {
        $this->authorize('fileManage', $contest);

        $headers = array("Content-Type" => $this->getContentType($fname));
        return Storage::disk('contests')->download($contest->id . "/" . $fname, $fname, $headers);
    }

    /** Deletes specified contest-related file from the storage */
    public function fileDelete(Contest $contest, string $fname) : RedirectResponse
    {
        $this->authorize('fileManage', $contest);

        Storage::disk('contests')->delete($contest->id . "/" . $fname);
        return redirect(route('contest.maintain', $contest->id) . '?export');
    }

    /** Stores and processes imported contest results file */
    public function processImport(Request $request, Contest $contest) : Application|\Illuminate\Foundation\Application|RedirectResponse|View
    {
        $this->authorize('update', $contest);

        $file = $request->file('file');
        $extension = $file->getClientOriginalExtension();
        $uploadName = $contest->id . "/import-" . date("Y-m-d_H-i-s") . "." . $extension;
        Storage::disk('contests')->put($uploadName, $file->getContent());

        $importer = new ContestImporter($contest, $uploadName, $extension);

        if($importer->hasMessages())
        {
            return view('contests.import-messages', [
                'importer' => $importer,
                'contestId' => $contest->id
            ]);
        }

        return redirect(route('contest.show', $contest->id) . "#anresults");
    }

    /** Displays print-ready page with all/present contestants. This can be used to collect signatures form shooters and isnerted in to the range visitors book afterwards. */
    public function presentation(Contest $contest)
    {
        $this->authorize('update', $contest);

        $regs = request()->has('all') ? $contest->registrations : $contest->registrations->where('present', 1);
        $regs = $regs->sortBy(['squad', 'user.lastname']);

        $data = array(
            'contest' => $contest,
            'registrations' => $regs,
            'freeSlots' => $contest->capacity < 50 ? 5 : 10
        );

        return view("contests.presentation", $data);
    }

    /** Displays detailed contest results */
    public function detailedResults(Contest $contest)
    {
        $this->authorize('view', $contest);

        if(request()->has('print')) {
            $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-detail.pdf";
            if (!Storage::disk('contests')->exists($contest->id . "/" . $pdfname))
            {
                $results = $this->getResults($contest, true);
                $width = 500 + $contest->stages * 300;
                $height = 100;

                $divisions = array();
                if ($contest->isLosik()) {
                    $width = 1550;
                    $height = 170 + count($results['divisions']['All']->shooters) * 15;
                    $divisions = ['All' => $results['divisions']['All']];
                } else {
                    foreach ($results['divisions'] as $division) {
                        if ($division->name == "All") { continue; }
                        $height += 60 + count($division->shooters) * 15;
                        $divisions[$division->name] = $division;
                    }
                }
                $results['divisions'] = $divisions;

                $papersize = [0, 0, $width, $height];

                $pdf = PDF::loadView("contests.results.detail-print", $results)->setPaper($papersize);
                Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname));
            }
            return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => $this->getContentType($pdfname)]);
        }
        return view("contests.results.detail", $this->getResults($contest, true));
    }

    /** Displays basic contest results overview */
    public function overviewResults(Contest $contest)
    {
        $this->authorize('view', $contest);

        if(request()->has('print'))
        {
            $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-overview.pdf";
            if(!Storage::disk('contests')->exists($contest->id . "/" . $pdfname)) {
                $template = "contests.results.overview-print";
                $results = $this->getResults($contest, $this->needDetailedResults($contest));
                $width = 220 + $contest->stages * 60;
                $height = 100;

                $divisions = array();
                $overridePaperSize = false;
                if ($contest->isLosik()) {
                    $width = 500;
                    $height = 170 + count($results['divisions']['All']->shooters) * 15;
                    $divisions = ['All' => $results['divisions']['All']];
                } elseif($contest->contest_level_id == ContestLevelEnum::SZSCLASSIFICATION->value) {
                    $divisions = ['All' => $results['divisions']['All']];
                    $template = "contests.results.szclassification-print";
                    $overridePaperSize = "a4";
                } else {
                    foreach ($results['divisions'] as $division) {
                        if ($division->name == "All") { continue; }
                        $height += 60 + count($division->shooters) * 15;
                        $divisions[$division->name] = $division;
                    }
                }
                $results['divisions'] = $divisions;

                $papersize = $overridePaperSize ? $overridePaperSize : [0, 0, $width, $height];

                $pdf = PDF::loadView($template, $results)
                    ->setPaper($papersize);

                Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname));
            }
            return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => "application/pdf"]);
        }
    }

    /** Displays a simplified table with each division winners */
    public function ceremonies(Contest $contest)
    {
        $this->authorize('view', $contest);

        if(request()->has('print'))
        {
            $pdfname = $contest->date->isoFormat("YYYY-MM-DD") . "_" . str_replace("/", "_", str_replace(".", "-", Str::ucfirst(Str::camel(Str::ascii($contest->contestname))))) . "-awards.pdf";
            if(!Storage::disk('contests')->exists($contest->id . "/" . $pdfname))
            {
                $results = $this->getResultsCeremony($contest);
                $pdf = PDF::loadView("contests.results.ceremonies", $results)
                    ->setPaper('a4');
                Storage::disk('contests')->put($contest->id . "/" . $pdfname, $pdf->stream($pdfname));
            }
            return Storage::disk('contests')->download($contest->id . "/" . $pdfname, $pdfname, ["Content-Type" => "application/pdf"]);
        }
        return view("contests.results.ceremonies", $this->getResultsCeremony($contest));
    }

    private function getResults(Contest $contest, bool $detailed)
    {
        $cacheKey = $contest->getCacheKeyPrefix('results') . ($detailed ? "detail" : "overview");
        return Cache::remember($cacheKey, RESULTS_CACHE_TTL, function() use ($detailed, $contest, $cacheKey) {
            $results = DB::select("SELECT * FROM `contest_results` WHERE `contest_id`=?", [$contest->id]);
            if (!$results) { return null; }

            Log::info("Cache MISS", ['key' => $cacheKey]);
            // Prepare empty arrays with detailed results
            $stages = array();

            $divisions = array();
            foreach ($results as $result) {
                if (!isset($divisions[$result->bgdivision])) {
                    $divisions[$result->bgdivision] = new \stdClass();
                    $divisions[$result->bgdivision]->name = $result->bgdivision;
                    $divisions[$result->bgdivision]->shooters = array();
                }
                $divisions[$result->bgdivision]->shooters[$result->registration_id] = new \stdClass();
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->percent = number_format($result->percent, 2);
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->name = $result->lastname . " " . $result->firstname . ($result->namesuffix ? " " . $result->namesuffix : "");
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->anonname = implode(" ", array_filter([Str::mask($result->lastname, '*', 1), Str::mask($result->firstname, '*', 1), Str::mask($result->namesuffix, '*', 1)]));
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->registrationId = $result->registration_id;
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->origDivision = $result->origdivision;
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->notcomp = $result->notcomp;
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->dq = $result->dq;
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->stages = array();
            }

            foreach (DB::select("SELECT * FROM `stage_results` WHERE `contest_id`=?", [$contest->id]) as $result) {
                $divisions[$result->bgdivision]->shooters[$result->registration_id]->stages[$result->order] = $result;
            }

            if ($detailed) {
                foreach (DB::select("SELECT `id`, `strings`, `order` FROM `stages` WHERE `contest_id`=?", [$contest->id]) as $stg) {
                    $stages[$stg->order] = new \stdClass();
                    $stages[$stg->order]->strings = $stg->strings;
                    $stages[$stg->order]->id = $stg->id;
                    $stages[$stg->order]->span = 11 + ($stg->strings > 1 ? $stg->strings : 0);
                }

                foreach (DB::select("SELECT `order`, `time`, `bgdivision`, `registration_id`, `storder` FROM `stage_times` WHERE `contest_id`=?", [$contest->id]) as $str) {
                    if (!isset($divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder])) {
                        $divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder] = array();
                    }
                    $divisions[$str->bgdivision]->shooters[$str->registration_id]->stageStrings[$str->storder][$str->order] = $str->time;
                }
            }

            $divisions = $this->sortResults($divisions);

            return array(
                "divisions" => $divisions,
                "numStages" => $contest->stages,
                "name" => $contest->contestname,
                "date" => $contest->date,
                "stages" => $stages
            );
        });
    }

    private function getOrganizerUsers()
    {
        if (Auth::user()->is_admin) {
            $organizers = User::get()->sortBy('displayname');
        } else {
            $organizers = Collection::make();
            foreach (Auth::user()->organizer_groups as $group) {
                $organizers = $organizers->merge($group->users);
            }
            $organizers = $organizers->unique('id')->sortBy('displayname');
        }

        return $organizers;
    }

    private function getResultsCeremony(Contest $contest)
    {
        $cacheKey = $contest->getCacheKeyPrefix('results') . 'ceremony';
        return Cache::remember($cacheKey, RESULTS_CACHE_TTL, function() use ($contest, $cacheKey)
        {
            Log::info("Cache MISS", ['key' => $cacheKey]);
            $results = DB::select("SELECT firstname, lastname, namesuffix, percent, bgdivision FROM `contest_results` WHERE `contest_id`=:contestId AND notcomp=0 and bgdivision collate utf8mb4_general_ci <> 'All'", ['contestId' => $contest->id]);
            if (!$results) { return null; }

            $divisions = array();
            foreach ($results as $result) {
                if (!isset($divisions[$result->bgdivision])) {
                    $divisions[$result->bgdivision] = new \stdClass();
                    $divisions[$result->bgdivision]->name = $result->bgdivision;
                    $divisions[$result->bgdivision]->shooters = array();
                }
                $shooter = new \stdClass();
                $shooter->percent = number_format($result->percent, 2);
                $shooter->name = $result->lastname . " " . $result->firstname . ($result->namesuffix ? " " . $result->namesuffix : "");
                $divisions[$result->bgdivision]->shooters[] = $shooter;
            }

            $divisions = $this->sortResults($divisions);

            return array(
                "divisions" => $divisions,
                "name" => $contest->contestname,
                "date" => $contest->date,
            );
        });
    }

    private function sortResults(array $divisions): array
    {
        foreach ($divisions as $div) {
            uasort($div->shooters, function ($a, $b) {
                if ($a->percent < $b->percent) { return 1; }
                if ($a->percent == $b->percent && $a->name < $b->name) { return 1; }
                return -1;
            });
        }

        uasort($divisions, function ($a, $b) {
            $divPrio = array(
                "ALL" => 1,
                "PI" => 2,
                "KPI" => 3,
                "MPI" => 4,
                "RE" => 5,
                "MRE" => 6,
                "OPT" => 7,
                "PDW" => 8,
                "PSA" => 9,
                "POP" => 10,
                "BSA" => 11,
                "BOP" => 12,
                "BOS" => 13
            );
            return $divPrio[strtoupper($a->name)] > $divPrio[strtoupper($b->name)] ? 1 : -1;
        });
        return $divisions;
    }

    private function needDetailedResults(Contest $contest)
    {
        return $contest->contest_level_id == ContestLevelEnum::SZSCLASSIFICATION->value;
    }
}

Zerion Mini Shell 1.0