first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
brusnitsyn
2026-04-06 00:06:00 +09:00
commit fb2e6c58e3
409 changed files with 42953 additions and 0 deletions

59
app/Models/Department.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Database\Factories\DepartmentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['name', 'is_active', 'department_profile_id'])]
class Department extends Model
{
/** @use HasFactory<DepartmentFactory> */
use HasFactory;
/**
* Get the profile that the department belongs to.
*
* @return BelongsTo<DepartmentProfile, $this>
*/
public function departmentProfile(): BelongsTo
{
return $this->belongsTo(DepartmentProfile::class);
}
/**
* Get the services provided by the department.
*
* @return HasMany<ServiceEntry, $this>
*/
public function providedServiceEntries(): HasMany
{
return $this->hasMany(ServiceEntry::class, 'provider_department_id');
}
/**
* Get the services received by the department.
*
* @return HasMany<ServiceEntry, $this>
*/
public function receivedServiceEntries(): HasMany
{
return $this->hasMany(ServiceEntry::class, 'recipient_department_id');
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_active' => 'boolean',
];
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Database\Factories\DepartmentProfileFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['name'])]
class DepartmentProfile extends Model
{
/** @use HasFactory<DepartmentProfileFactory> */
use HasFactory;
/**
* Get the departments for the profile.
*
* @return HasMany<Department, $this>
*/
public function departments(): HasMany
{
return $this->hasMany(Department::class);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Database\Factories\MedicationExpenseRowFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['report_period_id', 'department_id'])]
class MedicationExpenseRow extends Model
{
/** @use HasFactory<MedicationExpenseRowFactory> */
use HasFactory;
/**
* Get the period that owns the row.
*
* @return BelongsTo<ReportPeriod, $this>
*/
public function reportPeriod(): BelongsTo
{
return $this->belongsTo(ReportPeriod::class);
}
/**
* Get the department that owns the row.
*
* @return BelongsTo<Department, $this>
*/
public function department(): BelongsTo
{
return $this->belongsTo(Department::class);
}
/**
* Get the values for the row.
*
* @return HasMany<MedicationExpenseValue, $this>
*/
public function values(): HasMany
{
return $this->hasMany(MedicationExpenseValue::class);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use App\Enums\ExpenseCategory;
use App\Enums\FundingSource;
use Database\Factories\MedicationExpenseValueFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable(['medication_expense_row_id', 'funding_source', 'expense_category', 'amount'])]
class MedicationExpenseValue extends Model
{
/** @use HasFactory<MedicationExpenseValueFactory> */
use HasFactory;
/**
* Get the row that owns the value.
*
* @return BelongsTo<MedicationExpenseRow, $this>
*/
public function medicationExpenseRow(): BelongsTo
{
return $this->belongsTo(MedicationExpenseRow::class);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'amount' => 'decimal:2',
'funding_source' => FundingSource::class,
'expense_category' => ExpenseCategory::class,
];
}
}

59
app/Models/Membership.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use App\Enums\TeamRole;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
#[Fillable(['team_id', 'user_id', 'role'])]
class Membership extends Pivot
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'team_members';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = true;
/**
* Get the team that the membership belongs to.
*
* @return BelongsTo<Team, $this>
*/
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
/**
* Get the user that belongs to this membership.
*
* @return BelongsTo<Model, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'role' => TeamRole::class,
];
}
}

100
app/Models/ReportPeriod.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
namespace App\Models;
use App\Enums\ReportPeriodStatus;
use Database\Factories\ReportPeriodFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['team_id', 'year', 'month', 'status'])]
class ReportPeriod extends Model
{
/** @use HasFactory<ReportPeriodFactory> */
use HasFactory;
/**
* Get the team that owns the period.
*
* @return BelongsTo<Team, $this>
*/
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
/**
* Get the service entries for the period.
*
* @return HasMany<ServiceEntry, $this>
*/
public function serviceEntries(): HasMany
{
return $this->hasMany(ServiceEntry::class);
}
/**
* Get the medication expense rows for the period.
*
* @return HasMany<MedicationExpenseRow, $this>
*/
public function medicationExpenseRows(): HasMany
{
return $this->hasMany(MedicationExpenseRow::class);
}
/**
* Determine whether the period can still be edited.
*/
public function isEditable(): bool
{
return $this->status === ReportPeriodStatus::Draft;
}
/**
* Get a human-readable label for the period.
*/
public function label(): string
{
return sprintf('%s %s', $this->monthName(), $this->year);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'year' => 'integer',
'month' => 'integer',
'status' => ReportPeriodStatus::class,
];
}
/**
* Get the localized month name.
*/
protected function monthName(): string
{
return match ($this->month) {
1 => 'Январь',
2 => 'Февраль',
3 => 'Март',
4 => 'Апрель',
5 => 'Май',
6 => 'Июнь',
7 => 'Июль',
8 => 'Август',
9 => 'Сентябрь',
10 => 'Октябрь',
11 => 'Ноябрь',
12 => 'Декабрь',
default => (string) $this->month,
};
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Database\Factories\ServiceCatalogFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['code', 'name', 'unit', 'default_price', 'sort_order', 'is_active'])]
class ServiceCatalog extends Model
{
/** @use HasFactory<ServiceCatalogFactory> */
use HasFactory;
/**
* Get the service entries for the catalog item.
*
* @return HasMany<ServiceEntry, $this>
*/
public function serviceEntries(): HasMany
{
return $this->hasMany(ServiceEntry::class);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'default_price' => 'decimal:2',
'sort_order' => 'integer',
'is_active' => 'boolean',
];
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Models;
use Database\Factories\ServiceEntryFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'report_period_id',
'service_catalog_id',
'provider_department_id',
'recipient_department_id',
'quantity',
'unit_price',
])]
class ServiceEntry extends Model
{
/** @use HasFactory<ServiceEntryFactory> */
use HasFactory;
/**
* Get the period that owns the service entry.
*
* @return BelongsTo<ReportPeriod, $this>
*/
public function reportPeriod(): BelongsTo
{
return $this->belongsTo(ReportPeriod::class);
}
/**
* Get the service definition for the entry.
*
* @return BelongsTo<ServiceCatalog, $this>
*/
public function serviceCatalog(): BelongsTo
{
return $this->belongsTo(ServiceCatalog::class);
}
/**
* Get the provider department.
*
* @return BelongsTo<Department, $this>
*/
public function providerDepartment(): BelongsTo
{
return $this->belongsTo(Department::class, 'provider_department_id');
}
/**
* Get the recipient department.
*
* @return BelongsTo<Department, $this>
*/
public function recipientDepartment(): BelongsTo
{
return $this->belongsTo(Department::class, 'recipient_department_id');
}
/**
* Get the total amount for the entry.
*/
public function totalAmount(): float
{
return round((float) $this->quantity * (float) $this->unit_price, 2);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'quantity' => 'decimal:2',
'unit_price' => 'decimal:2',
];
}
}

113
app/Models/Team.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace App\Models;
use App\Concerns\GeneratesUniqueTeamSlugs;
use App\Enums\TeamRole;
use Database\Factories\TeamFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Fillable(['name', 'slug', 'is_personal'])]
class Team extends Model
{
/** @use HasFactory<TeamFactory> */
use GeneratesUniqueTeamSlugs, HasFactory, SoftDeletes;
/**
* Bootstrap the model and its traits.
*/
protected static function boot(): void
{
parent::boot();
static::creating(function (Team $team) {
if (empty($team->slug)) {
$team->slug = static::generateUniqueTeamSlug($team->name);
}
});
static::updating(function (Team $team) {
if ($team->isDirty('name')) {
$team->slug = static::generateUniqueTeamSlug($team->name, $team->id);
}
});
}
/**
* Get the team owner.
*/
public function owner(): ?Model
{
return $this->members()
->wherePivot('role', TeamRole::Owner->value)
->first();
}
/**
* Get all members of this team.
*
* @return BelongsToMany<Model, $this>
*/
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class, 'team_members', 'team_id', 'user_id')
->using(Membership::class)
->withPivot(['role'])
->withTimestamps();
}
/**
* Get all memberships for this team.
*
* @return HasMany<Membership, $this>
*/
public function memberships(): HasMany
{
return $this->hasMany(Membership::class);
}
/**
* Get all invitations for this team.
*
* @return HasMany<TeamInvitation, $this>
*/
public function invitations(): HasMany
{
return $this->hasMany(TeamInvitation::class);
}
/**
* Get all report periods for this team.
*
* @return HasMany<ReportPeriod, $this>
*/
public function reportPeriods(): HasMany
{
return $this->hasMany(ReportPeriod::class);
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'is_personal' => 'boolean',
];
}
/**
* Get the route key for the model.
*/
public function getRouteKeyName(): string
{
return 'slug';
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Models;
use App\Enums\TeamRole;
use Database\Factories\TeamInvitationFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
#[Fillable(['team_id', 'email', 'role', 'invited_by', 'expires_at', 'accepted_at'])]
class TeamInvitation extends Model
{
/** @use HasFactory<TeamInvitationFactory> */
use HasFactory;
/**
* Bootstrap the model and its traits.
*/
protected static function boot(): void
{
parent::boot();
static::creating(function (TeamInvitation $invitation) {
if (empty($invitation->code)) {
$invitation->code = Str::random(64);
}
});
}
/**
* Get the team that the invitation belongs to.
*
* @return BelongsTo<Team, $this>
*/
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
/**
* Get the user who sent the invitation.
*
* @return BelongsTo<Model, $this>
*/
public function inviter(): BelongsTo
{
return $this->belongsTo(User::class, 'invited_by');
}
/**
* Determine if the invitation has been accepted.
*/
public function isAccepted(): bool
{
return $this->accepted_at !== null;
}
/**
* Determine if the invitation is pending.
*/
public function isPending(): bool
{
return $this->accepted_at === null && ! $this->isExpired();
}
/**
* Determine if the invitation has expired.
*/
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
}
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'role' => TeamRole::class,
'expires_at' => 'datetime',
'accepted_at' => 'datetime',
];
}
/**
* Get the route key for the model.
*/
public function getRouteKeyName(): string
{
return 'code';
}
}

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

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Concerns\HasTeams;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
#[Fillable(['name', 'email', 'password', 'current_team_id'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, HasTeams, Notifiable, TwoFactorAuthenticatable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'two_factor_confirmed_at' => 'datetime',
];
}
}