Initial Laravel API import
Some checks failed
continuous-integration/drone/push Build is failing

- Complete GGZ Ecademy Laravel backend application
- RESTful API for learning products, members, filters
- Authentication and authorization system
- Database migrations and seeders
- Custom CRUD generator commands
- Email notification system
- Integration with frontend applications
This commit is contained in:
Joris Slagter
2025-12-02 17:40:21 +01:00
parent 786b6b6a78
commit df155bb13d
341 changed files with 116385 additions and 2 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Accreditation extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
protected $with = array('filters');
public function learning_product()
{
return $this->belongsTo(LearningProduct::class);
}
public function filters()
{
return $this->morphMany(FilterItemsAssociation::class, 'filter_items_associations');
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Address extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class Branch extends Model
{
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Relations\Pivot;
class BranchMember extends Pivot
{
protected $hidden = ['pivot'];
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class Checklist extends Model
{
protected $guarded = ['id'];
public $timestamps = false;
public function category()
{
return $this->belongsTo(ChecklistCategory::class);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class ChecklistCategory extends Model
{
protected $guarded = ['id'];
public $timestamps = false;
public function items()
{
return $this->hasMany(Checklist::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class ChecklistVersion extends Model
{
public $timestamps = ["created_at"];
const UPDATED_AT = null;
protected $guarded = ['id'];
protected $casts = [
'created_at' => 'date:d M Y',
];
protected $with = ['user'];
public function version()
{
return $this->belongsTo(Version::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Contribution extends Model
{
// use SoftDeletes;
// protected $dates = ['deleted_at'];
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Repositories;
use DateTime;
use DateInterval;
use Illuminate\Database\Eloquent\Model;
class CourseNotification extends Model
{
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
public function learning_product()
{
return $this->belongsTo(LearningProduct::class);
}
public function scopeNotExpired($query)
{
return $query->where('date', '>', new DateTime());
}
public function scopeExpired($query)
{
return $query->where('date', '<', new DateTime());
}
public function scopeExpireInFiveMinutes($query)
{
$minutes_to_add = 5;
$time = new DateTime();
$time->add(new DateInterval('PT' . $minutes_to_add . 'M'));
// $stamp = $time->format('Y-m-d H:i');
return $query->where('date', '<', $time);
}
public function scopeNotSent($query)
{
return $query->where('sent', false);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Repositories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
class Filter extends Model
{
protected $guarded = ['id'];
protected $appends = ['slug'];
public $timestamps = false;
public function getSlugAttribute()
{
return $this->id . '-' . Str::slug($this->title);
}
public function items()
{
return $this->hasMany(FilterItem::class)->orderBy('title');
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class FilterItem extends Model
{
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
public $timestamps = false;
public function filter()
{
return $this->belongsTo(Filter::class);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class FilterItemsAssociation extends Model
{
protected $guarded = ['id'];
public function morphable()
{
return $this->morphTo();
}
public function filter_item()
{
return $this->belongsTo(FilterItem::class);
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace App\Repositories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class LearningProduct extends Model implements HasMedia
{
use SoftDeletes, InteractsWithMedia;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
protected $appends = ['slug'];
protected $casts = [
'for_members' => 'boolean',
'third_party_training' => 'boolean',
'voor_opleiders' => 'boolean',
];
public function getSlugAttribute()
{
return $this->id . '-' . Str::slug($this->title);
}
public function registerMediaCollections(): void
{
$this
->addMediaCollection('learning_products_covers')
->singleFile();
$this
->addMediaCollection('learning_products_tiles')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->height(50);
}
public function draft()
{
return $this->hasOne('App\Repositories\LearningProduct', 'parent_id', 'id');
}
public function filters()
{
return $this->morphMany(FilterItemsAssociation::class, 'filter_items_associations');
}
public function versions()
{
return $this->hasMany(Version::class);
}
public function accreditations()
{
return $this->hasMany(Accreditation::class);
}
public function notifications()
{
return $this->hasMany(CourseNotification::class);
}
public function synonyms()
{
return $this->belongsToMany(Synonym::class);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Relations\Pivot;
class LearningProductSynonym extends Pivot
{
public $incrementing = true;
public $timestamps = false;
protected $guarded = ['id'];
protected $table = 'learning_product_synonym';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class ManagementLink extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Repositories;
use App\Repositories\QueryBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Member extends Model implements HasMedia, QueryBuilder\ConfigProvider
{
use SoftDeletes, InteractsWithMedia;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
public function getSlugAttribute()
{
return $this->id . '-' . Str::slug($this->formal_name);
}
public function summaries()
{
return $this->hasMany(Summary::class);
}
public function contributions()
{
return $this->hasMany(Contribution::class);
}
public function addresses()
{
return $this->hasMany(Address::class);
}
public function contacts()
{
return $this->hasMany(Contact::class);
}
public function management_links()
{
return $this->hasMany(ManagementLink::class);
}
public function main_branch()
{
return $this->hasOne(Branch::class, 'id', 'branch_id');
}
public function revision()
{
return $this->hasOne(Revision::class);
}
public function sub_branches()
{
return $this->belongsToMany(Branch::class, 'branch_members')->using(BranchMember::class);
}
public function users()
{
return $this->belongsToMany(User::class, 'member_users');
}
public function registerMediaCollections(): void
{
$this
->addMediaCollection('members_logos')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->height(50);
}
public static function getQueryBuilderConfig(): QueryBuilder\Config
{
return (new QueryBuilder\Config())
->setAllowedFilters(['show_on_website'])
->setAllowedSorts(['id', 'informal_name']);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Repositories\QueryBuilder;
class Config
{
/**
* @var string[]
*/
private array $allowedAppends = [];
/**
* @var string[]
*/
private array $allowedFields = [];
/**
* @var string[]
*/
private array $allowedFilters = [];
/**
* @var string[]
*/
private array $allowedIncludes = [];
/**
* @var string[]
*/
private array $allowedSorts = [];
/**
* @param string[] $allowedAppends
* @return self
*/
public function setAllowedAppends(array $allowedAppends): self
{
$this->allowedAppends = $allowedAppends;
return $this;
}
/**
* @return string[]
*/
public function getAllowedAppends(): array
{
return $this->allowedAppends;
}
/**
* @param string[] $allowedFields
* @return self
*/
public function setAllowedFields(array $allowedFields): self
{
$this->allowedFields = $allowedFields;
return $this;
}
/**
* @return string[]
*/
public function getAllowedFields(): array
{
return $this->allowedFields;
}
/**
* @param string[] $allowedFilters
* @return self
*/
public function setAllowedFilters(array $allowedFilters): self
{
$this->allowedFilters = $allowedFilters;
return $this;
}
/**
* @return string[]
*/
public function getAllowedFilters(): array
{
return $this->allowedFilters;
}
/**
* @param string[] $allowedIncludes
* @return self
*/
public function setAllowedIncludes(array $allowedIncludes): self
{
$this->allowedIncludes = $allowedIncludes;
return $this;
}
/**
* @return string[]
*/
public function getAllowedIncludes(): array
{
return $this->allowedIncludes;
}
/**
* @param string[] $allowedSorts
* @return self
*/
public function setAllowedSorts(array $allowedSorts): self
{
$this->allowedSorts = $allowedSorts;
return $this;
}
/**
* @return string[]
*/
public function getAllowedSorts(): array
{
return $this->allowedSorts;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Repositories\QueryBuilder;
interface ConfigProvider
{
public static function getQueryBuilderConfig(): Config;
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class Revision extends Model
{
protected $guarded = ['id'];
protected $appends = ['hasChanges'];
protected $with = array('user', 'revisor');
public function user()
{
return $this->belongsTo(User::class);
}
public function revisor()
{
return $this->belongsTo(User::class);
}
public function getHasChangesAttribute()
{
return $this->updated_at != $this->accepted_at;
}
}

20
app/Repositories/Role.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
public $timestamps = false;
protected $fillable = [
'name', 'color'
];
public function users()
{
return $this->belongsToMany(User::class);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Webmozart\Assert\Assert;
class Summary extends Model
{
protected $guarded = ['id'];
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
protected static function booted()
{
static::created(fn (self $summary) => static::cascadeAttributes($summary));
static::updated(fn (self $summary) => static::cascadeAttributes($summary));
static::deleting(fn (self $summary) => static::cascadeAttributes($summary));
}
/**
* @todo extract to generic subsystem
*/
private static function cascadeAttributes(self $summary): void
{
static::maybeCascadeAttribute($summary, 'updated_at', 'member', 'updated_at');
}
/**
* @todo extract to generic subsystem
*/
private static function maybeCascadeAttribute(
self $summary,
string $sourceAttribute,
string $relationship,
string $targetAttribute
): void
{
$target = $summary->{$relationship} ?? null;
if (is_null($target)) {
return;
}
/** @var Model $target */
Assert::isAOf($target, Model::class);
$target->withoutEvents(fn () => static::cascadeAttribute($summary, $target, $sourceAttribute, $targetAttribute));
}
/**
* @todo extract to generic subsystem
*/
private static function cascadeAttribute(self $summary, Model $target, string $sourceAttribute, string $targetAttribute): void
{
$target->update([
$targetAttribute => $summary->{$sourceAttribute},
'timestamps' => false,
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Repositories;
// use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Synonym extends Model
{
public $timestamps = false;
protected $guarded = ['id'];
protected $hidden = ['pivot'];
}

126
app/Repositories/User.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
namespace App\Repositories;
use Laravel\Sanctum\HasApiTokens;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Notifications\Notifiable;
use Spatie\MediaLibrary\InteractsWithMedia;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Notifications\PasswordResetNotification;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasMedia
{
use HasApiTokens, Notifiable, InteractsWithMedia;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'first_name', 'last_name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
protected $appends = [
'fullName',
'isMemberEditor',
'isAdmin',
'isOperator',
'isUser',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function roles()
{
return $this->belongsToMany(Role::class);
}
public function hasAnyRoles($roles)
{
return $this->roles()->whereIn('name', $roles)->first() ? true : false;
}
public function hasRole($role)
{
return $this->roles()->where('name', $role)->first() ? true : false;
}
public function members()
{
return $this->belongsToMany(Member::class, 'member_users');
}
public function getIsMemberEditorAttribute()
{
return $this->members->count() > 0;
}
public function registerMediaCollections(): void
{
$this
->addMediaCollection('profile_pics')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(50)
->height(50);
}
public function sendPasswordResetNotification($token)
{
$this->notify(new PasswordResetNotification($token));
}
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
public function getIsSuperAdminAttribute()
{
return $this->hasRole('super_admin');
}
public function getIsAdminAttribute()
{
return $this->hasRole('admin');
}
public function getIsOperatorAttribute()
{
return $this->hasRole('operator');
}
public function getIsUserAttribute()
{
return $this->hasRole('user');
}
// public function setPasswordAttribute($password)
// {
// $this->attributes['password'] = bcrypt($password);
// }
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Repositories;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Version extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $guarded = ['id'];
protected $with = ['filters', 'checklists'];
public function learning_product()
{
return $this->belongsTo(LearningProduct::class);
}
public function filters()
{
return $this->morphMany(FilterItemsAssociation::class, 'filter_items_associations');
}
public function checklists()
{
return $this->hasMany(ChecklistVersion::class);
}
}