Initial commit

This commit is contained in:
Zdeněk Burda
2026-01-09 21:26:40 +01:00
parent e83aec6dca
commit 41e3ce6f25
404 changed files with 61250 additions and 28 deletions

32
app/Models/Band.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Band extends Model
{
protected $table = 'bands';
use HasFactory;
protected $fillable = [
'name',
'order',
'edi_band_begin',
'edi_band_end',
'has_power_category'
];
public function ediBands(): BelongsToMany
{
return $this->belongsToMany(EdiBand::class, 'bands_edi_bands', 'band_id', 'edi_band_id');
}
public function contests(): BelongsToMany
{
return $this->belongsToMany(Contest::class, 'contests_bands', 'band_id', 'contest_id');
}
}

29
app/Models/Category.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Category extends Model
{
protected $table = 'categories';
use HasFactory;
protected $fillable = [
'name',
'order'
];
public function ediCategories(): BelongsToMany
{
return $this->belongsToMany(EdiCategory::class, 'categories_edi_categories', 'category_id', 'edi_category_id');
}
public function contests(): BelongsToMany
{
return $this->belongsToMany(Contest::class, 'contests_categories', 'category_id', 'category_id');
}
}

86
app/Models/Contest.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Models\EvaluationRuleSet;
class Contest extends Model
{
protected $table = 'contests';
use HasFactory;
use HasTranslations;
public array $translatable = [
'name',
'description'
];
protected $fillable = [
'name',
'description',
'url',
'evaluator',
'email',
'email2',
'is_mcr',
'is_test',
'is_sixhr',
'is_active',
'start_time',
'duration',
'logs_deadline_days',
'rule_set_id',
];
protected $casts = [
'name' => 'array',
'description' => 'array',
'is_mcr' => 'boolean',
'is_test' => 'boolean',
'is_sixhr' => 'boolean',
'is_active' => 'boolean',
'duration' => 'integer',
'logs_deadline_days' => 'integer',
'rule_set_id' => 'integer',
// 'start_time' => 'string', // pokud chceš čistý string; pro fancy práci s časem můžeš dát vlastní cast
];
public function ruleSet(): BelongsTo
{
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
}
public function rounds(): HasMany
{
return $this->hasMany(Round::class, 'contest_id')
->orderByDesc('start_time')
->orderByDesc('end_time');
}
public function parameters(): HasMany
{
return $this->hasMany(ContestParameter::class, 'contest_id');
}
public function bands(): BelongsToMany
{
return $this->belongsToMany(Band::class, 'contests_bands', 'contest_id', 'band_id');
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'contests_categories', 'contest_id', 'category_id');
}
public function powerCategories(): BelongsToMany
{
return $this->belongsToMany(PowerCategory::class, 'contests_power_categories', 'contest_id', 'power_category_id');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContestParameter extends Model
{
use HasFactory;
protected $table = 'contests_parameters';
protected $fillable = [
'contest_id',
'log_type',
'ignore_slash_part',
'ignore_third_part',
'letters_in_rst',
'discard_qso_rec_diff_call',
'discard_qso_sent_diff_call',
'discard_qso_rec_diff_rst',
'discard_qso_sent_diff_rst',
'discard_qso_rec_diff_code',
'discard_qso_sent_diff_code',
'unique_qso',
'time_tolerance',
];
protected $casts = [
'contest_id' => 'integer',
'ignore_slash_part' => 'boolean',
'ignore_third_part' => 'boolean',
'letters_in_rst' => 'boolean',
'discard_qso_rec_diff_call' => 'boolean',
'discard_qso_sent_diff_call' => 'boolean',
'discard_qso_rec_diff_rst' => 'boolean',
'discard_qso_sent_diff_rst' => 'boolean',
'discard_qso_rec_diff_code' => 'boolean',
'discard_qso_sent_diff_code' => 'boolean',
'unique_qso' => 'boolean',
'time_tolerance' => 'integer',
];
public function contest(): BelongsTo
{
return $this->belongsTo(Contest::class, 'contest_id');
}
}

31
app/Models/CountryWwl.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CountryWwl extends Model
{
use HasFactory;
protected $table = 'countries_wwl';
// Nemáme primární klíč typu int → vypnout auto increment
public $incrementing = false;
// Primární klíč není integer → type = string
protected $keyType = 'string';
// Eloquent NEPODPORUJE composite PK → necháme primaryKey prázdné
protected $primaryKey = null;
// Zakázat timestamps auto-handling? → ne, používáš timestamps v DB
// Pokud bys je chtěl řídit sám:
// public $timestamps = false;
protected $fillable = [
'country_name',
'wwl'
];
}

38
app/Models/Cty.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Cty extends Model
{
protected $table = 'cty';
use HasFactory;
protected $fillable = [
'country_name',
'dxcc',
'cq_zone',
'itu_zone',
'continent',
'latitude',
'longitude',
'time_offset',
'prefix',
'prefix_norm',
'precise',
'source'
];
protected $casts = [
'dxcc' => 'integer',
'cq_zone' => 'integer',
'itu_zone' => 'integer',
'latitude' => 'float',
'longitude' => 'float',
'time_offset' => 'float',
'precise' => 'boolean',
];
}

23
app/Models/EdiBand.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class EdiBand extends Model
{
protected $table = 'edi_bands';
use HasFactory;
protected $fillable = [
'value'
];
public function bands(): BelongsToMany
{
return $this->belongsToMany(Band::class, 'bands_edi_bands', 'edi_band_id', 'band_id');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class EdiCategory extends Model
{
protected $table = 'edi_categories';
use HasFactory;
protected $fillable = [
'value',
'regex_pattern',
];
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'categories_edi_categories', 'edi_category_id', 'category_id');
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\QueryException;
use Illuminate\Support\Carbon;
/**
* Model: EvaluationLock
*
* Účel:
* - Reprezentuje aplikační zámek (lock) pro vyhodnocovací procesy.
* - Slouží k zabránění souběžného spuštění více vyhodnocovacích běhů
* nad stejným rozsahem dat (scope).
*
* Kontext v architektuře:
* - Používá se při spuštění vyhodnocení (StartEvaluationRunJob)
* a během celé evaluation pipeline.
* - Je součástí mechanismu ochrany proti race condition a kolizím
* mezi paralelně běžícími background joby.
*
* Princip fungování:
* - Každý lock je identifikován unikátním klíčem (`key`), který
* reprezentuje zamčený rozsah dat, typicky např.:
* - `round:{round_id}`
* - `round:{round_id}:band:{band_id}`
* - Při pokusu o vytvoření locku se spoléhá na unikátní index v DB.
* Pokud záznam s daným klíčem již existuje, znamená to, že jiný
* vyhodnocovací běh nad stejným scope již probíhá.
*
* Pole modelu:
* - key:
* Jednoznačný identifikátor zamčeného scope (unikátní v DB).
* - evaluation_run_id:
* Reference na EvaluationRun, který lock vlastní.
* - locked_at:
* Čas, kdy byl lock získán.
* - expires_at:
* Volitelný čas expirace locku (slouží jako bezpečnostní pojistka
* proti "visícím" lockům při havárii procesu).
*
* Co model NEDĚLÁ:
* - neřeší automatické obnovování nebo expirování locků
* - neobsahuje byznys logiku vyhodnocení
* - nesupluje databázové transakce
*
* Zásady návrhu:
* - Lock musí být získáván a uvolňován atomicky.
* - Locky musí být vždy uvolněny ve finálním kroku vyhodnocení
* (FinalizeRunJob), případně při chybě.
* - Existence locku je autoritativní zdroj informace o tom,
* zda je daný scope právě zpracováván.
*/
class EvaluationLock extends Model
{
protected $table = 'evaluation_locks';
protected $fillable = [
'key',
'evaluation_run_id',
'locked_at',
'expires_at',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'locked_at' => 'datetime',
'expires_at' => 'datetime',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
/**
* Pokusí se získat lock pro daný klíč.
*
* - Pokud existuje expirovaný lock, před pokusem ho smaže.
* - Pokud aktivní lock existuje, vrátí null.
*
* @param string $key Jedinečný identifikátor scope (např. "round:5").
* @param \App\Models\EvaluationRun|null $run Volitelný běh, ke kterému se lock váže.
* @param \DateInterval|int|\Carbon\Carbon|null $ttl Doba platnosti (sekundy nebo DateInterval nebo konkrétní expirace).
*/
public static function acquire(string $key, ?EvaluationRun $run = null, \DateInterval|int|Carbon|null $ttl = null): ?self
{
$now = Carbon::now();
// uklidíme expirovaný lock se stejným klíčem, aby neblokoval unikátní index
self::where('key', $key)
->whereNotNull('expires_at')
->where('expires_at', '<', $now)
->delete();
$expiresAt = null;
if ($ttl instanceof Carbon) {
$expiresAt = $ttl;
} elseif ($ttl instanceof \DateInterval) {
$expiresAt = (clone $now)->add($ttl);
} elseif (is_int($ttl)) {
$expiresAt = (clone $now)->addSeconds($ttl);
}
try {
return self::create([
'key' => $key,
'evaluation_run_id' => $run?->id,
'locked_at' => $now,
'expires_at' => $expiresAt,
]);
} catch (QueryException $e) {
// unikátní klíč porušen -> lock už drží někdo jiný
if (str_contains(strtolower($e->getMessage()), 'duplicate') || str_contains(strtolower($e->getMessage()), 'unique')) {
return null;
}
throw $e;
}
}
/**
* Uvolní lock podle klíče (a volitelně evaluation_run_id).
*/
public static function release(string $key, ?EvaluationRun $run = null): int
{
$query = self::where('key', $key);
if ($run) {
$query->where('evaluation_run_id', $run->id);
}
return $query->delete();
}
/**
* Zjistí, zda lock existuje a neexpiroval.
*/
public static function isLocked(string $key): bool
{
$now = Carbon::now();
return self::where('key', $key)
->where(function ($q) use ($now) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', $now);
})
->exists();
}
}

View File

@@ -0,0 +1,479 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EvaluationRuleSet extends Model
{
use HasFactory;
protected $table = 'evaluation_rule_sets';
/**
* Atributy rulesetu používané při vyhodnocování.
*
* Skupiny:
* - scoring_mode/points_per_*: způsob bodování.
* - multipliers*: multiplikátory a jejich scope.
* - *_policy / penalty_*: penalizace a způsob započtení chyb.
* - exchange_* / discard_*: pravidla výměny a busted detekce.
* - match_* / callsign_*: pravidla párování a normalizace.
* - out_of_window_*: chování mimo časové okno.
*/
protected $fillable = [
'name',
'code',
'description',
'scoring_mode', // DISTANCE / FIXED_POINTS
'points_per_qso', // Fixní body za valid QSO
'points_per_km', // Body za km v režimu DISTANCE
'use_multipliers', // Zapnout multiplikátory ve výsledku
'multiplier_type', // NONE / WWL / DXCC / SECTION / COUNTRY
'dup_qso_policy', // ZERO_POINTS / PENALTY
'nil_qso_policy', // ZERO_POINTS / PENALTY
'no_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
'not_in_counterpart_log_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
'unique_qso_policy', // INVALID / ZERO_POINTS / FLAG_ONLY
'busted_call_policy', // ZERO_POINTS / PENALTY
'busted_exchange_policy',// ZERO_POINTS / PENALTY
'busted_serial_policy', // ZERO_POINTS / PENALTY
'busted_locator_policy', // ZERO_POINTS / PENALTY
'penalty_dup_points', // Penalizace za DUP
'penalty_nil_points', // Penalizace za NIL
'penalty_busted_call_points', // Penalizace za BUSTED_CALL
'penalty_busted_exchange_points', // Penalizace za BUSTED_EXCHANGE
'penalty_busted_serial_points', // Penalizace za BUSTED_SERIAL
'penalty_busted_locator_points', // Penalizace za BUSTED_LOCATOR
'dupe_scope', // BAND / BAND_MODE
'callsign_normalization',// STRICT / IGNORE_SUFFIX
'distance_rounding', // FLOOR / ROUND / CEIL
'min_distance_km', // Minimum km pro bodované QSO
'require_locators', // Bez lokátorů = nevalidní / bez bodů
'out_of_window_policy', // IGNORE / ZERO_POINTS / PENALTY / INVALID
'penalty_out_of_window_points', // Penalizace za OUT_OF_WINDOW
'exchange_type', // SERIAL / WWL / SERIAL_WWL / CUSTOM
'exchange_requires_wwl', // WWL je povinná část výměny
'exchange_requires_serial', // Serial je povinná část výměny
'exchange_requires_report', // RST je povinná část výměny
'exchange_pattern', // Regex pro CUSTOM výměnu
'ignore_slash_part', // Ignorovat suffix za lomítkem v callsign
'ignore_third_part', // Ignorovat 3. část callsign
'letters_in_rst', // Povolit písmena v RST
'rst_ignore_third_char', // Ignorovat 3. znak v RST
'discard_qso_rec_diff_call', // RX neshoda callsign -> BUSTED_CALL (error_side=RX)
'discard_qso_sent_diff_call', // TX neshoda callsign -> BUSTED_CALL (error_side=TX)
'discard_qso_rec_diff_rst', // RX neshoda RST -> BUSTED_RST (error_side=RX)
'discard_qso_sent_diff_rst', // TX neshoda RST -> BUSTED_RST (error_side=TX)
'discard_qso_rec_diff_code', // RX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=RX)
'discard_qso_sent_diff_code',// TX neshoda exchange -> BUSTED_SERIAL/LOCATOR (error_side=TX)
'discard_qso_rec_diff_serial', // RX neshoda serial -> BUSTED_SERIAL (error_side=RX)
'discard_qso_sent_diff_serial',// TX neshoda serial -> BUSTED_SERIAL (error_side=TX)
'discard_qso_rec_diff_wwl', // RX neshoda WWL -> BUSTED_LOCATOR (error_side=RX)
'discard_qso_sent_diff_wwl', // TX neshoda WWL -> BUSTED_LOCATOR (error_side=TX)
'busted_rst_policy', // ZERO_POINTS / PENALTY
'penalty_busted_rst_points', // Penalizace za BUSTED_RST
'match_tiebreak_order', // Pořadí tiebreak kritérií
'match_require_locator_match', // Matching vyžaduje lokátor
'match_require_exchange_match',// Matching vyžaduje exchange
'multiplier_scope', // PER_BAND / OVERALL
'multiplier_source', // VALID_ONLY / ALL_MATCHED
'wwl_multiplier_level', // LOCATOR_2 / LOCATOR_4 / LOCATOR_6
'checklog_matching', // CHECK logy v matchingu
'out_of_window_dq_threshold', // DQ logu při nadlimitních OOW QSO
'time_diff_dq_threshold_percent', // DQ logu při nadlimitním % časového rozdílu
'time_diff_dq_threshold_sec', // Prah časového rozdílu v sekundách
'bad_qso_dq_threshold_percent', // DQ logu při nadlimitním % špatných QSO
'time_tolerance_sec', // Tolerance času v matchingu (sekundy)
'require_unique_qso', // Zapnout detekci duplicit v logu
'allow_time_shift_one_hour', // Povolit posun o 1 hodinu při matchingu
'time_shift_seconds', // Velikost časového posunu v sekundách
'time_mismatch_policy', // INVALID / ZERO_POINTS / FLAG_ONLY / PENALTY
'callsign_suffix_max_len', // Max délka suffixu za /
'callsign_levenshtein_max', // Maximální Levenshtein vzdálenost pro fuzzy match
'allow_time_mismatch_pairing', // Povolit párování mimo toleranci
'time_mismatch_max_sec', // Max. odchylka mimo toleranci (NULL = bez limitu)
'dup_resolution_strategy', // Pořadí pravidel pro volbu survivor v duplicitách
'operating_window_mode', // NONE / BEST_CONTIGUOUS
'operating_window_hours', // Délka okna v hodinách
'sixhr_ranking_mode', // IARU / CRK
'options', // Volitelné JSON rozšíření pravidel
];
protected $casts = [
'points_per_qso' => 'integer',
'points_per_km' => 'float',
'use_multipliers' => 'boolean',
'time_tolerance_sec' => 'integer',
'require_unique_qso' => 'boolean',
'penalty_dup_points' => 'integer',
'penalty_nil_points' => 'integer',
'penalty_busted_call_points' => 'integer',
'penalty_busted_exchange_points' => 'integer',
'penalty_busted_serial_points' => 'integer',
'penalty_busted_locator_points' => 'integer',
'min_distance_km' => 'integer',
'require_locators' => 'boolean',
'penalty_out_of_window_points' => 'integer',
'exchange_requires_wwl' => 'boolean',
'exchange_requires_serial' => 'boolean',
'exchange_requires_report' => 'boolean',
'ignore_slash_part' => 'boolean',
'ignore_third_part' => 'boolean',
'letters_in_rst' => 'boolean',
'rst_ignore_third_char' => 'boolean',
'discard_qso_rec_diff_call' => 'boolean',
'discard_qso_sent_diff_call' => 'boolean',
'discard_qso_rec_diff_rst' => 'boolean',
'discard_qso_sent_diff_rst' => 'boolean',
'discard_qso_rec_diff_code' => 'boolean',
'discard_qso_sent_diff_code' => 'boolean',
'discard_qso_rec_diff_serial' => 'boolean',
'discard_qso_sent_diff_serial' => 'boolean',
'discard_qso_rec_diff_wwl' => 'boolean',
'discard_qso_sent_diff_wwl' => 'boolean',
'penalty_busted_rst_points' => 'integer',
'match_tiebreak_order' => 'array',
'match_require_locator_match' => 'boolean',
'match_require_exchange_match' => 'boolean',
'checklog_matching' => 'boolean',
'out_of_window_dq_threshold' => 'integer',
'time_diff_dq_threshold_percent' => 'integer',
'time_diff_dq_threshold_sec' => 'integer',
'bad_qso_dq_threshold_percent' => 'integer',
'allow_time_shift_one_hour' => 'boolean',
'time_shift_seconds' => 'integer',
'callsign_suffix_max_len' => 'integer',
'callsign_levenshtein_max' => 'integer',
'allow_time_mismatch_pairing' => 'boolean',
'time_mismatch_max_sec' => 'integer',
'dup_resolution_strategy' => 'array',
'operating_window_mode' => 'string',
'operating_window_hours' => 'integer',
'sixhr_ranking_mode' => 'string',
'options' => 'array',
];
/**
* Mapování: kde je jednotlivý atribut rulesetu použit.
* Pokud je hodnota "unused", musí být explicitně zdůvodněno.
*/
public const FLAG_USAGE = [
'name' => 'metadata (UI, identifikace rulesetu)',
'code' => 'metadata (UI, identifikace rulesetu)',
'description' => 'metadata (UI, identifikace rulesetu)',
'scoring_mode' => 'ScoringService::computeBasePoints',
'points_per_qso' => 'ScoringService::computeBasePoints',
'points_per_km' => 'ScoringService::computeBasePoints',
'use_multipliers' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
'multiplier_type' => 'ScoreGroupJob::applyMultipliers + AggregateLogResultsJob',
'dup_qso_policy' => 'ScoringService::penaltyPointsFor',
'nil_qso_policy' => 'ScoringService::penaltyPointsFor',
'no_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
'not_in_counterpart_log_policy' => 'ScoreGroupJob (validita/bodování)',
'unique_qso_policy' => 'ScoreGroupJob (validita/bodování)',
'busted_call_policy' => 'ScoringService::penaltyPointsFor',
'busted_exchange_policy' => 'ScoringService::penaltyPointsFor',
'busted_serial_policy' => 'ScoringService::penaltyPointsFor',
'busted_locator_policy' => 'ScoringService::penaltyPointsFor',
'penalty_dup_points' => 'ScoringService::penaltyPointsFor',
'penalty_nil_points' => 'ScoringService::penaltyPointsFor',
'penalty_busted_call_points' => 'ScoringService::penaltyPointsFor',
'penalty_busted_exchange_points' => 'ScoringService::penaltyPointsFor',
'penalty_busted_serial_points' => 'ScoringService::penaltyPointsFor',
'penalty_busted_locator_points' => 'ScoringService::penaltyPointsFor',
'dupe_scope' => 'BuildWorkingSetLogJob::dupe_key',
'callsign_normalization' => 'MatchingService::normalizeCallsign + MatchQsoGroupJob',
'distance_rounding' => 'ScoringService::calculateDistanceKm',
'min_distance_km' => 'ScoringService::calculateDistanceKm',
'require_locators' => 'ScoreGroupJob (validita/bodování)',
'out_of_window_policy' => 'ScoringService::outOfWindowDecision + ScoreGroupJob',
'penalty_out_of_window_points' => 'ScoringService::penaltyPointsFor',
'exchange_type' => 'MatchQsoGroupJob::resolveExchangeMatch',
'exchange_requires_wwl' => 'MatchQsoGroupJob::resolveExchangeMatch',
'exchange_requires_serial' => 'MatchQsoGroupJob::resolveExchangeMatch',
'exchange_requires_report' => 'MatchQsoGroupJob::resolveExchangeMatch',
'exchange_pattern' => 'MatchQsoGroupJob::resolveExchangeMatch',
'ignore_slash_part' => 'MatchQsoGroupJob::normalizeCallsign',
'ignore_third_part' => 'MatchQsoGroupJob::normalizeCallsign',
'letters_in_rst' => 'MatchQsoGroupJob::normalizeRst',
'rst_ignore_third_char' => 'MatchQsoGroupJob::reportsMatch',
'discard_qso_rec_diff_call' => 'MatchQsoGroupJob (error_side=RX)',
'discard_qso_sent_diff_call' => 'MatchQsoGroupJob (error_side=TX)',
'discard_qso_rec_diff_rst' => 'MatchQsoGroupJob (error_side=RX)',
'discard_qso_sent_diff_rst' => 'MatchQsoGroupJob (error_side=TX)',
'discard_qso_rec_diff_code' => 'MatchQsoGroupJob (RX exchange mismatch)',
'discard_qso_sent_diff_code' => 'MatchQsoGroupJob (TX exchange mismatch)',
'discard_qso_rec_diff_serial' => 'MatchQsoGroupJob (RX serial mismatch)',
'discard_qso_sent_diff_serial' => 'MatchQsoGroupJob (TX serial mismatch)',
'discard_qso_rec_diff_wwl' => 'MatchQsoGroupJob (RX locator mismatch)',
'discard_qso_sent_diff_wwl' => 'MatchQsoGroupJob (TX locator mismatch)',
'busted_rst_policy' => 'ScoringService::penaltyPointsFor',
'penalty_busted_rst_points' => 'ScoringService::penaltyPointsFor',
'match_tiebreak_order' => 'MatchQsoGroupJob::rankDecision',
'match_require_locator_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
'match_require_exchange_match' => 'MatchQsoGroupJob::resolveExchangeMatch',
'multiplier_scope' => 'AggregateLogResultsJob::aggregateMultipliers',
'multiplier_source' => 'AggregateLogResultsJob::aggregateMultipliers',
'wwl_multiplier_level' => 'ScoreGroupJob::applyMultipliers',
'checklog_matching' => 'MatchQsoGroupJob::groupLogsByKey',
'out_of_window_dq_threshold' => 'AggregateLogResultsJob (DQ logu)',
'time_diff_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
'time_diff_dq_threshold_sec' => 'AggregateLogResultsJob (DQ logu)',
'bad_qso_dq_threshold_percent' => 'AggregateLogResultsJob (DQ logu)',
'time_tolerance_sec' => 'MatchQsoGroupJob::findCandidates',
'require_unique_qso' => 'UnpairedClassificationJob::UNIQUE',
'allow_time_shift_one_hour' => 'MatchQsoGroupJob::matchWithTimeShift',
'time_shift_seconds' => 'MatchQsoGroupJob::matchWithTimeShift',
'time_mismatch_policy' => 'ScoreGroupJob (validita/bodování)',
'callsign_suffix_max_len' => 'MatchingService::normalizeCallsign',
'callsign_levenshtein_max' => 'MatchQsoGroupJob::rankDecision',
'allow_time_mismatch_pairing' => 'MatchQsoGroupJob::findCandidates',
'time_mismatch_max_sec' => 'MatchQsoGroupJob::findCandidates',
'dup_resolution_strategy' => 'DuplicateResolutionJob::sort',
'operating_window_mode' => 'AggregateLogResultsJob (operating window, 6H agregace)',
'operating_window_hours' => 'AggregateLogResultsJob (operating window, 6H agregace)',
'sixhr_ranking_mode' => 'RecalculateOfficialRanksJob (6H ranking mode)',
'options' => 'fallback pro hodnoty bez sloupců (EvaluationRuleSet::getOption)',
];
/**
* Options fallback (používá se jen pokud není vyplněn odpovídající sloupec).
*
* Klíče, typy, defaulty a fáze použití:
* - ignore_slash_part (bool, default=true) matching (normalizace callsign)
* - callsign_suffix_max_len (int, default=4) matching (normalizace callsign)
* - rst_ignore_third_char (bool, default=true) matching (porovnání RST)
* - letters_in_rst (bool, default=false) matching (porovnání RST)
* - callsign_levenshtein_max (int, default=0) matching (fuzzy callsign)
* - allow_time_shift_one_hour (bool, default=true) matching (časový posun)
* - time_shift_seconds (int, default=3600) matching (časový posun)
* - allow_time_mismatch_pairing (bool, default=false) matching (TIME_MISMATCH pairing)
* - time_mismatch_max_sec (int|null, default=null) matching (max odchylka mimo toleranci)
* - match_require_locator_match (bool, default=false) matching (mismatch flagging)
* - match_require_exchange_match (bool, default=false) matching (mismatch flagging)
* - unique_qso_enabled (bool, default=true) unpaired klasifikace (UNIQUE)
* - unique_qso_policy (string, default=FLAG_ONLY) scoring (validita/bodování)
* - no_counterpart_log_policy (string, default=FLAG_ONLY) scoring
* - not_in_counterpart_log_policy (string, default=ZERO_POINTS) scoring
* - duplicate_resolution_strategy (array, default=[paired_first, ok_first, earlier_time, lower_id]) duplicity
* - distance_rounding (string, default=CEIL) scoring (vzdálenost)
* - min_distance_km (int, default=1) scoring (vzdálenost)
*/
// ----- Vztahy -----
public function evaluationRuns(): HasMany
{
return $this->hasMany(EvaluationRun::class, 'rule_set_id');
}
// ----- Pomocné metody pro logiku vyhodnocení -----
public function isDistanceScoring(): bool
{
return $this->scoring_mode === 'DISTANCE';
}
public function isFixedPointsScoring(): bool
{
return $this->scoring_mode === 'FIXED_POINTS';
}
public function usesMultipliers(): bool
{
return $this->use_multipliers && $this->multiplier_type !== 'NONE';
}
public function multiplierIsWwl(): bool
{
return $this->multiplier_type === 'WWL';
}
public function multiplierIsDxcc(): bool
{
return $this->multiplier_type === 'DXCC';
}
public function multiplierIsSection(): bool
{
return $this->multiplier_type === 'SECTION';
}
public function multiplierIsCountry(): bool
{
return $this->multiplier_type === 'COUNTRY';
}
public function dupCountsAsPenalty(): bool
{
return $this->dup_qso_policy === 'PENALTY';
}
// ----- Options fallback & typed accessors -----
protected function getOption(string $key, mixed $default = null): mixed
{
$options = $this->options ?? [];
return array_key_exists($key, $options) ? $options[$key] : $default;
}
public function getString(string $key, ?string $columnValue, ?string $default = null): ?string
{
if ($columnValue !== null) {
return $columnValue;
}
$value = $this->getOption($key, $default);
return is_string($value) ? $value : $default;
}
public function getBool(string $key, ?bool $columnValue, bool $default = false): bool
{
if ($columnValue !== null) {
return (bool) $columnValue;
}
return (bool) $this->getOption($key, $default);
}
public function getInt(string $key, ?int $columnValue, ?int $default = null): ?int
{
if ($columnValue !== null) {
return (int) $columnValue;
}
$value = $this->getOption($key, $default);
return $value === null ? null : (int) $value;
}
public function getArray(string $key, ?array $columnValue, array $default = []): array
{
if ($columnValue !== null) {
return $columnValue;
}
$value = $this->getOption($key, $default);
return is_array($value) ? $value : $default;
}
public function ignoreSlashPart(): bool
{
return $this->getBool('ignore_slash_part', $this->ignore_slash_part);
}
public function ignoreThirdPart(): bool
{
return $this->getBool('ignore_third_part', $this->ignore_third_part);
}
public function lettersInRst(): bool
{
return $this->getBool('letters_in_rst', $this->letters_in_rst);
}
public function callsignLevenshteinMax(): int
{
return (int) ($this->getInt('callsign_levenshtein_max', $this->callsign_levenshtein_max, 0) ?? 0);
}
public function callsignSuffixMaxLen(): int
{
return (int) ($this->getInt('callsign_suffix_max_len', $this->callsign_suffix_max_len, 4) ?? 4);
}
public function rstIgnoreThirdChar(): bool
{
return $this->getBool('rst_ignore_third_char', $this->rst_ignore_third_char, true);
}
public function matchRequireLocatorMatch(): bool
{
return $this->getBool('match_require_locator_match', $this->match_require_locator_match, false);
}
public function matchRequireExchangeMatch(): bool
{
return $this->getBool('match_require_exchange_match', $this->match_require_exchange_match, false);
}
public function allowTimeShiftOneHour(): bool
{
return $this->getBool('allow_time_shift_one_hour', $this->allow_time_shift_one_hour, true);
}
public function allowTimeMismatchPairing(): bool
{
return $this->getBool('allow_time_mismatch_pairing', $this->allow_time_mismatch_pairing, false);
}
public function timeMismatchMaxSec(): ?int
{
return $this->getInt('time_mismatch_max_sec', $this->time_mismatch_max_sec, null);
}
public function uniqueQsoEnabled(): bool
{
return $this->getBool('unique_qso_enabled', $this->require_unique_qso, true);
}
public function dupResolutionStrategy(): array
{
$fallback = $this->getArray('duplicate_resolution_strategy', null, [
'paired_first',
'ok_first',
'earlier_time',
'lower_id',
]);
$strategy = $this->getArray('duplicate_resolution_strategy', $this->dup_resolution_strategy, $fallback);
if ($strategy !== $fallback) {
return $strategy;
}
return $this->getArray('dup_resolution_strategy', $this->dup_resolution_strategy, $fallback);
}
public function distanceRounding(): string
{
return $this->getString('distance_rounding', $this->distance_rounding, 'CEIL') ?? 'CEIL';
}
public function minDistanceKm(): ?int
{
return $this->getInt('min_distance_km', $this->min_distance_km, 1);
}
public function nilCountsAsPenalty(): bool
{
return $this->nil_qso_policy === 'PENALTY';
}
public function bustedCallCountsAsPenalty(): bool
{
return $this->busted_call_policy === 'PENALTY';
}
public function bustedExchangeCountsAsPenalty(): bool
{
return $this->busted_exchange_policy === 'PENALTY';
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\User;
use App\Models\EvaluationLock;
use App\Models\EvaluationRunEvent;
class EvaluationRun extends Model
{
use HasFactory;
protected $table = 'evaluation_runs';
protected $fillable = [
'round_id',
'rule_set_id',
'name',
'rules_version',
'result_type',
'is_official',
'notes',
'status',
'batch_id',
'current_step',
'progress_total',
'progress_done',
'scope',
'error',
'started_at',
'finished_at',
'created_by_user_id',
];
protected $casts = [
'round_id' => 'integer',
'rule_set_id' => 'integer',
'is_official' => 'boolean',
'result_type' => 'string',
'progress_total' => 'integer',
'progress_done' => 'integer',
'scope' => 'array',
'started_at' => 'datetime',
'finished_at' => 'datetime',
];
public function round(): BelongsTo
{
return $this->belongsTo(Round::class);
}
public function logResults(): HasMany
{
return $this->hasMany(LogResult::class);
}
public function qsoResults(): HasMany
{
return $this->hasMany(QsoResult::class);
}
public function evaluationLocks(): HasMany
{
return $this->hasMany(EvaluationLock::class);
}
public function events(): HasMany
{
return $this->hasMany(EvaluationRunEvent::class);
}
public function ruleSet(): BelongsTo
{
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by_user_id');
}
public function isCanceled(): bool
{
return strtoupper((string) $this->status) === 'CANCELED';
}
public static function isCanceledRun(int $runId): bool
{
return static::where('id', $runId)->value('status') === 'CANCELED';
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\EvaluationRun;
/**
* Model: EvaluationRunEvent
*
* Účel:
* - Reprezentuje jednu auditní / diagnostickou událost vyhodnocovacího běhu
* (EvaluationRun).
* - Slouží k zaznamenávání průběhu vyhodnocovací pipeline pro účely:
* - monitoringu v administraci
* - diagnostiky chyb
* - auditní stopy (co, kdy a v jakém kroku proběhlo)
*
* Kontext v architektuře:
* - Události jsou vytvářeny během běhu background jobů
* (PrepareRunJob, ParseLogJob, MatchQsoGroupJob, , FinalizeRunJob).
* - Jsou úzce svázány s jedním EvaluationRun a nikdy neexistují samostatně.
*
* Typické použití:
* - Informování UI o aktuálním stavu a průběhu vyhodnocení.
* - Záznam varování (např. nevalidní logy, ignorované QSO).
* - Záznam chyb, které vedly k selhání kroku nebo celého běhu.
*
* Pole modelu:
* - evaluation_run_id:
* Reference na vyhodnocovací běh, ke kterému událost patří.
* - level:
* Úroveň události (např. info / warning / error / debug).
* - message:
* Lidsky čitelný popis události, vhodný pro zobrazení v UI.
* - context:
* Strukturovaná doplňující data (JSON), např.:
* - identifikátory logů nebo QSO
* - technické detaily chyby
* - počty zpracovaných záznamů
*
* Co model NEDĚLÁ:
* - neřídí stav EvaluationRun
* - neobsahuje byznys logiku vyhodnocení
* - neslouží jako systémový log (nahrazuje pouze audit pipeline)
*
* Zásady návrhu:
* - Události mají být zapisovány sekvenčně během běhu pipeline.
* - Neměly by se mazat ani přepisovat (append-only charakter).
* - Slouží jako autoritativní zdroj informací o průběhu vyhodnocení.
*/
class EvaluationRunEvent extends Model
{
protected $table = 'evaluation_run_events';
protected $fillable = [
'evaluation_run_id',
'level',
'message',
'context',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'context' => 'array',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
}

20
app/Models/File.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class File extends Model
{
use HasFactory;
protected $fillable = [
'path',
'filename',
'mimetype',
'filesize',
'hash',
'uploaded_by'
];
}

116
app/Models/Log.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Models\PowerCategory;
class Log extends Model
{
use HasFactory;
protected $table = 'logs';
protected $fillable = [
'round_id',
'file_id',
'accepted',
'processed',
'ip_address',
'tname',
'tdate',
'pcall',
'pwwlo',
'pexch',
'psect',
'pband',
'pclub',
'padr1',
'padr2',
'rname',
'rcall',
'rcoun',
'locator',
'radr1',
'radr2',
'rpoco',
'rcity',
'rphon',
'rhbbs',
'mope1',
'mope2',
'stxeq',
'srxeq',
'sante',
'santh',
'power_watt',
'power_category',
'power_category_id',
'sixhr_category',
'claimed_qso_count',
'claimed_score',
'claimed_wwl',
'claimed_dxcc',
'cqsos',
'cqsop',
'cwwls',
'cwwlb',
'cexcs',
'cexcb',
'cdxcs',
'cdxcb',
'ctosc',
'codxc',
'remarks',
'remarks_eval',
'raw_header',
];
protected $casts = [
'round_id' => 'integer',
'file_id' => 'integer',
'accepted' => 'boolean',
'processed' => 'boolean',
'power_watt' => 'float',
'power_category_id' => 'integer',
'sixhr_category' => 'boolean',
'claimed_qso_count' => 'integer',
'claimed_score' => 'integer',
];
public function round(): BelongsTo
{
return $this->belongsTo(Round::class);
}
public function file(): BelongsTo
{
return $this->belongsTo(File::class);
}
public function qsos(): HasMany
{
return $this->hasMany(LogQso::class);
}
public function logResults(): HasMany
{
return $this->hasMany(LogResult::class);
}
public function powerCategory(): BelongsTo
{
return $this->belongsTo(PowerCategory::class);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LogOverride extends Model
{
use HasFactory;
protected $table = 'log_overrides';
protected $fillable = [
'evaluation_run_id',
'log_id',
'forced_log_status',
'forced_band_id',
'forced_category_id',
'forced_power_category_id',
'forced_sixhr_category',
'forced_power_w',
'reason',
'context',
'created_by_user_id',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'log_id' => 'integer',
'forced_band_id' => 'integer',
'forced_category_id' => 'integer',
'forced_power_category_id' => 'integer',
'forced_sixhr_category' => 'boolean',
'forced_power_w' => 'integer',
'context' => 'array',
'created_by_user_id' => 'integer',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
public function log(): BelongsTo
{
return $this->belongsTo(Log::class);
}
public function forcedBand(): BelongsTo
{
return $this->belongsTo(Band::class, 'forced_band_id');
}
public function forcedCategory(): BelongsTo
{
return $this->belongsTo(Category::class, 'forced_category_id');
}
public function forcedPowerCategory(): BelongsTo
{
return $this->belongsTo(PowerCategory::class, 'forced_power_category_id');
}
public function createdByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by_user_id');
}
}

65
app/Models/LogQso.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LogQso extends Model
{
use HasFactory;
protected $table = 'log_qsos';
protected $fillable = [
'log_id',
'qso_index',
'time_on',
'band',
'freq_khz',
'mode',
'my_call',
'my_rst',
'my_serial',
'my_locator',
'dx_call',
'dx_rst',
'dx_serial',
'rx_wwl',
'rx_exchange',
'mode_code',
'points',
'wwl',
'dxcc',
'new_exchange',
'new_wwl',
'new_dxcc',
'duplicate_qso',
'raw_line',
];
protected $casts = [
'log_id' => 'integer',
'qso_index' => 'integer',
'time_on' => 'datetime',
'freq_khz' => 'integer',
'points' => 'integer',
'new_exchange'=> 'boolean',
'new_wwl' => 'boolean',
'new_dxcc' => 'boolean',
'duplicate_qso'=> 'boolean',
];
public function log(): BelongsTo
{
return $this->belongsTo(Log::class);
}
}

125
app/Models/LogResult.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LogResult extends Model
{
use HasFactory;
protected $table = 'log_results';
protected $fillable = [
'evaluation_run_id',
'log_id',
'band_id',
'category_id',
'power_category_id',
'sixhr_category',
'sixhr_ranking_bucket',
'operating_window_start_utc',
'operating_window_end_utc',
'operating_window_2_start_utc',
'operating_window_2_end_utc',
'operating_window_hours',
'operating_window_qso_count',
'claimed_qso_count',
'claimed_score',
'valid_qso_count',
'dupe_qso_count',
'busted_qso_count',
'other_error_qso_count',
'total_qso_count',
'discarded_qso_count',
'discarded_points',
'discarded_qso_percent',
'unique_qso_count',
'official_score',
'penalty_score',
'base_score',
'multiplier_count',
'multiplier_score',
'score_per_qso',
'rank_overall',
'rank_in_category',
'rank_overall_ok',
'rank_in_category_ok',
'status',
'status_reason',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'log_id' => 'integer',
'band_id' => 'integer',
'category_id' => 'integer',
'power_category_id' => 'integer',
'sixhr_category' => 'boolean',
'sixhr_ranking_bucket' => 'string',
'operating_window_start_utc' => 'datetime',
'operating_window_end_utc' => 'datetime',
'operating_window_2_start_utc' => 'datetime',
'operating_window_2_end_utc' => 'datetime',
'operating_window_hours' => 'integer',
'operating_window_qso_count' => 'integer',
'claimed_qso_count' => 'integer',
'claimed_score' => 'integer',
'valid_qso_count' => 'integer',
'dupe_qso_count' => 'integer',
'busted_qso_count' => 'integer',
'other_error_qso_count' => 'integer',
'total_qso_count' => 'integer',
'discarded_qso_count' => 'integer',
'discarded_points' => 'integer',
'discarded_qso_percent' => 'float',
'unique_qso_count' => 'integer',
'official_score' => 'integer',
'penalty_score' => 'integer',
'base_score' => 'integer',
'multiplier_count' => 'integer',
'multiplier_score' => 'integer',
'score_per_qso' => 'float',
'rank_overall' => 'integer',
'rank_in_category' => 'integer',
'rank_overall_ok' => 'integer',
'rank_in_category_ok' => 'integer',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
public function log(): BelongsTo
{
return $this->belongsTo(Log::class);
}
public function band(): BelongsTo
{
return $this->belongsTo(Band::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function powerCategory(): BelongsTo
{
return $this->belongsTo(PowerCategory::class);
}
}

52
app/Models/NewsPost.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
//use App\Policies\NewsPostPolicy;
//use Illuminate\Database\Eloquent\Attributes\UsePolicy;
//#[UsePolicy(NewsPostPolicy::class)]
class NewsPost extends Model
{
use HasFactory;
use HasTranslations;
protected $table = 'news_posts';
public array $translatable = [
'title',
'content',
'excerpt'
];
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'is_published',
'published_at',
'author_id',
];
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
'author_id' => 'integer',
];
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
// route model binding přes slug (pro /api/news/{slug})
public function getRouteKeyName(): string
{
return 'slug';
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PowerCategory extends Model
{
protected $table = 'power_categories';
use HasFactory;
protected $fillable = [
'name',
'order',
'power_level'
];
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class QsoOverride extends Model
{
use HasFactory;
protected $table = 'qso_overrides';
protected $fillable = [
'evaluation_run_id',
'log_qso_id',
'forced_matched_log_qso_id',
'forced_status',
'forced_points',
'forced_penalty',
'reason',
'context',
'created_by_user_id',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'log_qso_id' => 'integer',
'forced_matched_log_qso_id' => 'integer',
'forced_points' => 'float',
'forced_penalty' => 'float',
'context' => 'array',
'created_by_user_id' => 'integer',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
public function logQso(): BelongsTo
{
return $this->belongsTo(LogQso::class);
}
public function forcedMatchedLogQso(): BelongsTo
{
return $this->belongsTo(LogQso::class, 'forced_matched_log_qso_id');
}
public function createdByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by_user_id');
}
}

92
app/Models/QsoResult.php Normal file
View File

@@ -0,0 +1,92 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
class QsoResult extends Model
{
use HasFactory;
protected $table = 'qso_results';
protected $fillable = [
'evaluation_run_id',
'log_qso_id',
'is_valid',
'is_duplicate',
'is_nil',
'is_busted_call',
'is_busted_rst',
'is_busted_exchange',
'is_time_out_of_window',
'is_operating_window_excluded',
'points',
'penalty_points',
'distance_km',
'time_diff_sec',
'wwl',
'dxcc',
'country',
'section',
'matched_qso_id',
'matched_log_qso_id',
'match_type',
'match_confidence',
'error_flags',
'error_code',
'error_side',
'error_detail',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'log_qso_id' => 'integer',
'is_valid' => 'boolean',
'is_duplicate' => 'boolean',
'is_nil' => 'boolean',
'is_busted_call' => 'boolean',
'is_busted_rst' => 'boolean',
'is_busted_exchange' => 'boolean',
'is_time_out_of_window' => 'boolean',
'is_operating_window_excluded' => 'boolean',
'points' => 'integer',
'penalty_points' => 'integer',
'distance_km' => 'float',
'time_diff_sec' => 'integer',
'matched_qso_id' => 'integer',
'matched_log_qso_id' => 'integer',
'error_flags' => 'array',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
public function logQso(): BelongsTo
{
return $this->belongsTo(LogQso::class);
}
public function matchedQso(): BelongsTo
{
return $this->belongsTo(LogQso::class, 'matched_qso_id');
}
public function workingQso(): HasOne
{
return $this->hasOne(WorkingQso::class, 'log_qso_id', 'log_qso_id');
}
}

97
app/Models/Round.php Normal file
View File

@@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Spatie\Translatable\HasTranslations;
use App\Models\EvaluationRuleSet;
class Round extends Model
{
use HasFactory;
use HasTranslations;
public array $translatable = [
'name',
'description',
];
protected $table = 'rounds';
use HasFactory;
protected $fillable = [
'contest_id',
'rule_set_id',
'preliminary_evaluation_run_id',
'official_evaluation_run_id',
'test_evaluation_run_id',
'name',
'description',
'is_active',
'is_test',
'is_sixhr',
'start_time',
'end_time',
'logs_deadline',
'first_check',
'second_check',
'unique_qso_check',
'third_check',
'fourth_check',
'prelimitary_results',
];
protected $casts = [
'contest_id' => 'integer',
'rule_set_id' => 'integer',
'preliminary_evaluation_run_id' => 'integer',
'official_evaluation_run_id' => 'integer',
'test_evaluation_run_id' => 'integer',
'name' => 'array',
'description' => 'array',
'is_active' => 'boolean',
'is_test' => 'boolean',
'is_sixhr' => 'boolean',
'start_time' => 'datetime',
'end_time' => 'datetime',
'logs_deadline' => 'datetime',
'first_check' => 'datetime',
'second_check' => 'datetime',
'unique_qso_check' => 'datetime',
'third_check' => 'datetime',
'fourth_check' => 'datetime',
'prelimitary_results'=> 'datetime',
];
public function contest(): BelongsTo
{
return $this->belongsTo(Contest::class);
}
public function ruleSet(): BelongsTo
{
return $this->belongsTo(EvaluationRuleSet::class, 'rule_set_id');
}
public function bands(): BelongsToMany
{
return $this->belongsToMany(Band::class, 'rounds_bands', 'round_id', 'band_id');
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'rounds_categories', 'round_id', 'category_id');
}
public function powerCategories(): BelongsToMany
{
return $this->belongsToMany(PowerCategory::class, 'rounds_power_categories', 'round_id', 'power_category_id');
}
}

53
app/Models/User.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, HasApiTokens;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'is_admin',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
'is_active' => 'boolean',
];
}
}

56
app/Models/WorkingQso.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WorkingQso extends Model
{
use HasFactory;
protected $table = 'working_qsos';
protected $fillable = [
'evaluation_run_id',
'log_qso_id',
'log_id',
'ts_utc',
'call_norm',
'rcall_norm',
'loc_norm',
'rloc_norm',
'band_id',
'mode',
'match_key',
'dupe_key',
'out_of_window',
'errors',
];
protected $casts = [
'evaluation_run_id' => 'integer',
'log_qso_id' => 'integer',
'log_id' => 'integer',
'band_id' => 'integer',
'out_of_window' => 'boolean',
'errors' => 'array',
'ts_utc' => 'datetime',
];
public function evaluationRun(): BelongsTo
{
return $this->belongsTo(EvaluationRun::class);
}
public function logQso(): BelongsTo
{
return $this->belongsTo(LogQso::class);
}
public function log(): BelongsTo
{
return $this->belongsTo(Log::class);
}
}