Initial commit
This commit is contained in:
77
app/Models/Account.php
Normal file
77
app/Models/Account.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use App\Traits\AccountInfoTrait;
|
||||
use App\Traits\ActiveStatesTrait;
|
||||
use App\Traits\ProfilePermissionTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Account extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use AccountInfoTrait;
|
||||
use ActiveStatesTrait;
|
||||
use ProfilePermissionTrait;
|
||||
|
||||
|
||||
protected $fillable = [];
|
||||
protected $table = 'accounts';
|
||||
|
||||
/**
|
||||
* Accessor for balance attribute.
|
||||
* Calculates the balance by calling getBalance() method from AccountInfoTrait.
|
||||
*
|
||||
* @return float|int
|
||||
*/
|
||||
public function getBalanceAttribute()
|
||||
{
|
||||
return $this->getBalance($this->id);
|
||||
}
|
||||
|
||||
// Get all of the owning accountable tables: users and organizations
|
||||
// One-to-many polymorphic
|
||||
public function accountable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
// Define the relationship for transactions where the account is the sender
|
||||
public function transactionsFrom()
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'from_account_id');
|
||||
}
|
||||
|
||||
// Define the relationship for transactions where the account is the receiver
|
||||
public function transactionsTo()
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'to_account_id');
|
||||
}
|
||||
|
||||
// Define a combined relationship for all transactions involving the account
|
||||
public function transactions()
|
||||
{
|
||||
return $this->transactionsFrom()->union($this->transactionsTo());
|
||||
}
|
||||
|
||||
public static function accountsCyclosMember($cyclos_id)
|
||||
{
|
||||
return Account::with('accountable')
|
||||
->whereHas('accountable', function ($query) use ($cyclos_id) {
|
||||
$query->where('cyclos_id', $cyclos_id)
|
||||
->whereNull('inactive_at');
|
||||
})
|
||||
->pluck('name', 'id');
|
||||
|
||||
}
|
||||
|
||||
//get all accounts owned by the same accountable
|
||||
public function getAccountsBySameAccountable()
|
||||
{
|
||||
return Account::where('accountable_id', $this->accountable_id)
|
||||
->where('accountable_type', $this->accountable_type)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
322
app/Models/Admin.php
Normal file
322
app/Models/Admin.php
Normal file
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\User;
|
||||
use App\Notifications\NonUserPasswordResetNotification;
|
||||
use App\Notifications\VerifyProfileEmail;
|
||||
use App\Traits\ActiveStatesTrait;
|
||||
use App\Traits\HasPresence;
|
||||
use App\Traits\LocationTrait;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Auth\MustVerifyEmail as AuthMustVerifyEmail;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Namu\WireChat\Traits\Chatable;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class Admin extends Authenticatable implements MustVerifyEmail, CanResetPasswordContract
|
||||
{
|
||||
use HasFactory;
|
||||
use AuthMustVerifyEmail;
|
||||
use Notifiable;
|
||||
use HasRoles;
|
||||
use LogsActivity; // Spatie Activity Log
|
||||
use HasProfilePhoto;
|
||||
use TaggableWithLocale;
|
||||
use LocationTrait;
|
||||
use CanResetPassword; // Trait
|
||||
use Chatable;
|
||||
use ActiveStatesTrait;
|
||||
use HasPresence;
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
* BEWARE: API's CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'full_name',
|
||||
'password',
|
||||
'remember_token',
|
||||
'phone',
|
||||
'cyclos_id',
|
||||
'cyclos_salt',
|
||||
'two_factor_confirmed_at',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_login_at',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'full_name',
|
||||
'email',
|
||||
'profile_photo_path',
|
||||
'about',
|
||||
'about_short',
|
||||
'motivation',
|
||||
'website',
|
||||
'phone',
|
||||
'phone_public',
|
||||
'password',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'last_login_at',
|
||||
'last_login_ip'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'phone_public' => 'boolean',
|
||||
'email_verified_at' => 'datetime',
|
||||
'inactive_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Get the admin's user(s) that can manage admin profiles.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related languages of the admin.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function languages()
|
||||
{
|
||||
return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related the locations of the admin.
|
||||
* One-to-many polymorph.
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->morphMany(Location::class, 'locatable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related accounts of the user.
|
||||
* One-to-many polymorphic.
|
||||
* NOTE: At the moment admins do not use accounts, but to keep methods consistent
|
||||
* with the other profile models, we keep this method.
|
||||
* Removing this method will break generic methods that will are used on all profile models!
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return $this->morphMany(Account::class, 'accountable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the admin's message settings.
|
||||
*/
|
||||
public function message_settings()
|
||||
{
|
||||
return $this->morphMany(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
|
||||
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
\Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
// Construct the full reset URL using the named route
|
||||
// The route 'non-user.password.reset' expects 'profileType' and 'token' parameters.
|
||||
// It's also good practice to include the email in the query string for the reset form.
|
||||
$resetUrl = route('non-user.password.reset', [
|
||||
'profileType' => 'admin', // Hardcode 'admin' for the Admin model
|
||||
'token' => $token,
|
||||
'email' => $this->getEmailForPasswordReset(), // Method from CanResetPassword trait
|
||||
]);
|
||||
|
||||
// Pass the fully constructed URL to your notification
|
||||
$this->notify(new NonUserPasswordResetNotification($resetUrl, $this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the activity log options for the User model.
|
||||
*
|
||||
* This method sets up the logging to:
|
||||
* - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
|
||||
* - Log only when these attributes have been modified (dirty).
|
||||
* - Prevent submission of empty logs.
|
||||
* - Use 'user' as the log name.
|
||||
*
|
||||
* @return \Spatie\Activitylog\LogOptions
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly([
|
||||
'name',
|
||||
'last_login_ip',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->logOnlyDirty() // Only log attributes that have been changed
|
||||
->dontSubmitEmptyLogs()
|
||||
->useLogName('Admin');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the URL for the user's cover image (avatar).
|
||||
* Adjust the 'avatar_url' field to your database setup.
|
||||
*/
|
||||
public function getCoverUrlAttribute(): ?string
|
||||
{
|
||||
return Storage::url($this->profile_photo_path) ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat:Returns the URL for the user's profile page.
|
||||
* Adjust the 'profile' route as needed for your setup.
|
||||
*/
|
||||
public function getProfileUrlAttribute(): ?string
|
||||
{
|
||||
return route('profile.show_by_type_and_id', ['type' => __('admin'), 'id' => $this->id]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the display name for the user.
|
||||
* Modify this to use your preferred name field.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): ?string
|
||||
{
|
||||
if ($this->full_name !== $this->name) {
|
||||
return $this->full_name . ' (' . $this->name . ')';
|
||||
} elseif ($this->name) {
|
||||
return $this->name;
|
||||
} else {
|
||||
return __('Admin');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Search for users when creating a new chat or adding members to a group.
|
||||
* Customize the search logic to limit results, such as restricting to friends or eligible users only.
|
||||
*/
|
||||
public function searchChatables(string $query): ?\Illuminate\Support\Collection
|
||||
{
|
||||
$searchableFields = ['name', 'full_name'];
|
||||
|
||||
// Search across all profile types
|
||||
$users = \App\Models\User::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$organizations = \App\Models\Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$banks = \App\Models\Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$admins = Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
// Combine all results into a base Collection to avoid serialization issues
|
||||
$results = collect()
|
||||
->merge($users->all())
|
||||
->merge($organizations->all())
|
||||
->merge($banks->all())
|
||||
->merge($admins->all());
|
||||
|
||||
// Filter out profiles based on configuration
|
||||
return $results->filter(function ($profile) {
|
||||
// Check inactive profiles
|
||||
if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check email unverified profiles
|
||||
if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check incomplete profiles
|
||||
if (
|
||||
timebank_config('profile_incomplete.messenger_hidden')
|
||||
&& method_exists($profile, 'hasIncompleteProfile')
|
||||
&& $profile->hasIncompleteProfile($profile)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->take(6)->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Determine if the user can create new groups.
|
||||
*/
|
||||
public function canCreateGroups(): bool
|
||||
{
|
||||
return timebank_config('profiles.bank.messenger_can_create_groups');
|
||||
}
|
||||
}
|
||||
11
app/Models/Article.php
Normal file
11
app/Models/Article.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Article extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
533
app/Models/Bank.php
Normal file
533
app/Models/Bank.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\User;
|
||||
use App\Notifications\NonUserPasswordResetNotification;
|
||||
use App\Notifications\VerifyProfileEmail;
|
||||
use App\Traits\ActiveStatesTrait;
|
||||
use App\Traits\HasPresence;
|
||||
use App\Traits\LocationTrait;
|
||||
use App\Traits\ProfileTrait;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
|
||||
use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableInterface;
|
||||
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
|
||||
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
|
||||
use Illuminate\Auth\MustVerifyEmail as AuthMustVerifyEmail;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Namu\WireChat\Traits\Chatable;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class Bank extends Authenticatable implements ReacterableInterface, ReactableInterface, MustVerifyEmail, CanResetPasswordContract
|
||||
{
|
||||
use HasFactory;
|
||||
use HasProfilePhoto;
|
||||
use AuthMustVerifyEmail;
|
||||
use Notifiable;
|
||||
use HasRoles;
|
||||
use LogsActivity; // Spatie Activity Log
|
||||
use TaggableWithLocale;
|
||||
use Reacterable; // cybercog/laravel-love
|
||||
use Reactable; // cybercog/laravel-love
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
use LocationTrait;
|
||||
use CanResetPassword; // Trait
|
||||
use Chatable;
|
||||
use ActiveStatesTrait;
|
||||
use HasPresence;
|
||||
use ProfileTrait;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
* BEWARE: API'S CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'full_name',
|
||||
'password',
|
||||
'remember_token',
|
||||
'phone',
|
||||
'cyclos_id',
|
||||
'cyclos_salt',
|
||||
'two_factor_confirmed_at',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_login_at',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'full_name',
|
||||
'email',
|
||||
'profile_photo_path',
|
||||
'about',
|
||||
'about_short',
|
||||
'motivation',
|
||||
'website',
|
||||
'phone',
|
||||
'phone_public',
|
||||
'password',
|
||||
'level',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'last_login_at',
|
||||
'last_login_ip'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'phone_public' => 'boolean',
|
||||
'date_of_birth' => 'date',
|
||||
'email_verified_at' => 'datetime',
|
||||
'inactive_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $guard_name = 'web';
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'banks_index';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert this model to a searchable array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
// Prepare eager loaded relationships
|
||||
$this->load(
|
||||
'languages',
|
||||
'locations.district.translations',
|
||||
'locations.city.translations',
|
||||
'locations.division.translations',
|
||||
'locations.country.translations',
|
||||
'tags.contexts.tags',
|
||||
'tags.contexts.tags.locale',
|
||||
'tags.contexts.category.ancestorsAndSelf',
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
|
||||
//TODO: Update to multilang database structure in future
|
||||
'about_nl' => $this->about,
|
||||
'about_en' => $this->about,
|
||||
'about_fr' => $this->about,
|
||||
'about_de' => $this->about,
|
||||
'about_es' => $this->about,
|
||||
'about_short_nl' => $this->about_short,
|
||||
'about_short_en' => $this->about_short,
|
||||
'about_short_fr' => $this->about_short,
|
||||
'about_short_de' => $this->about_short,
|
||||
'about_short_es' => $this->about_short,
|
||||
'motivation_nl' => $this->motivation,
|
||||
'motivation_en' => $this->motivation,
|
||||
'motivation_fr' => $this->motivation,
|
||||
'motivation_de' => $this->motivation,
|
||||
'motivation_es' => $this->motivation,
|
||||
'website' => $this->website,
|
||||
|
||||
'last_login_at' => $this->last_login_at,
|
||||
'lang_preference' => $this->lang_preference,
|
||||
|
||||
'languages' => $this->languages->map(function ($language) { // map() as languages is a collection
|
||||
return [
|
||||
'id' => $language->id,
|
||||
'name' => $language->name,
|
||||
'lang_code' => $language->lang_code,
|
||||
];
|
||||
}),
|
||||
|
||||
'locations' => $this->locations->map(function ($location) { // map() as locations is a collection
|
||||
return [
|
||||
'id' => $location->id,
|
||||
'district' => $location->district ? $location->district->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'city' => $location->city ? $location->city->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'division' => $location->division ? $location->division->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'country' => $location->country ? $location->country->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
];
|
||||
}),
|
||||
|
||||
'tags' => $this->tags->map(function ($tag) {
|
||||
return [
|
||||
'contexts' => $tag->contexts
|
||||
->map(function ($context) {
|
||||
return [
|
||||
'tags' => $context->tags->map(function ($tag) {
|
||||
// Include the locale in the field name for tags
|
||||
return [
|
||||
'name_' . $tag->locale->locale => StringHelper::DutchTitleCase($tag->normalized),
|
||||
];
|
||||
}),
|
||||
'categories' => Category::with(['translations' => function ($query) { $query->select('category_id', 'locale', 'name');}])
|
||||
->find([ $context->category->ancestorsAndSelf()->get()->flatMap(function ($related) {
|
||||
$categoryPath = explode('.', $related->path);
|
||||
return $categoryPath;
|
||||
})
|
||||
->unique()->values()->toArray()
|
||||
])->map(function ($category) {
|
||||
// Include the locale in the field name for categories
|
||||
return $category->translations->mapWithKeys(function ($translation) {
|
||||
return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
|
||||
});
|
||||
}),
|
||||
];
|
||||
}),
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bank's related admin's.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function admins()
|
||||
{
|
||||
return $this->belongsToMany(Admin::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the bank's accounts.
|
||||
* One-to-many polymorphic.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return $this->morphMany(Account::class, 'accountable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the bank's manager(s) that can manage bank profiles.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function managers()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'bank_user', 'bank_id', 'user_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve all related bank clients.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function clients()
|
||||
{
|
||||
return $this->userClients->merge($this->organizationClients);
|
||||
}
|
||||
|
||||
|
||||
public function userClients()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'client', 'bank_clients')
|
||||
->withPivot('relationship_type');
|
||||
}
|
||||
|
||||
|
||||
public function organizationClients()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'client', 'bank_clients')
|
||||
->withPivot('relationship_type');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related the locations of the bank.
|
||||
* One-to-many polymorph.
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->morphMany(Location::class, 'locatable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the languages for the organization.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function languages()
|
||||
{
|
||||
return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the social for the organization.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function socials()
|
||||
{
|
||||
return $this->morphToMany(Social::class, 'sociable')->withPivot('id', 'user_on_social', 'server_of_social');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the bank's message settings.
|
||||
*/
|
||||
public function message_settings()
|
||||
{
|
||||
return $this->morphMany(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
\Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
// Construct the full reset URL using the named route
|
||||
// The route 'non-user.password.reset' expects 'profileType' and 'token' parameters.
|
||||
$resetUrl = route('non-user.password.reset', [
|
||||
'profileType' => 'bank', // Hardcode 'admin' for the Admin model
|
||||
'token' => $token,
|
||||
'email' => $this->getEmailForPasswordReset(), // Method from CanResetPassword trait
|
||||
]);
|
||||
|
||||
// Pass the fully constructed URL to your notification
|
||||
$this->notify(new NonUserPasswordResetNotification($resetUrl, $this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the activity log options for the User model.
|
||||
*
|
||||
* This method sets up the logging to:
|
||||
* - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
|
||||
* - Log only when these attributes have been modified (dirty).
|
||||
* - Prevent submission of empty logs.
|
||||
* - Use 'user' as the log name.
|
||||
*
|
||||
* @return \Spatie\Activitylog\LogOptions
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly([
|
||||
'name',
|
||||
'last_login_ip',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->logOnlyDirty() // Only log attributes that have been changed
|
||||
->dontSubmitEmptyLogs()
|
||||
->useLogName('Bank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the URL for the user's cover image (avatar).
|
||||
* Adjust the 'avatar_url' field to your database setup.
|
||||
*/
|
||||
public function getCoverUrlAttribute(): ?string
|
||||
{
|
||||
return Storage::url($this->profile_photo_path) ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat:Returns the URL for the user's profile page.
|
||||
* Adjust the 'profile' route as needed for your setup.
|
||||
*/
|
||||
public function getProfileUrlAttribute(): ?string
|
||||
{
|
||||
return route('profile.show_by_type_and_id', ['type' => __('bank'), 'id' => $this->id]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the display name for the user.
|
||||
* Modify this to use your preferred name field.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): ?string
|
||||
{
|
||||
if ($this->full_name !== $this->name) {
|
||||
return $this->full_name . ' (' . $this->name . ')';
|
||||
} elseif ($this->name) {
|
||||
return $this->name;
|
||||
} else {
|
||||
return __('Bank');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Search for users when creating a new chat or adding members to a group.
|
||||
* Customize the search logic to limit results, such as restricting to friends or eligible users only.
|
||||
*/
|
||||
public function searchChatables(string $query): ?\Illuminate\Support\Collection
|
||||
{
|
||||
$searchableFields = ['name', 'full_name'];
|
||||
|
||||
// Search across all profile types
|
||||
$users = \App\Models\User::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$organizations = \App\Models\Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$banks = Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$admins = \App\Models\Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
// Combine all results into a base Collection to avoid serialization issues
|
||||
$results = collect()
|
||||
->merge($users->all())
|
||||
->merge($organizations->all())
|
||||
->merge($banks->all())
|
||||
->merge($admins->all());
|
||||
|
||||
// Filter out profiles based on configuration
|
||||
return $results->filter(function ($profile) {
|
||||
// Check inactive profiles
|
||||
if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check email unverified profiles
|
||||
if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check incomplete profiles
|
||||
if (
|
||||
timebank_config('profile_incomplete.messenger_hidden')
|
||||
&& method_exists($profile, 'hasIncompleteProfile')
|
||||
&& $profile->hasIncompleteProfile($profile)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->take(6)->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Determine if the user can create new groups.
|
||||
*/
|
||||
public function canCreateGroups(): bool
|
||||
{
|
||||
return timebank_config('profiles.bank.messenger_can_create_groups');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the Bank has any transactions with another model (User, Organization, Bank).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTransactionsWith($model): bool
|
||||
{
|
||||
$modelClass = get_class($model);
|
||||
$modelId = $model->id;
|
||||
|
||||
// Check if this Bank is involved in any transaction where the other model is either the sender or receiver
|
||||
return Transaction::where(function ($query) use ($modelClass, $modelId) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
});
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) {
|
||||
$q->where('accountable_type', Bank::class)
|
||||
->where('accountable_id', $this->id);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) {
|
||||
$q->where('accountable_type', Bank::class)
|
||||
->where('accountable_id', $this->id);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
121
app/Models/Call.php
Normal file
121
app/Models/Call.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Locations\Location;
|
||||
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
|
||||
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Call extends Model implements ReactableInterface
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
use Searchable;
|
||||
use Reactable;
|
||||
|
||||
protected $fillable = [
|
||||
'callable_id',
|
||||
'callable_type',
|
||||
'tag_id',
|
||||
'location_id',
|
||||
'from',
|
||||
'till',
|
||||
'is_public',
|
||||
'is_suppressed',
|
||||
'is_paused',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'from' => 'datetime',
|
||||
'till' => 'datetime',
|
||||
'is_public' => 'boolean',
|
||||
'is_suppressed' => 'boolean',
|
||||
'is_paused' => 'boolean',
|
||||
];
|
||||
|
||||
public function callable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(CallTranslation::class);
|
||||
}
|
||||
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class, 'tag_id', 'tag_id');
|
||||
}
|
||||
|
||||
public function location()
|
||||
{
|
||||
return $this->belongsTo(Location::class);
|
||||
}
|
||||
|
||||
public function searchableAs(): string
|
||||
{
|
||||
return 'calls_index';
|
||||
}
|
||||
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
// Only index active (non-deleted, non-expired, non-blocked) calls
|
||||
if ($this->trashed()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->till !== null && $this->till->isPast()) {
|
||||
return false;
|
||||
}
|
||||
if ($this->is_suppressed) {
|
||||
return false;
|
||||
}
|
||||
if ($this->is_paused) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
$tag = $this->tag;
|
||||
$tagNames = [];
|
||||
if ($tag) {
|
||||
foreach (['en', 'nl', 'fr', 'de', 'es'] as $locale) {
|
||||
$trans = $tag->translations()->firstWhere('locale', $locale);
|
||||
$tagNames['name_' . $locale] = $trans ? $trans->name : $tag->name;
|
||||
}
|
||||
}
|
||||
|
||||
$location = $this->location;
|
||||
$locationData = [
|
||||
'city' => $location?->city?->translations->map(fn($t) => $t->name)->toArray() ?? [],
|
||||
'division' => $location?->division?->translations->map(fn($t) => $t->name)->toArray() ?? [],
|
||||
'country' => $location?->country?->translations->map(fn($t) => $t->name)->toArray() ?? [],
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'__class_name' => self::class,
|
||||
'callable' => [
|
||||
'id' => $this->callable?->id,
|
||||
'name' => $this->callable?->name,
|
||||
],
|
||||
'tag' => array_merge(
|
||||
['tag_id' => $tag?->tag_id, 'color' => $tag?->contexts->first()?->category?->relatedColor ?? 'gray'],
|
||||
$tagNames
|
||||
),
|
||||
'location' => $locationData,
|
||||
'from' => $this->from?->utc()->format('Y-m-d H:i:s'),
|
||||
'till' => $this->till?->utc()->format('Y-m-d H:i:s'),
|
||||
'is_public' => (bool) $this->is_public,
|
||||
'call_translations' => $this->translations->mapWithKeys(fn($t) => [
|
||||
'content_' . $t->locale => $t->content,
|
||||
])->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Models/CallTranslation.php
Normal file
24
app/Models/CallTranslation.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CallTranslation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'call_id',
|
||||
'locale',
|
||||
'content',
|
||||
];
|
||||
|
||||
public function call()
|
||||
{
|
||||
return $this->belongsTo(Call::class);
|
||||
}
|
||||
}
|
||||
138
app/Models/Category.php
Normal file
138
app/Models/Category.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\CategoryTranslation;
|
||||
use App\Models\TaggableContext;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;
|
||||
|
||||
class Category extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasRecursiveRelationships;
|
||||
|
||||
protected $appends = ['translation', 'relatedPathTranslation', 'relatedPathExSelfTranslation', 'relatedColor'];
|
||||
protected $fillable = ['type', 'country_id', 'division_id', 'city_id', 'district_id', 'parent_id', 'color', 'categoryable_id', 'categoryable_type'];
|
||||
|
||||
|
||||
/**
|
||||
* Get the related posts for this category
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related translations of this category
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(CategoryTranslation::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related posts for this category
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(TaggableContext::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the translation attribute for the category.
|
||||
*
|
||||
* This method attempts to retrieve the translation for the category
|
||||
* in the current locale. If a translation in the current locale is
|
||||
* not found, it falls back to the base locale defined in the timebank-cc config.
|
||||
*
|
||||
* @return \App\Models\Translation|null The translation object for the category in the current or base locale, or null if not found.
|
||||
*/
|
||||
public function getTranslationAttribute()
|
||||
{
|
||||
$locale = App::getLocale();
|
||||
$baseLocale = timebank_config('base_language');
|
||||
|
||||
// Attempt to get the translation in the current locale
|
||||
$translation = $this->translations->firstWhere('locale', $locale);
|
||||
|
||||
// Fallback to base locale if translation not found
|
||||
if (!$translation) {
|
||||
$translation = $this->translations->firstWhere('locale', $baseLocale);
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
|
||||
public function related()
|
||||
{
|
||||
return $this->ancestorsAndSelf()->get();
|
||||
}
|
||||
|
||||
|
||||
public function getRelatedPathTranslationAttribute()
|
||||
{
|
||||
$categoryPathIds = $this->ancestors->pluck('id');
|
||||
$categoryPath = implode(
|
||||
' > ',
|
||||
CategoryTranslation::whereIn('category_id', $categoryPathIds)
|
||||
->where('locale', App::getLocale())
|
||||
->pluck('name')
|
||||
->toArray()
|
||||
);
|
||||
|
||||
return $categoryPath;
|
||||
}
|
||||
|
||||
|
||||
public function getRelatedPathExSelfTranslationAttribute()
|
||||
{
|
||||
$categoryPathIds = $this->ancestors->pluck('id');
|
||||
$categoryPath = implode(
|
||||
' > ',
|
||||
CategoryTranslation::whereIn('category_id', $categoryPathIds)
|
||||
->where('locale', App::getLocale())
|
||||
->pluck('name')
|
||||
->toArray()
|
||||
);
|
||||
|
||||
return $categoryPath;
|
||||
}
|
||||
|
||||
|
||||
public function getRelatedColorAttribute()
|
||||
{
|
||||
$categoryColor = $this->rootAncestor ? $this->rootAncestor->color : $this->color;
|
||||
|
||||
return $categoryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the polymorph relation of this type of category (i.e. division, user, organization).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function categoryable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
|
||||
public function tagContexts()
|
||||
{
|
||||
return $this->hasMany(TaggableContext::class);
|
||||
}
|
||||
|
||||
}
|
||||
25
app/Models/CategoryTranslation.php
Normal file
25
app/Models/CategoryTranslation.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
||||
class CategoryTranslation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['category_id', 'locale', 'slug', 'name'];
|
||||
|
||||
|
||||
/**
|
||||
* Get related category for this translation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
26
app/Models/Image.php
Normal file
26
app/Models/Image.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Post;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
|
||||
class Image extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['path','position','caption'];
|
||||
|
||||
|
||||
/**
|
||||
* Get the related posts of the images
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function posts(): MorphToMany
|
||||
{
|
||||
return $this->morphedByMany(Post:: class, 'imageable');
|
||||
}
|
||||
}
|
||||
44
app/Models/Language.php
Normal file
44
app/Models/Language.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Language extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'lang_code',
|
||||
'flag'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all users that are assigned to this language.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'languagable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all organizations that are assigned to this language.
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'languagable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all posts that are assigned to this language.
|
||||
*/
|
||||
public function posts()
|
||||
{
|
||||
return $this->morphedByMany(Post::class, 'languagable');
|
||||
}
|
||||
|
||||
}
|
||||
208
app/Models/Locations/City.php
Normal file
208
app/Models/Locations/City.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\CityLocale;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\DistrictLocale;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
|
||||
class City extends Model
|
||||
{
|
||||
use HasRelationships;
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
/**
|
||||
* Return all available locales of the city.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(CityLocale::class, 'city_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the users of the cities.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'districtable', 'districtables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organizations of the cities.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'cityable', 'cityables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the banks of the cities.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function banks()
|
||||
{
|
||||
return $this->morphedByMany(Bank::class, 'cityable', 'cityables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the cityables of the cities.
|
||||
* Many-to-many polymorph
|
||||
* @return void
|
||||
*/
|
||||
public function cityables()
|
||||
{
|
||||
return $this->morphedByMany(City::class, 'cityable', 'cityables');
|
||||
// cityable refers to pivot columns and cityables refers to pivot table
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the related districts of the city.
|
||||
* @return void
|
||||
*/
|
||||
public function districtsRelation()
|
||||
{
|
||||
return $this->hasManyThrough(DistrictLocale::class, District::class, 'city_id', 'district_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related locations of the division.
|
||||
* One-to-many.
|
||||
* @return void
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(Location::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the districts of the city in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function districts()
|
||||
{
|
||||
// $locale = collect(
|
||||
// $this->hasManyThrough(DistrictLocale::class, District::class, 'city_id', 'district_id')
|
||||
// ->where('locale', App::getLocale())
|
||||
// ->get()
|
||||
// )->keyBy('district_id');
|
||||
|
||||
// $fallback = collect(
|
||||
// $this->hasManyThrough(DistrictLocale::class, District::class, 'city_id', 'district_id')
|
||||
// ->where('locale', App::getFallbackLocale())
|
||||
// ->get()
|
||||
// )->keyBy('district_id');
|
||||
|
||||
// $result = $locale
|
||||
// ->union($fallback)
|
||||
// ->filter(function ($item) use ($search) {
|
||||
// return false !== stripos($item->name, $search);
|
||||
// })
|
||||
// ->sortBy('name');
|
||||
|
||||
return $this->hasMany(District::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the country of the city in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function country()
|
||||
{
|
||||
// $country = $this->belongsTo(Country::class, 'country_id')->pluck('id');
|
||||
// $result = CountryLocale::where('country_id', $country)
|
||||
// ->where('locale', App::getLocale());
|
||||
// if ($result->count() === 0) {
|
||||
// $result = CountryLocale::where('country_id', $country)
|
||||
// ->where('locale', App::getFallbackLocale());
|
||||
// }
|
||||
// return $result;
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the division of the city in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function division()
|
||||
{
|
||||
// $division = $this->belongsTo(Division::class, 'division_id')->pluck('id');
|
||||
// $result = DivisionLocale::where('division_id', $division)
|
||||
// ->where('locale', App::getLocale());
|
||||
// if ($result->count() === 0) {
|
||||
// $result = DivisionLocale::where('division_id', $division)
|
||||
// ->where('locale', App::getFallbackLocale());
|
||||
// }
|
||||
// return $result;
|
||||
return $this->belongsTo(Division::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related children of this model.
|
||||
*
|
||||
* @return collection
|
||||
*/
|
||||
public function children($search = '')
|
||||
{
|
||||
return $this->districts($search);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related parent of this model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
if ($this->division() === null) {
|
||||
return $this->country();
|
||||
}
|
||||
return $this->division();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the related categories for this model.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
}
|
||||
37
app/Models/Locations/CityLocale.php
Normal file
37
app/Models/Locations/CityLocale.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Scopes\LocalizeScope;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class CityLocale extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
// Scope a query to only include country locales that match the current application locale or fallback locale.
|
||||
static::addGlobalScope(new LocalizeScope);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* return belonged City
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function city()
|
||||
{
|
||||
return $this->belongsTo(City::class, 'city_id');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
214
app/Models/Locations/Country.php
Normal file
214
app/Models/Locations/Country.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
use HasRelationships;
|
||||
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
/**
|
||||
* Get all related locations of the division.
|
||||
* One-to-many.
|
||||
* @return void
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(Location::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return all related locales.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(CountryLocale::class, 'country_id');
|
||||
}
|
||||
|
||||
|
||||
public function translationExists()
|
||||
{
|
||||
return $this->hasOne(CountryLocale::class, 'country_id')->where('locale', App::getLocale())->exists();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the users of the countries.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'countryable', 'countryables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organizations of the countries.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'countryable', 'countryables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the banks of the countries.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function banks()
|
||||
{
|
||||
return $this->morphedByMany(Bank::class, 'countryable', 'countryables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the related divisions of the country.
|
||||
* @return void
|
||||
*/
|
||||
public function divisionsRelation()
|
||||
{
|
||||
return $this->hasManyThrough(DivisionLocale::class, Division::class, 'country_id', 'division_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the divisions of the country.
|
||||
* One-to-many
|
||||
* @param string
|
||||
* @return void
|
||||
*/
|
||||
public function divisions()
|
||||
{
|
||||
return $this->hasMany(Division::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the related cities of the country.
|
||||
* @return void
|
||||
*/
|
||||
public function cities()
|
||||
{
|
||||
return $this->hasManyThrough(CityLocale::class, City::class, 'country_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related cities locale of the country in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* The optional property will filter the localized city names.
|
||||
* @return void
|
||||
*/
|
||||
public function citiesLocale(string $search = '')
|
||||
{
|
||||
$locale = collect(
|
||||
$this->hasManyThrough(CityLocale::class, City::class, 'country_id')
|
||||
->where('locale', App::getLocale())
|
||||
->get()
|
||||
)->keyBy('city_id');
|
||||
|
||||
$fallback = collect(
|
||||
$this->hasManyThrough(CityLocale::class, City::class, 'country_id')
|
||||
->where('locale', App::getFallbackLocale())
|
||||
->get()
|
||||
)->keyBy('city_id');
|
||||
|
||||
$result = $locale
|
||||
->union($fallback)
|
||||
->filter(function ($item) use ($search) {
|
||||
return false !== stripos($item->name, $search);
|
||||
})
|
||||
->sortBy('name');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get all the related districts of the country.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function districtsRelation()
|
||||
{
|
||||
return $this->hasManyDeep(DistrictLocale::class, [City::class, District::class], ['country_id', 'city_id', 'district_id'], ['id', 'id', 'id']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the districts of the countries.
|
||||
* Only fallback locale is used when App language is not a country language of the city to prevent double results.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function districts(string $search = '')
|
||||
{
|
||||
$locale = collect(
|
||||
$this->hasManyDeep(DistrictLocale::class, [City::class, District::class], ['country_id', 'city_id', 'district_id'], ['id', 'id', 'id'])
|
||||
->where('locale', App::getLocale())
|
||||
->get()
|
||||
)->keyBy('district_id');
|
||||
|
||||
$fallback = collect(
|
||||
$this->hasManyDeep(DistrictLocale::class, [City::class, District::class], ['country_id', 'city_id', 'district_id'], ['id', 'id', 'id'])
|
||||
->where('locale', App::getFallbackLocale())
|
||||
->get()
|
||||
)->keyBy('district_id');
|
||||
|
||||
$result = $locale
|
||||
->union($fallback)
|
||||
->filter(function ($item) use ($search) {
|
||||
return false !== stripos($item->name, $search);
|
||||
})
|
||||
->sortBy('name');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related children of this model.
|
||||
*
|
||||
* @return collection
|
||||
*/
|
||||
public function children(string $search = '')
|
||||
{
|
||||
if ($this->divisions() == true) {
|
||||
return $this->divisions($search);
|
||||
}
|
||||
return $this->cities($search);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the related categories for this model.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
}
|
||||
19
app/Models/Locations/CountryLanguage.php
Normal file
19
app/Models/Locations/CountryLanguage.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CountryLanguage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
}
|
||||
31
app/Models/Locations/CountryLocale.php
Normal file
31
app/Models/Locations/CountryLocale.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Scopes\LocalizeScope;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class CountryLocale extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
// Scope a query to only include country locales that match the current application locale or fallback locale.
|
||||
static::addGlobalScope(new LocalizeScope);
|
||||
}
|
||||
|
||||
|
||||
public function country()
|
||||
{
|
||||
return $this->belongsTo(Country::class, 'country_id');
|
||||
}
|
||||
|
||||
}
|
||||
175
app/Models/Locations/District.php
Normal file
175
app/Models/Locations/District.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\DistrictLocale;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class District extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
/**
|
||||
* Return all available locales of the district.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(DistrictLocale::class, 'district_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the local district name.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* @return void
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
$result = $this->hasMany(DistrictLocale::class, 'district_id')
|
||||
->where('locale', App::getLocale());
|
||||
if ($result->count() === 0) {
|
||||
$result = $this->hasMany(DistrictLocale::class, 'district_id')
|
||||
->where('locale', App::getFallbackLocale());
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the users of the districts.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'districtable', 'districtables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organizations of the districts.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'districtable', 'districtables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the banks of the districts.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function banks()
|
||||
{
|
||||
return $this->morphedByMany(Bank::class, 'districtable', 'districtables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related locations of the division.
|
||||
* One-to-many.
|
||||
* @return void
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(Location::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related city of the district.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* @return void
|
||||
*/
|
||||
public function city()
|
||||
{
|
||||
$city = $this->belongsTo(City::class, 'city_id')->pluck('id');
|
||||
$result = CityLocale::where('city_id', $city)
|
||||
->where('locale', App::getLocale());
|
||||
if ($result->count() === 0) {
|
||||
$result = CityLocale::where('city_id', $city)
|
||||
->where('locale', App::getFallbackLocale());
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the related country of the district.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* @return void
|
||||
*/
|
||||
public function country()
|
||||
{
|
||||
$country = $this->belongsTo(City::class, 'city_id')->pluck('country_id');
|
||||
$result = CountryLocale::where('country_id', $country)
|
||||
->where('locale', App::getLocale());
|
||||
if ($result->count() === 0) {
|
||||
$result = CountryLocale::where('country_id', $country)
|
||||
->where('locale', App::getFallbackLocale());
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related division of the district.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* @return void
|
||||
*/
|
||||
public function division()
|
||||
{
|
||||
$division = $this->belongsTo(City::class, 'city_id')->pluck('division_id');
|
||||
$result = DivisionLocale::where('division_id', $division)
|
||||
->where('locale', App::getLocale());
|
||||
if ($result->count() === 0) {
|
||||
$result = DivisionLocale::where('division_id', $division)
|
||||
->where('locale', App::getFallbackLocale());
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related parent of this model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
// Check if division exists
|
||||
if ($this->division_id) {
|
||||
return $this->belongsTo(Division::class, 'division_id');
|
||||
}
|
||||
|
||||
// Otherwise, return the country relationship
|
||||
return $this->belongsTo(Country::class, 'country_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the related categories for this model.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
}
|
||||
29
app/Models/Locations/DistrictLocale.php
Normal file
29
app/Models/Locations/DistrictLocale.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Scopes\LocalizeScope;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DistrictLocale extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
// Scope a query to only include country locales that match the current application locale or fallback locale.
|
||||
static::addGlobalScope(new LocalizeScope);
|
||||
}
|
||||
|
||||
|
||||
public function district()
|
||||
{
|
||||
return $this->belongsTo(District::class, 'district_id');
|
||||
}
|
||||
}
|
||||
203
app/Models/Locations/Division.php
Normal file
203
app/Models/Locations/Division.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Bank;
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\DivisionLocale;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class Division extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
/**
|
||||
* Return all available locales of the division.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(DivisionLocale::class, 'division_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the local division name.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* @return void
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
$result = $this->hasMany(DivisionLocale::class, 'division_id')
|
||||
->whereIn('locale', [App::getLocale()]);
|
||||
if ($result->count() === 0) {
|
||||
$result = $this->hasMany(DivisionLocale::class, 'division_id')
|
||||
->whereIn('locale', [App::getFallbackLocale()]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the users of the divisions.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'divisionable', 'divisionables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organizations of the divisions.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'divisionable', 'divisionables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the banks of the divisions.
|
||||
* Many-to-many polymorphic.
|
||||
* @return void
|
||||
*/
|
||||
public function banks()
|
||||
{
|
||||
return $this->morphedByMany(Bank::class, 'divisionable', 'divisionables');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related locations of the division.
|
||||
* One-to-many.
|
||||
* @return void
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->hasMany(Location::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related country of the division.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function countryRelation()
|
||||
{
|
||||
return $this->belongsTo(Country::class, 'country_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the country of the division in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function country()
|
||||
{
|
||||
// $country = $this->belongsTo(Country::class, 'country_id')->pluck('id');
|
||||
// $result = CountryLocale::where('country_id', $country)
|
||||
// ->where('locale', App::getLocale());
|
||||
// if ($result->count() === 0) {
|
||||
// $result = CountryLocale::where('country_id', $country)
|
||||
// ->where('locale', App::getFallbackLocale());
|
||||
// }
|
||||
// return $result;
|
||||
|
||||
return $this->belongsTo(Country::class, 'country_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all the related cities of the division.
|
||||
* @return void
|
||||
*/
|
||||
public function citiesRelation()
|
||||
{
|
||||
return $this->hasManyThrough(CityLocale::class, City::class, 'division_id', 'city_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the cities of the division in the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
* The optional paramameter will filter the localized city names.
|
||||
* @param string $search
|
||||
* @return void
|
||||
*/
|
||||
public function cities()
|
||||
{
|
||||
// {
|
||||
// $locale = collect(
|
||||
// $this->hasManyThrough(CityLocale::class, City::class, 'division_id', 'city_id')
|
||||
// ->where('locale', App::getLocale())
|
||||
// ->get()
|
||||
// )->keyBy('city_id');
|
||||
|
||||
// $fallback = collect(
|
||||
// $this->hasManyThrough(CityLocale::class, City::class, 'division_id', 'city_id')
|
||||
// ->where('locale', App::getFallbackLocale())
|
||||
// ->get()
|
||||
// )->keyBy('city_id');
|
||||
|
||||
// $result = $locale
|
||||
// ->union($fallback)
|
||||
// ->filter(function ($item) use ($search){
|
||||
// return false !== stripos($item->name, $search);
|
||||
// })
|
||||
// ->sortBy('name');
|
||||
|
||||
// return $result;
|
||||
|
||||
return $this->hasMany(City::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related children of this model.
|
||||
*
|
||||
* @param string $search
|
||||
* @return void
|
||||
*/
|
||||
public function children(string $search = '')
|
||||
{
|
||||
return $this->cities($search);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related parent of this model.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->country();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the related categories for this model.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
}
|
||||
28
app/Models/Locations/DivisionLocale.php
Normal file
28
app/Models/Locations/DivisionLocale.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Scopes\LocalizeScope;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DivisionLocale extends Model
|
||||
{
|
||||
/**
|
||||
* The database table doesn't use 'created_at' and 'updated_at' so we disable it from Inserts/Updates.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
// Scope a query to only include country locales that match the current application locale or fallback locale.
|
||||
static::addGlobalScope(new LocalizeScope);
|
||||
}
|
||||
|
||||
public function division()
|
||||
{
|
||||
return $this->belongsTo(Division::class, 'division_id');
|
||||
}
|
||||
}
|
||||
305
app/Models/Locations/Location.php
Normal file
305
app/Models/Locations/Location.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Locations;
|
||||
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\District;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Meeting;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Location extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'country_id', 'division_id', 'city_id', 'district_id', 'locatable_id', 'locatable_type'];
|
||||
|
||||
/**
|
||||
* Return related country.
|
||||
* one-to-many
|
||||
* @return void
|
||||
*/
|
||||
public function country()
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most appropriate country for this location.
|
||||
* Tries multiple sources in order of preference.
|
||||
* If resolved it syncs the location's missing country_id.
|
||||
*
|
||||
* @return Country|null
|
||||
*/
|
||||
public function getCountry()
|
||||
{
|
||||
// 1. Direct country relationship
|
||||
if ($this->country_id && $this->country) {
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
// 2. Country from division
|
||||
if ($this->division_id && $this->division && $this->division->country_id) {
|
||||
$this->syncCountryFromDivision();
|
||||
return $this->fresh()->country;
|
||||
}
|
||||
|
||||
// 3. Country from city
|
||||
if ($this->city_id && $this->city && $this->city->country_id) {
|
||||
$this->syncCountryFromCity();
|
||||
return $this->fresh()->country;
|
||||
}
|
||||
|
||||
// 4. Country from district's city (if district exists)
|
||||
if ($this->district_id && $this->district && $this->district->city_id) {
|
||||
// First ensure we have city synced
|
||||
if (!$this->city_id) {
|
||||
$this->syncCityFromDistrict();
|
||||
}
|
||||
|
||||
// Now try to get country from city
|
||||
if ($this->city && $this->city->country_id) {
|
||||
$this->syncCountryFromCity();
|
||||
return $this->fresh()->country;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the location's country_id based on the division's country.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function syncCountryFromDivision()
|
||||
{
|
||||
if ($this->division && $this->division->country_id) {
|
||||
$this->country_id = $this->division->country_id;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the location's country_id based on the city's country.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function syncCountryFromCity()
|
||||
{
|
||||
if ($this->city && $this->city->country_id) {
|
||||
$this->country_id = $this->city->country_id;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return all related divisions.
|
||||
* One-to-many
|
||||
* @return void
|
||||
*/
|
||||
public function division()
|
||||
{
|
||||
return $this->belongsTo(Division::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most appropriate division for this location.
|
||||
* Tries multiple sources in order of preference.
|
||||
* If resolved it syncs the location's missing division_id.
|
||||
*
|
||||
* @return Division|null
|
||||
*/
|
||||
public function getDivision()
|
||||
{
|
||||
// 1. Direct division relationship
|
||||
if ($this->division_id && $this->division) {
|
||||
return $this->division;
|
||||
}
|
||||
|
||||
// 2. Division from city
|
||||
if ($this->city_id && $this->city && $this->city->division_id) {
|
||||
$this->syncDivisionFromCity();
|
||||
return $this->fresh()->division; // Fresh instance to get updated division_id
|
||||
}
|
||||
|
||||
// 3. Division from district's city (if district exists)
|
||||
if ($this->district_id && $this->district && $this->district->city_id) {
|
||||
// First sync city from district if city_id is missing
|
||||
if (!$this->city_id) {
|
||||
$this->syncCityFromDistrict();
|
||||
}
|
||||
|
||||
// Now try to get division from city
|
||||
if ($this->city && $this->city->division_id) {
|
||||
$this->syncDivisionFromCity();
|
||||
return $this->fresh()->division;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the location's country_id based on the division's country.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function syncCountryFromDistrict()
|
||||
{
|
||||
if ($this->division && $this->division->country_id) {
|
||||
$this->country_id = $this->division->country_id;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the location's division_id based on the city's division.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function syncDivisionFromCity()
|
||||
{
|
||||
if ($this->city && $this->city->division_id) {
|
||||
$this->division_id = $this->city->division_id;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the location's division_id based on the city's division.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function syncCityFromDistrict()
|
||||
{
|
||||
if ($this->district && $this->district->city_id) {
|
||||
$this->city_id = $this->district->city_id;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return related city.
|
||||
* One-to-many
|
||||
* @return void
|
||||
*/
|
||||
public function city()
|
||||
{
|
||||
return $this->belongsTo(City::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return related district.
|
||||
* One-to-many
|
||||
* @return void
|
||||
*/
|
||||
public function district()
|
||||
{
|
||||
return $this->belongsTo(District::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sync all missing location hierarchy data.
|
||||
* This method will populate all missing IDs based on available relationships.
|
||||
*
|
||||
* @return array Returns what was synced
|
||||
*/
|
||||
public function syncAllLocationData()
|
||||
{
|
||||
$synced = [];
|
||||
|
||||
// Step 1: Sync city from district if missing
|
||||
if (!$this->city_id && $this->district_id) {
|
||||
if ($this->syncCityFromDistrict()) {
|
||||
$synced[] = 'city_from_district';
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Sync division from city if missing
|
||||
if (!$this->division_id && $this->city_id) {
|
||||
if ($this->syncDivisionFromCity()) {
|
||||
$synced[] = 'division_from_city';
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Sync country from division if missing
|
||||
if (!$this->country_id && $this->division_id) {
|
||||
if ($this->syncCountryFromDivision()) {
|
||||
$synced[] = 'country_from_division';
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Fallback - sync country from city if still missing
|
||||
if (!$this->country_id && $this->city_id) {
|
||||
if ($this->syncCountryFromCity()) {
|
||||
$synced[] = 'country_from_city';
|
||||
}
|
||||
}
|
||||
|
||||
return $synced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a complete location hierarchy.
|
||||
* This method ensures all data is synced and returns the complete hierarchy.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCompleteHierarchy()
|
||||
{
|
||||
// Sync all data first
|
||||
$this->syncAllLocationData();
|
||||
|
||||
// Refresh the model to get updated relationships
|
||||
$location = $this->fresh();
|
||||
|
||||
return [
|
||||
'country' => $location->country,
|
||||
'division' => $location->getDivision(),
|
||||
'city' => $location->city,
|
||||
'district' => $location->district,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return related locatable (i.e. user or organization).
|
||||
* One-to-many polymorph
|
||||
* @return void
|
||||
*/
|
||||
public function locatable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return related meeting.
|
||||
* one-to-many
|
||||
* @return void
|
||||
*/
|
||||
public function meeting()
|
||||
{
|
||||
return $this->hasOne(Meeting::class);
|
||||
}
|
||||
|
||||
}
|
||||
589
app/Models/Mailing.php
Normal file
589
app/Models/Mailing.php
Normal file
@@ -0,0 +1,589 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Bank;
|
||||
use App\Models\MessageSetting;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Mailing extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title', 'type', 'subject', 'content_blocks', 'scheduled_at', 'sent_at',
|
||||
'status', 'recipients_count', 'sent_count', 'failed_count',
|
||||
'updated_by_user_id', 'filter_by_location', 'location_country_id',
|
||||
'location_division_id', 'location_city_id', 'location_district_id',
|
||||
'filter_by_profile_type', 'selected_profile_types'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'content_blocks' => 'array',
|
||||
'subject' => 'array',
|
||||
'scheduled_at' => 'datetime',
|
||||
'sent_at' => 'datetime',
|
||||
'filter_by_location' => 'boolean',
|
||||
'filter_by_profile_type' => 'boolean',
|
||||
'selected_profile_types' => 'array',
|
||||
];
|
||||
|
||||
protected $dates = ['scheduled_at', 'sent_at', 'deleted_at'];
|
||||
|
||||
/**
|
||||
* Get the user who last updated the mailing
|
||||
*/
|
||||
public function updatedByUser()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class, 'updated_by_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounces for this mailing
|
||||
*/
|
||||
public function bounces()
|
||||
{
|
||||
return $this->hasMany(\App\Models\MailingBounce::class, 'mailing_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounced count dynamically from the mailing_bounces table
|
||||
*/
|
||||
public function getBouncedCountAttribute()
|
||||
{
|
||||
return $this->bounces()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get posts from content_blocks JSON
|
||||
*/
|
||||
public function getSelectedPosts()
|
||||
{
|
||||
if (empty($this->content_blocks)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$postIds = collect($this->content_blocks)->pluck('post_id');
|
||||
|
||||
return Post::whereIn('id', $postIds)->get()->sortBy(function ($post) {
|
||||
return array_search($post->id, collect($this->content_blocks)->pluck('post_id')->toArray());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get posts with all their translations
|
||||
*/
|
||||
public function getSelectedPostsWithTranslations()
|
||||
{
|
||||
return $this->getSelectedPosts()->load('translations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post translation for specific locale with fallback
|
||||
*/
|
||||
public function getPostTranslationForLocale($postId, $locale = null)
|
||||
{
|
||||
$locale = $locale ?: App::getLocale();
|
||||
|
||||
$post = Post::find($postId);
|
||||
if (!$post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try preferred locale first
|
||||
$translation = $post->translations()->where('locale', $locale)->first();
|
||||
|
||||
// Only fallback if enabled in configuration
|
||||
if (!$translation && timebank_config('bulk_mail.use_fallback_locale', true)) {
|
||||
$fallbackLocale = timebank_config('bulk_mail.fallback_locale', 'en');
|
||||
if ($locale !== $fallbackLocale) { // Avoid infinite loop
|
||||
$translation = $post->translations()->where('locale', $fallbackLocale)->first();
|
||||
}
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipients query based on mailing type
|
||||
*/
|
||||
public function getRecipientsQuery()
|
||||
{
|
||||
$queries = [];
|
||||
|
||||
// Initialize all profile type queries
|
||||
$users = User::whereHas('messageSettings', function ($query) {
|
||||
$query->where($this->type, true);
|
||||
})->select('id', 'name', 'email', 'lang_preference');
|
||||
|
||||
$organizations = Organization::whereHas('messageSettings', function ($query) {
|
||||
$query->where($this->type, true);
|
||||
})->select('id', 'name', 'email', 'lang_preference');
|
||||
|
||||
$banks = Bank::whereHas('message_settings', function ($query) {
|
||||
$query->where($this->type, true);
|
||||
})->select('id', 'name', 'email', 'lang_preference');
|
||||
|
||||
$admins = Admin::whereHas('message_settings', function ($query) {
|
||||
$query->where($this->type, true);
|
||||
})->select('id', 'name', 'email', 'lang_preference');
|
||||
|
||||
// Apply profile type filtering if enabled
|
||||
if ($this->filter_by_profile_type && !empty($this->selected_profile_types)) {
|
||||
$selectedTypes = $this->selected_profile_types;
|
||||
|
||||
// Only include selected profile types
|
||||
if (in_array('User', $selectedTypes)) {
|
||||
$queries[] = $users;
|
||||
}
|
||||
if (in_array('Organization', $selectedTypes)) {
|
||||
$queries[] = $organizations;
|
||||
}
|
||||
if (in_array('Bank', $selectedTypes)) {
|
||||
$queries[] = $banks;
|
||||
}
|
||||
if (in_array('Admin', $selectedTypes)) {
|
||||
$queries[] = $admins;
|
||||
}
|
||||
} else {
|
||||
// If no profile type filtering, include all types
|
||||
$queries = [$users, $organizations, $banks, $admins];
|
||||
}
|
||||
|
||||
// Apply location filtering if enabled
|
||||
if ($this->filter_by_location) {
|
||||
$queries = array_map(function($query) {
|
||||
return $this->applyLocationFilter($query);
|
||||
}, $queries);
|
||||
}
|
||||
|
||||
// Get count before bounce filtering for logging
|
||||
$beforeFilteringCount = 0;
|
||||
foreach ($queries as $query) {
|
||||
$beforeFilteringCount += $query->count();
|
||||
}
|
||||
|
||||
// Apply bounce filtering to all queries
|
||||
$queries = array_map(function($query) {
|
||||
return $query->whereNotIn('email', function($subquery) {
|
||||
$subquery->select('email')
|
||||
->from('mailing_bounces')
|
||||
->where('is_suppressed', true);
|
||||
});
|
||||
}, $queries);
|
||||
|
||||
// Get count after bounce filtering for logging
|
||||
$afterFilteringCount = 0;
|
||||
foreach ($queries as $query) {
|
||||
$afterFilteringCount += $query->count();
|
||||
}
|
||||
|
||||
// Log suppression statistics if any emails were excluded
|
||||
$suppressedCount = $beforeFilteringCount - $afterFilteringCount;
|
||||
if ($suppressedCount > 0) {
|
||||
Log::warning("MAILING RECIPIENTS: Excluded {$suppressedCount} suppressed emails from Mailing ID: {$this->id}");
|
||||
|
||||
// Log which specific emails were suppressed
|
||||
$suppressedEmails = MailingBounce::where('is_suppressed', true)->pluck('email');
|
||||
foreach ($suppressedEmails as $email) {
|
||||
Log::warning("MAILING RECIPIENTS: Suppressed email excluded: {$email}");
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all queries with union
|
||||
if (empty($queries)) {
|
||||
// Return empty query if no profile types selected
|
||||
return User::whereRaw('1 = 0')->select('id', 'name', 'email', 'lang_preference');
|
||||
}
|
||||
|
||||
$finalQuery = array_shift($queries);
|
||||
foreach ($queries as $query) {
|
||||
$finalQuery = $finalQuery->union($query);
|
||||
}
|
||||
|
||||
return $finalQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply location filtering to a query
|
||||
*/
|
||||
private function applyLocationFilter($query)
|
||||
{
|
||||
return $query->whereHas('locations', function ($locationQuery) {
|
||||
if ($this->location_district_id) {
|
||||
$locationQuery->where('district_id', $this->location_district_id);
|
||||
} elseif ($this->location_city_id) {
|
||||
$locationQuery->where('city_id', $this->location_city_id);
|
||||
} elseif ($this->location_division_id) {
|
||||
$locationQuery->where('division_id', $this->location_division_id);
|
||||
} elseif ($this->location_country_id) {
|
||||
$locationQuery->where('country_id', $this->location_country_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipients grouped by language preference for efficient processing
|
||||
*/
|
||||
public function getRecipientsByLanguage()
|
||||
{
|
||||
$recipients = $this->getRecipientsQuery()->get();
|
||||
|
||||
return $recipients->groupBy(function ($recipient) {
|
||||
return $recipient->lang_preference ?: timebank_config('base_language', 'en');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Status checking methods
|
||||
*/
|
||||
public function canBeSent()
|
||||
{
|
||||
$hasValidStatus = in_array($this->status, ['draft', 'scheduled']);
|
||||
$hasPosts = !empty($this->content_blocks) && count(array_filter($this->content_blocks, function($block) {
|
||||
return !empty($block['post_id']);
|
||||
})) > 0;
|
||||
|
||||
return $hasValidStatus && $hasPosts;
|
||||
}
|
||||
|
||||
public function canBeScheduled()
|
||||
{
|
||||
return in_array($this->status, ['draft']);
|
||||
}
|
||||
|
||||
public function canBeCancelled()
|
||||
{
|
||||
return in_array($this->status, ['scheduled', 'sending']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', 'draft');
|
||||
}
|
||||
|
||||
public function scopeScheduled($query)
|
||||
{
|
||||
return $query->where('status', 'scheduled');
|
||||
}
|
||||
|
||||
public function scopeSent($query)
|
||||
{
|
||||
return $query->where('status', 'sent');
|
||||
}
|
||||
|
||||
public function scopeByType($query, $type)
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available locales for recipients of this mailing type
|
||||
*/
|
||||
public function getAvailableRecipientLocales()
|
||||
{
|
||||
$recipientsQuery = $this->getRecipientsQuery();
|
||||
|
||||
return $recipientsQuery
|
||||
->whereNotNull('lang_preference')
|
||||
->distinct()
|
||||
->pluck('lang_preference')
|
||||
->filter()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available locales from selected posts
|
||||
*/
|
||||
public function getAvailablePostLocales()
|
||||
{
|
||||
if (empty($this->content_blocks)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$postIds = collect($this->content_blocks)->pluck('post_id');
|
||||
$locales = \DB::table('post_translations')
|
||||
->whereIn('post_id', $postIds)
|
||||
->where('status', 1) // Only published translations
|
||||
->distinct()
|
||||
->pluck('locale')
|
||||
->toArray();
|
||||
|
||||
// Always include base language first
|
||||
$baseLanguage = timebank_config('base_language', 'en');
|
||||
$locales = array_unique(array_merge([$baseLanguage], $locales));
|
||||
|
||||
return array_values($locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subject for a specific locale
|
||||
*/
|
||||
public function getSubjectForLocale($locale = null)
|
||||
{
|
||||
$locale = $locale ?: timebank_config('base_language', 'en');
|
||||
$baseLanguage = timebank_config('base_language', 'en');
|
||||
|
||||
// Handle legacy string subjects
|
||||
if (is_string($this->subject)) {
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
// Handle multilingual JSON subjects
|
||||
$subjects = $this->subject ?? [];
|
||||
|
||||
// Try preferred locale first
|
||||
if (isset($subjects[$locale])) {
|
||||
return $subjects[$locale];
|
||||
}
|
||||
|
||||
// Fallback to base language
|
||||
if (isset($subjects[$baseLanguage])) {
|
||||
return $subjects[$baseLanguage];
|
||||
}
|
||||
|
||||
// Fallback to first available subject
|
||||
return !empty($subjects) ? array_values($subjects)[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set subject for a specific locale
|
||||
*/
|
||||
public function setSubjectForLocale($locale, $subject)
|
||||
{
|
||||
$subjects = $this->subject ?? [];
|
||||
$subjects[$locale] = $subject;
|
||||
$this->subject = $subjects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subjects with their locales
|
||||
*/
|
||||
public function getAllSubjects()
|
||||
{
|
||||
return $this->subject ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subject exists for a locale
|
||||
*/
|
||||
public function hasSubjectForLocale($locale)
|
||||
{
|
||||
$subjects = $this->subject ?? [];
|
||||
return isset($subjects[$locale]) && !empty($subjects[$locale]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipients for a specific locale
|
||||
*/
|
||||
public function getRecipientsForLocale($locale)
|
||||
{
|
||||
return $this->getRecipientsQuery()
|
||||
->where('lang_preference', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipients grouped by available locales with fallback handling
|
||||
*/
|
||||
public function getRecipientsGroupedByAvailableLocales()
|
||||
{
|
||||
$availableLocales = $this->getAvailablePostLocales();
|
||||
$allRecipients = $this->getRecipientsQuery()->get();
|
||||
$recipientsByLocale = [];
|
||||
|
||||
foreach ($allRecipients as $recipient) {
|
||||
$preferredLocale = $recipient->lang_preference ?? timebank_config('base_language', 'en');
|
||||
|
||||
// Check if preferred locale has content
|
||||
if (in_array($preferredLocale, $availableLocales)) {
|
||||
$recipientsByLocale[$preferredLocale][] = $recipient;
|
||||
} else {
|
||||
// Handle fallback logic
|
||||
if (timebank_config('bulk_mail.use_fallback_locale', true)) {
|
||||
$fallbackLocale = timebank_config('bulk_mail.fallback_locale', 'en');
|
||||
if (in_array($fallbackLocale, $availableLocales)) {
|
||||
$recipientsByLocale[$fallbackLocale][] = $recipient;
|
||||
}
|
||||
}
|
||||
// If fallback is disabled or not available, recipient is skipped
|
||||
}
|
||||
}
|
||||
|
||||
return $recipientsByLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter content blocks to only include posts with published translations in the given locale
|
||||
*/
|
||||
public function getContentBlocksForLocale($locale)
|
||||
{
|
||||
if (empty($this->content_blocks)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$filteredBlocks = [];
|
||||
|
||||
foreach ($this->content_blocks as $block) {
|
||||
$post = Post::with('translations')->find($block['post_id']);
|
||||
|
||||
if (!$post) {
|
||||
continue; // Skip if post doesn't exist
|
||||
}
|
||||
|
||||
// Check if post has a published translation in the requested locale
|
||||
$hasPublishedTranslation = $post->translations->contains(function ($translation) use ($locale, $now) {
|
||||
return $translation->locale === $locale
|
||||
&& $translation->status == 1
|
||||
&& $translation->from <= $now
|
||||
&& ($translation->till === null || $translation->till >= $now);
|
||||
});
|
||||
|
||||
if ($hasPublishedTranslation) {
|
||||
$filteredBlocks[] = $block;
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recipient count grouped by locale with content availability
|
||||
*/
|
||||
public function getRecipientCountsByLocale()
|
||||
{
|
||||
// Get all recipients using the same logic as getRecipientsQuery()
|
||||
$recipients = $this->getRecipientsQuery()->get();
|
||||
|
||||
// Get base language
|
||||
$baseLanguage = timebank_config('base_language', 'en');
|
||||
|
||||
// Group recipients by language preference (with NULL fallback to base language)
|
||||
$recipientsByLocale = $recipients->groupBy(function ($recipient) use ($baseLanguage) {
|
||||
return $recipient->lang_preference ?: $baseLanguage;
|
||||
});
|
||||
|
||||
// Add content block information for each locale
|
||||
$result = [];
|
||||
foreach ($recipientsByLocale as $locale => $localeRecipients) {
|
||||
$contentBlocks = $this->getContentBlocksForLocale($locale);
|
||||
$result[$locale] = [
|
||||
'count' => $localeRecipients->count(),
|
||||
'content_blocks' => count($contentBlocks),
|
||||
'has_content' => !empty($contentBlocks)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted recipient counts display
|
||||
*/
|
||||
public function getFormattedRecipientCounts()
|
||||
{
|
||||
$counts = $this->getRecipientCountsByLocale();
|
||||
$formatted = [];
|
||||
|
||||
foreach ($counts as $locale => $data) {
|
||||
$language = \App\Models\Language::where('lang_code', $locale)->first();
|
||||
$flag = $language ? $language->flag : '🏳️';
|
||||
|
||||
if ($data['has_content']) {
|
||||
$formatted[] = "{$flag} {$data['count']}";
|
||||
} else {
|
||||
$formatted[] = "{$flag} {$data['count']} (no content)";
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $formatted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total recipients that will actually receive content
|
||||
*/
|
||||
public function getEffectiveRecipientsCount()
|
||||
{
|
||||
$counts = $this->getRecipientCountsByLocale();
|
||||
|
||||
return collect($counts)
|
||||
->where('has_content', true)
|
||||
->sum('count');
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch locale-specific email jobs for this mailing
|
||||
*/
|
||||
public function dispatchLocaleSpecificJobs()
|
||||
{
|
||||
Log::info("MAILING: Starting dispatch process for Mailing ID: {$this->id}");
|
||||
|
||||
// Get batch size from configuration
|
||||
$batchSize = timebank_config('mailing.batch_size', 10);
|
||||
|
||||
// Get recipients grouped by available locales with fallback logic
|
||||
$recipientsByLocale = $this->getRecipientsGroupedByAvailableLocales();
|
||||
|
||||
$totalJobsDispatched = 0;
|
||||
|
||||
foreach ($recipientsByLocale as $locale => $recipients) {
|
||||
$contentBlocks = $this->getContentBlocksForLocale($locale);
|
||||
|
||||
if (!empty($contentBlocks) && !empty($recipients)) {
|
||||
// Split recipients into batches
|
||||
$recipientBatches = collect($recipients)->chunk($batchSize);
|
||||
$batchCount = $recipientBatches->count();
|
||||
|
||||
Log::info("MAILING: Dispatching {$batchCount} batch job(s) for Mailing ID: {$this->id}, Locale: {$locale}, Total Recipients: " . count($recipients) . ", Batch Size: {$batchSize}");
|
||||
|
||||
// Dispatch a separate job for each batch
|
||||
foreach ($recipientBatches as $batchIndex => $batch) {
|
||||
$batchNumber = $batchIndex + 1;
|
||||
|
||||
Log::info("MAILING: Dispatching batch {$batchNumber}/{$batchCount} for Mailing ID: {$this->id}, Locale: {$locale}, Recipients in batch: " . $batch->count());
|
||||
|
||||
// Dispatch the actual job with batch of recipients to dedicated mailing queue
|
||||
\App\Jobs\SendBulkMailJob::dispatch($this, $locale, [], $batch)
|
||||
->onQueue('mailing');
|
||||
|
||||
$totalJobsDispatched++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate actual recipients who will receive emails
|
||||
$totalRecipients = $this->getRecipientsQuery()->count();
|
||||
$processedRecipients = collect($recipientsByLocale)->sum(function($recipients) {
|
||||
return count($recipients);
|
||||
});
|
||||
|
||||
// Update recipients_count to reflect actual sendable recipients
|
||||
$this->update(['recipients_count' => $processedRecipients]);
|
||||
|
||||
// Get suppressed email count for logging
|
||||
$suppressedCount = MailingBounce::where('is_suppressed', true)->count();
|
||||
|
||||
Log::info("MAILING: Completed dispatch for Mailing ID: {$this->id} - Jobs dispatched: {$totalJobsDispatched}, Locales: " . count($recipientsByLocale) . ", Recipients processed: {$processedRecipients}, Suppressed emails: {$suppressedCount}");
|
||||
|
||||
// If no jobs were dispatched (no recipients or no content), mark as sent immediately
|
||||
if (empty($recipientsByLocale) || $processedRecipients === 0) {
|
||||
Log::warning("MAILING: No jobs dispatched for Mailing ID: {$this->id} - marking as sent immediately");
|
||||
$this->update([
|
||||
'status' => 'sent',
|
||||
'sent_at' => now()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
254
app/Models/MailingBounce.php
Normal file
254
app/Models/MailingBounce.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Bank;
|
||||
use App\Models\Organization;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MailingBounce extends Model
|
||||
{
|
||||
protected $table = 'mailing_bounces';
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'bounce_type',
|
||||
'bounce_reason',
|
||||
'mailing_id',
|
||||
'bounced_at',
|
||||
'is_suppressed',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'bounced_at' => 'datetime',
|
||||
'is_suppressed' => 'boolean',
|
||||
];
|
||||
|
||||
public function mailing(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mailing::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an email address should be suppressed from sending
|
||||
*/
|
||||
public static function isSuppressed(string $email): bool
|
||||
{
|
||||
return self::where('email', $email)
|
||||
->where('is_suppressed', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress an email address from future mailings
|
||||
*/
|
||||
public static function suppressEmail(string $email, string $reason = null): void
|
||||
{
|
||||
self::updateOrCreate(
|
||||
['email' => $email],
|
||||
[
|
||||
'bounce_reason' => $reason ?? 'Email suppressed',
|
||||
'bounced_at' => now(),
|
||||
'is_suppressed' => true,
|
||||
'bounce_type' => 'suppressed',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a bounce for an email with threshold-based actions
|
||||
*/
|
||||
public static function recordBounce(
|
||||
string $email,
|
||||
string $bounceType = 'hard',
|
||||
string $reason = null,
|
||||
int $mailingId = null
|
||||
): self {
|
||||
// Create the bounce record
|
||||
$bounce = self::create([
|
||||
'email' => $email,
|
||||
'bounce_type' => $bounceType,
|
||||
'bounce_reason' => $reason,
|
||||
'mailing_id' => $mailingId,
|
||||
'bounced_at' => now(),
|
||||
'is_suppressed' => false, // Don't auto-suppress, use threshold-based logic
|
||||
]);
|
||||
|
||||
// Check if this bounce should trigger threshold-based actions
|
||||
if (self::isDefinitiveHardBounce($bounceType, $reason)) {
|
||||
self::checkAndApplyThresholds($email, $reason);
|
||||
}
|
||||
|
||||
return $bounce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check bounce counts and apply threshold-based actions
|
||||
*/
|
||||
protected static function checkAndApplyThresholds(string $email, string $reason): void
|
||||
{
|
||||
$config = timebank_config('mailing.bounce_thresholds', []);
|
||||
$suppressionThreshold = $config['suppression_threshold'] ?? 3;
|
||||
$verificationResetThreshold = $config['verification_reset_threshold'] ?? 2;
|
||||
$windowDays = $config['counting_window_days'] ?? 30;
|
||||
|
||||
// Count recent definitive hard bounces for this email
|
||||
$recentBounceCount = self::countRecentDefinitiveBounces($email, $windowDays);
|
||||
|
||||
Log::info("Email {$email} has {$recentBounceCount} recent hard bounces (suppression: {$suppressionThreshold}, verification reset: {$verificationResetThreshold})");
|
||||
|
||||
// Check verification reset threshold first (lower threshold)
|
||||
if ($recentBounceCount >= $verificationResetThreshold && !self::hasVerificationBeenReset($email)) {
|
||||
self::resetEmailVerificationForAddress($email, $reason);
|
||||
self::markVerificationReset($email);
|
||||
}
|
||||
|
||||
// Check suppression threshold
|
||||
if ($recentBounceCount >= $suppressionThreshold) {
|
||||
self::suppressEmailByThreshold($email, $reason, $recentBounceCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count recent definitive hard bounces for an email address
|
||||
*/
|
||||
protected static function countRecentDefinitiveBounces(string $email, int $windowDays): int
|
||||
{
|
||||
$config = timebank_config('mailing.bounce_thresholds', []);
|
||||
$countedTypes = $config['counted_bounce_types'] ?? ['hard'];
|
||||
$patterns = $config['definitive_hard_bounce_patterns'] ?? [];
|
||||
|
||||
$cutoffDate = now()->subDays($windowDays);
|
||||
|
||||
return self::where('email', $email)
|
||||
->whereIn('bounce_type', $countedTypes)
|
||||
->where('bounced_at', '>=', $cutoffDate)
|
||||
->where(function ($query) use ($patterns) {
|
||||
foreach ($patterns as $pattern) {
|
||||
$query->orWhere('bounce_reason', 'like', "%{$pattern}%");
|
||||
}
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bounce qualifies as a definitive hard bounce
|
||||
*/
|
||||
protected static function isDefinitiveHardBounce(string $bounceType, ?string $reason): bool
|
||||
{
|
||||
$config = timebank_config('mailing.bounce_thresholds', []);
|
||||
$countedTypes = $config['counted_bounce_types'] ?? ['hard'];
|
||||
$patterns = $config['definitive_hard_bounce_patterns'] ?? [];
|
||||
|
||||
if (!in_array($bounceType, $countedTypes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($reason) || empty($patterns)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$reasonLower = strtolower($reason);
|
||||
foreach ($patterns as $pattern) {
|
||||
if (strpos($reasonLower, strtolower($pattern)) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress email due to threshold being reached
|
||||
*/
|
||||
protected static function suppressEmailByThreshold(string $email, string $reason, int $bounceCount): void
|
||||
{
|
||||
// Update the latest bounce record to mark as suppressed
|
||||
self::where('email', $email)
|
||||
->where('is_suppressed', false)
|
||||
->update(['is_suppressed' => true]);
|
||||
|
||||
Log::warning("Email {$email} suppressed due to {$bounceCount} hard bounces. Latest reason: {$reason}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset email verification for all profile types using this email
|
||||
*/
|
||||
protected static function resetEmailVerificationForAddress(string $email, string $reason): void
|
||||
{
|
||||
$updatedProfiles = [];
|
||||
|
||||
// Reset verification for all profile types
|
||||
$userUpdated = User::where('email', $email)->whereNotNull('email_verified_at')->update(['email_verified_at' => null]);
|
||||
if ($userUpdated) $updatedProfiles[] = "Users: {$userUpdated}";
|
||||
|
||||
$orgUpdated = Organization::where('email', $email)->whereNotNull('email_verified_at')->update(['email_verified_at' => null]);
|
||||
if ($orgUpdated) $updatedProfiles[] = "Organizations: {$orgUpdated}";
|
||||
|
||||
$bankUpdated = Bank::where('email', $email)->whereNotNull('email_verified_at')->update(['email_verified_at' => null]);
|
||||
if ($bankUpdated) $updatedProfiles[] = "Banks: {$bankUpdated}";
|
||||
|
||||
$adminUpdated = Admin::where('email', $email)->whereNotNull('email_verified_at')->update(['email_verified_at' => null]);
|
||||
if ($adminUpdated) $updatedProfiles[] = "Admins: {$adminUpdated}";
|
||||
|
||||
if (!empty($updatedProfiles)) {
|
||||
Log::warning("Reset email verification for {$email} due to hard bounce threshold. Affected: " . implode(', ', $updatedProfiles) . ". Reason: {$reason}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that verification has been reset for this email (to prevent multiple resets)
|
||||
*/
|
||||
protected static function markVerificationReset(string $email): void
|
||||
{
|
||||
// We could add a verification_reset_at column to track this, but for now
|
||||
// we'll rely on the email_verified_at being null to prevent duplicate resets
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if verification has already been reset for this email
|
||||
*/
|
||||
protected static function hasVerificationBeenReset(string $email): bool
|
||||
{
|
||||
// Check if any profile with this email has null email_verified_at
|
||||
return User::where('email', $email)->whereNull('email_verified_at')->exists()
|
||||
|| Organization::where('email', $email)->whereNull('email_verified_at')->exists()
|
||||
|| Bank::where('email', $email)->whereNull('email_verified_at')->exists()
|
||||
|| Admin::where('email', $email)->whereNull('email_verified_at')->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method for testing pattern matching
|
||||
*/
|
||||
public static function testPatternMatching(string $bounceType, ?string $reason): bool
|
||||
{
|
||||
return self::isDefinitiveHardBounce($bounceType, $reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bounce statistics for an email address
|
||||
*/
|
||||
public static function getBounceStats(string $email): array
|
||||
{
|
||||
$windowDays = timebank_config('bulk_mail.bounce_thresholds.counting_window_days', 30);
|
||||
$cutoffDate = now()->subDays($windowDays);
|
||||
|
||||
$totalBounces = self::where('email', $email)->count();
|
||||
$recentBounces = self::where('email', $email)->where('bounced_at', '>=', $cutoffDate)->count();
|
||||
$recentHardBounces = self::countRecentDefinitiveBounces($email, $windowDays);
|
||||
$isSuppressed = self::isSuppressed($email);
|
||||
|
||||
return [
|
||||
'email' => $email,
|
||||
'total_bounces' => $totalBounces,
|
||||
'recent_bounces' => $recentBounces,
|
||||
'recent_hard_bounces' => $recentHardBounces,
|
||||
'is_suppressed' => $isSuppressed,
|
||||
'window_days' => $windowDays,
|
||||
];
|
||||
}
|
||||
}
|
||||
67
app/Models/Meeting.php
Normal file
67
app/Models/Meeting.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Post;
|
||||
use App\Traits\LocationTrait;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
||||
class Meeting extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use LocationTrait;
|
||||
|
||||
protected $fillable = ['post_id', 'venue', 'address', 'price', 'based_on_quantity', 'transaction_type_id', 'meetingable_id', 'meetingable_type', 'status', 'from', 'till'];
|
||||
|
||||
|
||||
/**
|
||||
* Get related post for this event
|
||||
* Ont-to-one relationship
|
||||
* @return void
|
||||
*/
|
||||
public function post()
|
||||
{
|
||||
return $this->belongsTo(Post::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the organizer of the meeting (i.e. user or organization).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function meetingable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the organizer of the meeting (i.e. user or organization).
|
||||
* Accessor and alias of meetingable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getOrganizerAttribute()
|
||||
{
|
||||
return $this->meetingable;
|
||||
}
|
||||
|
||||
|
||||
public function location()
|
||||
{
|
||||
return $this->morphOne(Location::class, 'locatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transaction type for this meeting
|
||||
*/
|
||||
public function transactionType()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\TransactionType::class, 'transaction_type_id');
|
||||
}
|
||||
|
||||
}
|
||||
36
app/Models/MessageSetting.php
Normal file
36
app/Models/MessageSetting.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MessageSetting extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'message_settingable_id',
|
||||
'message_settingable_type',
|
||||
'system_message',
|
||||
'payment_received',
|
||||
'star_received',
|
||||
'local_newsletter',
|
||||
'general_newsletter',
|
||||
'personal_chat',
|
||||
'group_chat',
|
||||
'chat_unread_delay',
|
||||
'call_expiry',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Get the parent message_settingable model (user, organization, or bank).
|
||||
* This is the instance (bank, organization, admin / alternative profile) where the settings are originally applied for.
|
||||
* one-to-many polymorphic
|
||||
*/
|
||||
public function message_settingable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
11
app/Models/News.php
Normal file
11
app/Models/News.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class News extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
604
app/Models/Organization.php
Normal file
604
app/Models/Organization.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Models\Language;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Notifications\VerifyProfileEmail;
|
||||
use App\Traits\ActiveStatesTrait;
|
||||
use App\Traits\HasPresence;
|
||||
use App\Traits\LocationTrait;
|
||||
use App\Traits\ProfileTrait;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
|
||||
use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableInterface;
|
||||
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
|
||||
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
|
||||
use Illuminate\Auth\MustVerifyEmail as AuthMustVerifyEmail;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Namu\WireChat\Traits\Chatable;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class Organization extends Authenticatable implements ReacterableInterface, MustVerifyEmail, ReactableInterface
|
||||
{
|
||||
use HasFactory;
|
||||
use AuthMustVerifyEmail;
|
||||
use Notifiable;
|
||||
use HasProfilePhoto;
|
||||
use HasRoles;
|
||||
use TaggableWithLocale;
|
||||
use Reacterable; // cybercog/laravel-love
|
||||
use Reactable; // cybercog/laravel-love
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
use LocationTrait;
|
||||
use Chatable;
|
||||
use ActiveStatesTrait;
|
||||
use LogsActivity; // Spatie Activity Log
|
||||
use HasPresence;
|
||||
use ProfileTrait;
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* The attributes that should be hidden for serialization.
|
||||
* WARNING: API'S CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'full_name',
|
||||
'password',
|
||||
'remember_token',
|
||||
'phone',
|
||||
'cyclos_id',
|
||||
'cyclos_salt',
|
||||
'cyclos_skills',
|
||||
'two_factor_confirmed_at',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_login_at',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'full_name',
|
||||
'email',
|
||||
'profile_photo_path',
|
||||
'about',
|
||||
'about_short',
|
||||
'motivation',
|
||||
'website',
|
||||
'phone',
|
||||
'phone_public',
|
||||
'password',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'last_login_at',
|
||||
'last_login_ip'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'phone_public' => 'boolean',
|
||||
'date_of_birth' => 'date',
|
||||
'email_verified_at' => 'datetime',
|
||||
'inactive_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
// protected $guard_name = 'web';
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'organizations_index';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert this model to a searchable array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
// Prepare eager loaded relationships
|
||||
$this->load(
|
||||
'languages',
|
||||
'locations.district.translations',
|
||||
'locations.city.translations',
|
||||
'locations.division.translations',
|
||||
'locations.country.translations',
|
||||
'tags.contexts.tags',
|
||||
'tags.contexts.tags.locale',
|
||||
'tags.contexts.category.ancestorsAndSelf',
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
|
||||
//TODO: Update to multilang database structure in future
|
||||
'about_nl' => $this->about,
|
||||
'about_en' => $this->about,
|
||||
'about_fr' => $this->about,
|
||||
'about_de' => $this->about,
|
||||
'about_es' => $this->about,
|
||||
'about_short_nl' => $this->about_short,
|
||||
'about_short_en' => $this->about_short,
|
||||
'about_short_fr' => $this->about_short,
|
||||
'about_short_de' => $this->about_short,
|
||||
'about_short_es' => $this->about_short,
|
||||
'motivation_nl' => $this->motivation,
|
||||
'motivation_en' => $this->motivation,
|
||||
'motivation_fr' => $this->motivation,
|
||||
'motivation_de' => $this->motivation,
|
||||
'motivation_es' => $this->motivation,
|
||||
'cyclos_skills' => $this->cyclos_skills, // Legacy column, will not be used in the future
|
||||
'website' => $this->website,
|
||||
|
||||
'last_login_at' => $this->last_login_at,
|
||||
'lang_preference' => $this->lang_preference,
|
||||
|
||||
'languages' => $this->languages->map(function ($language) { // map() as languages is a collection
|
||||
return [
|
||||
'id' => $language->id,
|
||||
'name' => $language->name,
|
||||
'lang_code' => $language->lang_code,
|
||||
];
|
||||
}),
|
||||
|
||||
'locations' => $this->locations->map(function ($location) { // map() as locations is a collection
|
||||
return [
|
||||
'id' => $location->id,
|
||||
'district' => $location->district ? $location->district->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'city' => $location->city ? $location->city->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'division' => $location->division ? $location->division->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'country' => $location->country ? $location->country->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
];
|
||||
}),
|
||||
|
||||
'tags' => $this->tags->map(function ($tag) {
|
||||
return [
|
||||
'contexts' => $tag->contexts
|
||||
->map(function ($context) {
|
||||
return [
|
||||
'tags' => $context->tags->map(function ($tag) {
|
||||
// Include the locale in the field name for tags
|
||||
return [
|
||||
'name_' . $tag->locale->locale => StringHelper::DutchTitleCase($tag->normalized),
|
||||
];
|
||||
}),
|
||||
'categories' => Category::with(['translations' => function ($query) { $query->select('category_id', 'locale', 'name');}])
|
||||
->find([ $context->category->ancestorsAndSelf()->get()->flatMap(function ($related) {
|
||||
$categoryPath = explode('.', $related->path);
|
||||
return $categoryPath;
|
||||
})
|
||||
->unique()->values()->toArray()
|
||||
])->map(function ($category) {
|
||||
// Include the locale in the field name for categories
|
||||
return $category->translations->mapWithKeys(function ($translation) {
|
||||
return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
|
||||
});
|
||||
}),
|
||||
];
|
||||
}),
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the organization's profile.
|
||||
* One-to-one
|
||||
*/
|
||||
public function profile()
|
||||
{
|
||||
return $this->hasOne(Profile::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organization's accounts.
|
||||
* One-to-many polymorphic.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return $this->morphMany(Account::class, 'accountable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the organization has any associated accounts.
|
||||
*
|
||||
* @return bool Returns true if the user has accounts, false otherwise.
|
||||
*/
|
||||
public function hasAccounts()
|
||||
{
|
||||
$accountsExists = DB::table('accounts')
|
||||
->where('accountable_id', $this->id)
|
||||
->where('accountable_type', 'App\Models\User')
|
||||
->exists();
|
||||
return $accountsExists;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the profile's bank(s) that it can manage.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function banksManaged()
|
||||
{
|
||||
return $this->belongsToMany(Bank::class);
|
||||
}
|
||||
|
||||
|
||||
public function banksClient()
|
||||
{
|
||||
return $this->morphToMany(Bank::class, 'client', 'bank_clients')
|
||||
->wherePivot('relationship_type', 'local');
|
||||
}
|
||||
|
||||
|
||||
public function attachBankClient($bank, $relationshipType = null)
|
||||
{
|
||||
// Set default relationship type if not provided
|
||||
$relationshipType = $relationshipType ?? 'local';
|
||||
|
||||
// Accept either ID or Bank model
|
||||
$bankId = $bank instanceof Bank ? $bank->id : $bank;
|
||||
|
||||
// Verify existence if numeric ID was provided
|
||||
if (is_numeric($bankId)) {
|
||||
if (!Bank::where('id', $bankId)->exists()) {
|
||||
throw new \Exception("Bank with ID {$bankId} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
$this->banksClient()->sync([$bankId => [
|
||||
'relationship_type' => $relationshipType,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function detachBankClient($bank, $relationshipType = null)
|
||||
{
|
||||
// Set default relationship type if not provided
|
||||
$relationshipType = $relationshipType ?? 'local';
|
||||
|
||||
// Accept either ID or Bank model
|
||||
$bankId = $bank instanceof Bank ? $bank->id : $bank;
|
||||
|
||||
// Verify existence if numeric ID was provided
|
||||
if (is_numeric($bankId)) {
|
||||
if (!Bank::where('id', $bankId)->exists()) {
|
||||
throw new \Exception("Bank with ID {$bankId} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
// Detach with additional pivot conditions
|
||||
$this->banksClient()->wherePivot('relationship_type', $relationshipType)
|
||||
->detach($bankId);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the organization's user(s).
|
||||
* Many-to-many.
|
||||
*/
|
||||
// todo: replace instances to managers() as these are actually profile managers and no organization users. See $this-> manangers()
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the organization's profile manager(s).
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function managers()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the languages for the organization.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function languages()
|
||||
{
|
||||
return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the social for the organization.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function socials()
|
||||
{
|
||||
return $this->morphToMany(Social::class, 'sociable')->withPivot('id', 'user_on_social', 'server_of_social');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related the locations of the organization.
|
||||
* One-to-many polymorph.
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->morphMany(Location::class, 'locatable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the organization's message settings.
|
||||
*/
|
||||
public function message_settings()
|
||||
{
|
||||
return $this->morphMany(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the Organization's posts.
|
||||
*/
|
||||
public function posts()
|
||||
{
|
||||
return $this->morphMany(Post::class, 'postable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the Organization's posts.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
\Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the activity log options for the User model.
|
||||
*
|
||||
* This method sets up the logging to:
|
||||
* - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
|
||||
* - Log only when these attributes have been modified (dirty).
|
||||
* - Prevent submission of empty logs.
|
||||
* - Use 'user' as the log name.
|
||||
*
|
||||
* @return \Spatie\Activitylog\LogOptions
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly([
|
||||
'name',
|
||||
'last_login_ip',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->logOnlyDirty() // Only log attributes that have been changed
|
||||
->dontSubmitEmptyLogs()
|
||||
->useLogName('Organization');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the URL for the user's cover image (avatar).
|
||||
* Adjust the 'avatar_url' field to your database setup.
|
||||
*/
|
||||
public function getCoverUrlAttribute(): ?string
|
||||
{
|
||||
return Storage::url($this->profile_photo_path) ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat:Returns the URL for the user's profile page.
|
||||
* Adjust the 'profile' route as needed for your setup.
|
||||
*/
|
||||
public function getProfileUrlAttribute(): ?string
|
||||
{
|
||||
return route('profile.show_by_type_and_id', ['type' => __('organization'), 'id' => $this->id]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the display name for the user.
|
||||
* Modify this to use your preferred name field.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): ?string
|
||||
{
|
||||
if ($this->full_name !== $this->name) {
|
||||
return $this->full_name . ' (' . $this->name . ')';
|
||||
} elseif ($this->name) {
|
||||
return $this->name;
|
||||
} else {
|
||||
return __('Organization');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Search for users when creating a new chat or adding members to a group.
|
||||
* Customize the search logic to limit results, such as restricting to friends or eligible users only.
|
||||
*/
|
||||
public function searchChatables(string $query): ?\Illuminate\Support\Collection
|
||||
{
|
||||
$searchableFields = ['name', 'full_name'];
|
||||
|
||||
// Search across all profile types
|
||||
$users = \App\Models\User::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$organizations = Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$banks = \App\Models\Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$admins = \App\Models\Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
// Combine all results into a base Collection to avoid serialization issues
|
||||
$results = collect()
|
||||
->merge($users->all())
|
||||
->merge($organizations->all())
|
||||
->merge($banks->all())
|
||||
->merge($admins->all());
|
||||
|
||||
// Filter out profiles based on configuration
|
||||
return $results->filter(function ($profile) {
|
||||
// Check inactive profiles
|
||||
if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check email unverified profiles
|
||||
if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check incomplete profiles
|
||||
if (
|
||||
timebank_config('profile_incomplete.messenger_hidden')
|
||||
&& method_exists($profile, 'hasIncompleteProfile')
|
||||
&& $profile->hasIncompleteProfile($profile)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->take(6)->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Determine if the user can create new groups.
|
||||
*/
|
||||
public function canCreateGroups(): bool
|
||||
{
|
||||
return timebank_config('profiles.organization.messenger_can_create_groups');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the Organization has any transactions with another model (User, Organization, Bank).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTransactionsWith($model): bool
|
||||
{
|
||||
$modelClass = get_class($model);
|
||||
$modelId = $model->id;
|
||||
|
||||
// Check if this Organization is involved in any transaction where the other model is either the sender or receiver
|
||||
return Transaction::where(function ($query) use ($modelClass, $modelId) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
});
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) {
|
||||
$q->where('accountable_type', Organization::class)
|
||||
->where('accountable_id', $this->id);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) {
|
||||
$q->where('accountable_type', Organization::class)
|
||||
->where('accountable_id', $this->id);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message settings for this organization
|
||||
*/
|
||||
public function messageSettings()
|
||||
{
|
||||
return $this->morphMany(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
}
|
||||
259
app/Models/Post.php
Normal file
259
app/Models/Post.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Models\Category;
|
||||
use App\Models\Image;
|
||||
use App\Models\Meeting;
|
||||
use App\Models\PostTranslation;
|
||||
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
|
||||
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Znck\Eloquent\Traits\BelongsToThrough;
|
||||
|
||||
|
||||
class Post extends Model implements HasMedia, ReactableInterface
|
||||
{
|
||||
use HasFactory;
|
||||
use BelongsToThrough;
|
||||
use InteractsWithMedia;
|
||||
use SoftDeletes;
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
use Reactable; // cybercog/laravel-love
|
||||
|
||||
|
||||
|
||||
protected $fillable = ['postable_id', 'postable_type', 'category_id', 'author_id', 'author_model'];
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'posts_index';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert the this model to a searchable array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'category_id' => $this->category_id,
|
||||
|
||||
'postable' => [
|
||||
'id' => $this->postable ? $this->postable->id : '',
|
||||
'name' => $this->postable ? $this->postable->name : '',
|
||||
],
|
||||
|
||||
'post_translations' => $this->translations->mapWithKeys(function ($translation) {
|
||||
// mapWithKeys() as translations is a collection
|
||||
return [
|
||||
'title_' . $translation->locale => $translation->title,
|
||||
'excerpt_' . $translation->locale => $translation->excerpt,
|
||||
'content_' . $translation->locale => $translation->content,
|
||||
// Add publication fields
|
||||
'from_' . $translation->locale => $translation->from,
|
||||
'till_' . $translation->locale => $translation->till,
|
||||
'deleted_at_' . $translation->locale => $translation->deleted_at,
|
||||
'status_' . $translation->locale => $translation->status,
|
||||
];
|
||||
})->toArray(),
|
||||
|
||||
'post_category' => $this->category ? [
|
||||
'id' => $this->category->id,
|
||||
'names' => $this->category->translations->mapWithKeys(function ($translation) {
|
||||
// Include the locale in the field name for categories
|
||||
return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
|
||||
})->toArray(),
|
||||
] : [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the creator of the post (i.e. user or organization).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function postable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the author of the post (polymorphic: User, Organization, or Bank).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function author()
|
||||
{
|
||||
return $this->morphTo('author', 'author_model', 'author_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related translations of the post
|
||||
* One-to-many relationship
|
||||
* @return void
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return $this->hasMany(PostTranslation::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related category of the post
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the related images of the posts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
return $this->morphToMany(Image::class, 'imageable');
|
||||
}
|
||||
|
||||
|
||||
public static function last()
|
||||
{
|
||||
return static::all()->last();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Spatie Media Library media conversions
|
||||
* When updated you can use:
|
||||
* php artisan media-library:regenerate
|
||||
* to regenerate media disk and database of all media stored (make sure you restart the queue worker first)
|
||||
*
|
||||
* @param mixed $media
|
||||
* @return void
|
||||
*/
|
||||
public function registerMediaConversions(Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('favicon') //1:1
|
||||
->focalCrop(32, 32, 50, 50)
|
||||
->optimize()
|
||||
->format('gif')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('logo') //1:1
|
||||
->focalCrop(160, 160, 50, 50)
|
||||
->optimize()
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('thumbnail') // 1:1
|
||||
->focalCrop(150, 150, 50, 50)
|
||||
->optimize()
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('blog') //3:2
|
||||
->focalCrop(1200, 630, 50, 50)
|
||||
->withResponsiveImages()
|
||||
->optimize()
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('hero') //16:9
|
||||
->focalCrop(3840, 2160, 50, 50)
|
||||
->withResponsiveImages()
|
||||
->optimize()
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('half_hero') //16:4.5 panoramic
|
||||
->focalCrop(3840, 1080, 50, 50)
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('4_3') //4:3
|
||||
->focalCrop(3072, 2304, 50, 50)
|
||||
->withResponsiveImages()
|
||||
->optimize()
|
||||
->format('webp')
|
||||
->performOnCollections('posts');
|
||||
|
||||
$this->addMediaConversion('email') // For email newsletters - resizes without cropping
|
||||
->width(800)
|
||||
->optimize()
|
||||
->format('jpg')
|
||||
->quality(85)
|
||||
->performOnCollections('posts');
|
||||
|
||||
}
|
||||
|
||||
public function registerMediaCollection(): void
|
||||
{
|
||||
$this->addMediaCollection('posts')
|
||||
->useDisk('media')
|
||||
->singleFile();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the related meeting of the post
|
||||
* One-to-one relation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function meeting()
|
||||
{
|
||||
return $this->hasOne(Meeting::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of 'like' reactions for this post
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getLikeCountAttribute()
|
||||
{
|
||||
// Check if post is registered as reactant before getting reactions
|
||||
if (!$this->isRegisteredAsLoveReactant()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the reactant facade
|
||||
$reactantFacade = $this->viaLoveReactant();
|
||||
|
||||
// Get the reaction counter for 'Like' type
|
||||
$reactionCounter = $reactantFacade->getReactionCounterOfType('Like');
|
||||
|
||||
if (!$reactionCounter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Return the total count from all users
|
||||
return $reactionCounter->getCount();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
48
app/Models/PostTranslation.php
Normal file
48
app/Models/PostTranslation.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Cviebrock\EloquentSluggable\Sluggable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
|
||||
class PostTranslation extends Model
|
||||
{
|
||||
use HasFactory, Sluggable, SoftDeletes;
|
||||
|
||||
protected $fillable = ['post_id', 'locale', 'slug', 'title', 'excerpt', 'content', 'status', 'updated_by_user_id', 'from', 'till'];
|
||||
|
||||
|
||||
/**
|
||||
* Get related post for this translation
|
||||
* Ont-to-many relationship
|
||||
* @return void
|
||||
*/
|
||||
public function post()
|
||||
{
|
||||
return $this->belongsTo(Post::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the user who last updated the post translation.
|
||||
*/
|
||||
public function updated_by_user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by_user_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function sluggable(): array
|
||||
{
|
||||
return [
|
||||
'slug' => [
|
||||
'source' => 'title'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
33
app/Models/Profile.php
Normal file
33
app/Models/Profile.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
// use Laravel\Jetstream\HasProfilePhoto;
|
||||
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
//The attributes that are mass assignable.
|
||||
// protected $fillable = [
|
||||
// 'profile_photo_path',
|
||||
// ];
|
||||
|
||||
|
||||
// protected $appends = [
|
||||
// 'profile_photo_url',
|
||||
// ];
|
||||
|
||||
|
||||
|
||||
// Get all of the owning user
|
||||
// One-to-one
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
27
app/Models/Scopes/LocalizeScope.php
Normal file
27
app/Models/Scopes/LocalizeScope.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
class LocalizeScope implements Scope
|
||||
{
|
||||
/**
|
||||
* Apply the scope to a given Eloquent query builder.
|
||||
* Gets the localized name of a model.
|
||||
* In the App::getLocale, or if not exists, in the App::getFallbackLocale language.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $builder The Eloquent query builder.
|
||||
* @param \Illuminate\Database\Eloquent\Model $model The Eloquent model.
|
||||
* @return void
|
||||
*/
|
||||
public function apply(Builder $builder, Model $model)
|
||||
{
|
||||
$builder->where('locale', App::getLocale())
|
||||
->orWhere('locale', App::getFallbackLocale())
|
||||
->orderByRaw("(CASE WHEN locale = ? THEN 1 WHEN locale = ? THEN 2 END)", [App::getLocale(), App::getFallbackLocale()]);
|
||||
}
|
||||
}
|
||||
11
app/Models/SiteContentsPay.php
Normal file
11
app/Models/SiteContentsPay.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SiteContentsPay extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
||||
48
app/Models/Social.php
Normal file
48
app/Models/Social.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Social extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'icon',
|
||||
'url_structure'
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
/**
|
||||
* Get all sociables that are assigned to this social.
|
||||
*/
|
||||
public function sociables()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'sociable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all users that are assigned to this social.
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'sociable')->where('sociable_type', User::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all organizations that are assigned to this social.
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'sociable')->where('sociable_type', Organization::class);
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
252
app/Models/Tag.php
Normal file
252
app/Models/Tag.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\TaggableContext;
|
||||
use App\Models\TaggableLocale;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Tag extends \Cviebrock\EloquentTaggable\Models\Tag
|
||||
|
||||
/**
|
||||
* Class Tag extends \Cviebrock\EloquentTaggable\Models\Tag
|
||||
*
|
||||
* This extended the Tag class of cviebrock / eloquent-taggable package.
|
||||
* It adds an one-to-many relationship to store additional tag context data in the tag_contexts table.
|
||||
* Use this extension with the trait App\Traits\TaggableWithContext.
|
||||
*
|
||||
*/
|
||||
{
|
||||
use HasFactory;
|
||||
use TaggableWithLocale;
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
|
||||
protected $table = 'taggable_tags';
|
||||
protected $primaryKey = 'tag_id';
|
||||
protected $appends = ['locales', 'categories'];
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Boot the model and add event listeners.
|
||||
*
|
||||
* The 'deleting' event listener performs the following actions:
|
||||
* - Detaches all relationships with users, organizations, and banks.
|
||||
* - Deletes all locales and contexts directly tied to the tag.
|
||||
*
|
||||
* This listener ensures that when a Tag instance is deleted,
|
||||
* all related data and associations are properly cleaned up to
|
||||
* maintain data integrity and avoid orphaned records.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function ($tag) {
|
||||
// Remove relationships of users, organizations, banks
|
||||
$tag->users()->detach();
|
||||
$tag->organizations()->detach();
|
||||
$tag->banks()->detach();
|
||||
// Delete locale tied directly to the tag
|
||||
$tag->locale()->delete();
|
||||
// Clean up any orphaned contexts (with no related tag models left)
|
||||
$unusedLocaleContexts = TaggableLocaleContext::doesntHave('tags')->get();
|
||||
$unusedLocaleContexts->each(fn ($localeContext) => $localeContext->delete());
|
||||
$unusedContexts = TaggableContext::doesntHave('tags')->get();
|
||||
$unusedContexts->each(fn ($context) => $context->delete());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'tags_index';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the value used to index the model.
|
||||
*/
|
||||
public function getScoutKey(): mixed
|
||||
{
|
||||
return $this->tag_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key name used to index the model.
|
||||
*/
|
||||
public function getScoutKeyName(): mixed
|
||||
{
|
||||
return 'tag_id';
|
||||
}
|
||||
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->morphedByMany(User::class, 'taggable', 'taggable_taggables', 'tag_id', 'taggable_id');
|
||||
}
|
||||
|
||||
|
||||
public function organizations()
|
||||
{
|
||||
return $this->morphedByMany(Organization::class, 'taggable', 'taggable_taggables', 'tag_id', 'taggable_id');
|
||||
}
|
||||
|
||||
|
||||
public function banks()
|
||||
{
|
||||
return $this->morphedByMany(Bank::class, 'taggable', 'taggable_taggables', 'tag_id', 'taggable_id');
|
||||
}
|
||||
|
||||
|
||||
public function locale()
|
||||
{
|
||||
return $this->hasOne(TaggableLocale::class, 'taggable_tag_id');
|
||||
}
|
||||
|
||||
|
||||
public function localeCode()
|
||||
{
|
||||
return $this->hasOne(TaggableLocale::class, 'taggable_tag_id')->select('locale');
|
||||
}
|
||||
|
||||
|
||||
public function contexts()
|
||||
{
|
||||
return $this->belongsToMany(TaggableContext::class, 'taggable_locale_context', 'tag_id', 'context_id');
|
||||
}
|
||||
|
||||
|
||||
public function localeContext()
|
||||
{
|
||||
return $this->hasOne(TaggableLocaleContext::class, 'tag_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Scope to find tags by name.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param $value
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByLocalName(Builder $query, string $value): Builder
|
||||
{
|
||||
$normalized = app(TagService::class)->normalize($value);
|
||||
|
||||
return $query->where('normalized', $normalized);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get all related tags with their locales using a raw query.
|
||||
* Note: this is not a relationship definition!
|
||||
*
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
return DB::table('taggable_tags as tt')
|
||||
->join('taggable_locale_context as tlc', 'tt.tag_id', '=', 'tlc.tag_id')
|
||||
->join('taggable_locales as tl', 'tt.tag_id', '=', 'tl.taggable_tag_id')
|
||||
->select('tt.*', 'tl.*', 'tlc.context_id')
|
||||
->whereIn('tlc.context_id', function ($query) {
|
||||
$query->select('context_id')
|
||||
->from('taggable_locale_context')
|
||||
->where('tag_id', $this->tag_id);
|
||||
})
|
||||
->distinct()
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
public function getLocalesAttribute()
|
||||
{
|
||||
// Get context_ids for current tag
|
||||
$contextIds = DB::table('taggable_locale_context')
|
||||
->where('tag_id', $this->tag_id)
|
||||
->pluck('context_id');
|
||||
|
||||
// Get tag_ids that share these context_ids
|
||||
$relatedTagIds = DB::table('taggable_locale_context')
|
||||
->whereIn('context_id', $contextIds)
|
||||
->pluck('tag_id');
|
||||
|
||||
// Get translations for these tags
|
||||
return DB::table('taggable_tags as tt')
|
||||
->join('taggable_locales as tl', 'tt.tag_id', '=', 'tl.taggable_tag_id')
|
||||
->whereIn('tt.tag_id', $relatedTagIds)
|
||||
->select('tt.tag_id', 'tt.name', 'tl.*')
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
public function getCategoriesAttribute()
|
||||
{
|
||||
// Get context_ids for current tag
|
||||
$contextIds = DB::table('taggable_locale_context')
|
||||
->where('tag_id', $this->tag_id)
|
||||
->pluck('context_id');
|
||||
|
||||
// Get categories for these contexts
|
||||
$categoryId = DB::table('taggable_contexts as tc')
|
||||
->join('categories as c', 'tc.category_id', '=', 'c.id')
|
||||
->whereIn('tc.id', $contextIds)
|
||||
->pluck('c.id');
|
||||
|
||||
// Get the first category ID
|
||||
$firstCategoryId = $categoryId->first();
|
||||
|
||||
// Find the category by its ID
|
||||
$category = Category::find($firstCategoryId);
|
||||
|
||||
$categoryWithRelated = $category ? $category->related() : null;
|
||||
|
||||
return $categoryWithRelated;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the translation attribute for the tag.
|
||||
*
|
||||
* This method attempts to retrieve the translation for the tag
|
||||
* in the current locale. If a translation in the current locale is
|
||||
* not found, it falls back to the base locale defined in the timebank-cc config.
|
||||
*
|
||||
* @return \App\Models\Translation|null The translation object for the category in the current or base locale, or null if not found.
|
||||
*/
|
||||
public function getTranslationAttribute()
|
||||
{
|
||||
$baseLocale = timebank_config('base_language');
|
||||
// Retrieve all translations using the translations() method
|
||||
$translations = $this->translations();
|
||||
|
||||
// Attempt to get the translation in the current locale
|
||||
$translation = $translations->firstWhere('locale', App::getLocale());
|
||||
|
||||
// Fallback to base locale if translation not found
|
||||
if (!$translation) {
|
||||
$translation = $translations->firstWhere('locale', $baseLocale);
|
||||
// Fallback to only available locale if no translation in base locale is not found
|
||||
if (!$translation) {
|
||||
return $translations->first(); //We can do first() as tags can only be translated to the base locale. So there are no other translations.
|
||||
}
|
||||
}
|
||||
return $translation;
|
||||
}
|
||||
}
|
||||
50
app/Models/TaggableContext.php
Normal file
50
app/Models/TaggableContext.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TaggableContext extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use TaggableWithLocale;
|
||||
|
||||
//! TODO: This method is not tested yet, as there's no interface yet for updating taggable_contexts
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// updating created_by and updated_by when model is created
|
||||
static::creating(function ($model) {
|
||||
if (!$model->isDirty('updated_by_user')) {
|
||||
$model->updated_by_user = Auth::guard('web')->user()->id;
|
||||
}
|
||||
});
|
||||
|
||||
// updating updated_by when model is updated
|
||||
static::updating(function ($model) {
|
||||
if (!$model->isDirty('updated_by_user')) {
|
||||
$model->updated_by_user = Auth::guard('web')->user()->id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected $table = 'taggable_contexts';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'taggable_locale_context', 'context_id', 'tag_id');
|
||||
}
|
||||
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
47
app/Models/TaggableLocale.php
Normal file
47
app/Models/TaggableLocale.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Tag;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TaggableLocale extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use TaggableWithLocale;
|
||||
|
||||
protected $table = 'taggable_locales';
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// updating created_by and updated_by when model is created
|
||||
static::creating(function ($model) {
|
||||
if (!$model->isDirty('updated_by_user')) {
|
||||
$model->updated_by_user = Auth::guard('web')->user()->id;
|
||||
}
|
||||
});
|
||||
|
||||
// updating updated_by when model is updated
|
||||
static::updating(function ($model) {
|
||||
if (!$model->isDirty('updated_by_user')) {
|
||||
$model->updated_by_user = Auth::guard('web')->user()->id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function tag()
|
||||
{
|
||||
return $this->belongsTo(Tag::class, 'taggable_tag_id', 'tag_id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
22
app/Models/TaggableLocaleContext.php
Normal file
22
app/Models/TaggableLocaleContext.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TaggableLocaleContext extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'taggable_locale_context';
|
||||
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'taggable_locale_context', 'context_id', 'tag_id');
|
||||
}
|
||||
|
||||
}
|
||||
24
app/Models/TaggableTaggable.php
Normal file
24
app/Models/TaggableTaggable.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class TaggableTaggable extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
Static::updating(function() {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
protected $table = 'taggable_taggables';
|
||||
protected $primaryKey = 'tag_id';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
129
app/Models/Transaction.php
Normal file
129
app/Models/Transaction.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Laravel\Scout\Searchable;
|
||||
|
||||
class Transaction extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'from_account_id',
|
||||
'to_account_id',
|
||||
'creator_user_id',
|
||||
'from_authorised_by_user_id',
|
||||
'from_authorisation_time',
|
||||
'to_authorised_by_user_id',
|
||||
'to_authorisation_time',
|
||||
'amount',
|
||||
'programmed_time',
|
||||
'description',
|
||||
'from_reference',
|
||||
'to_reference',
|
||||
'transaction_type_id',
|
||||
'transaction_status_id',
|
||||
'cancelled_by_user_id',
|
||||
'cancelled_time',
|
||||
'advertisement_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'transactions';
|
||||
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'transactions_index';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert the transaction model to a searchable array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
// Log the start of the method
|
||||
// info('toSearchableArray method called for Transaction ID: ' . $this->id);
|
||||
|
||||
// Eager load relationships
|
||||
$this->load(['accountFrom.accountable', 'accountTo.accountable']);
|
||||
|
||||
// Extract relationship data safely
|
||||
$accountFrom = $this->accountFrom;
|
||||
$accountTo = $this->accountTo;
|
||||
|
||||
$accountFromName = $accountFrom->name ?? '';
|
||||
$accountToName = $accountTo->name ?? '';
|
||||
|
||||
$relationFromName = $accountFrom && $accountFrom->accountable ? $accountFrom->accountable->first()->name ?? '' : '';
|
||||
$relationToName = $accountTo && $accountTo->accountable ? $accountTo->accountable->first()->name ?? '' : '';
|
||||
|
||||
// Log the loaded relationships for debugging
|
||||
// info('Account From:', ['accountFrom' => $accountFrom]);
|
||||
// info('Account To:', ['accountTo' => $accountTo]);
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'created_at' => $this->created_at,
|
||||
'from_account_id' => $this->accountFrom,
|
||||
'to_account_id' => $this->accountTo,
|
||||
'amount' => $this->amount,
|
||||
'account_from_name' => $accountFromName,
|
||||
'account_to_name' => $accountToName,
|
||||
'relation_from' => 'From ' . $relationFromName,
|
||||
'relation_to' => 'To ' . $relationToName,
|
||||
'description' => $this->description ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function accountFrom()
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'from_account_id');
|
||||
}
|
||||
|
||||
|
||||
public function accountTo()
|
||||
{
|
||||
return $this->belongsTo(Account::class, 'to_account_id');
|
||||
}
|
||||
|
||||
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'creator_user_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the transaction type that owns the transaction.
|
||||
*/
|
||||
public function transactionType()
|
||||
{
|
||||
return $this->belongsTo(TransactionType::class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
19
app/Models/TransactionType.php
Normal file
19
app/Models/TransactionType.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TransactionType extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Get the transactions for the transaction type.
|
||||
*/
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(Transaction::class);
|
||||
}
|
||||
}
|
||||
711
app/Models/User.php
Normal file
711
app/Models/User.php
Normal file
@@ -0,0 +1,711 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Models\Account;
|
||||
use App\Models\Admin;
|
||||
use App\Models\Bank;
|
||||
use App\Models\Language;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Post;
|
||||
use App\Notifications\VerifyProfileEmail;
|
||||
use App\Traits\ActiveStatesTrait;
|
||||
use App\Traits\HasPresence;
|
||||
use App\Traits\LocationTrait;
|
||||
use App\Traits\ProfileTrait;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
|
||||
use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableInterface;
|
||||
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
|
||||
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Namu\WireChat\Traits\Chatable;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail, ReacterableInterface, ReactableInterface
|
||||
{
|
||||
use HasApiTokens;
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
use TwoFactorAuthenticatable;
|
||||
use HasProfilePhoto;
|
||||
use HasApiTokens; // Laravel Sanctum API support
|
||||
use HasRoles; // Spatie Permissions
|
||||
use LogsActivity; // Spatie Activity Log
|
||||
use TaggableWithLocale;
|
||||
use Reacterable; // cybercog/laravel-love
|
||||
use Reactable; // cybercog/laravel-love
|
||||
use Searchable; // laravel/scout with ElasticSearch
|
||||
use LocationTrait;
|
||||
use Chatable;
|
||||
use ActiveStatesTrait;
|
||||
use HasPresence;
|
||||
use ProfileTrait;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'full_name',
|
||||
'email',
|
||||
'profile_photo_path',
|
||||
'about',
|
||||
'about_short',
|
||||
'motivation',
|
||||
'website',
|
||||
'phone',
|
||||
'phone_public',
|
||||
'password',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'lang_preference',
|
||||
'last_login_at',
|
||||
'last_login_ip',
|
||||
'principles_terms_accepted'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
* BEWARE: API'S CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'full_name',
|
||||
'password',
|
||||
'remember_token',
|
||||
'phone',
|
||||
'cyclos_id',
|
||||
'cyclos_salt',
|
||||
'cyclos_skills',
|
||||
'two_factor_confirmed_at',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_secret',
|
||||
'limit_min',
|
||||
'limit_max',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'last_login_at',
|
||||
'last_login_ip',
|
||||
'principles_terms_accepted',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'phone_public' => 'boolean',
|
||||
'date_of_birth' => 'date',
|
||||
'email_verified_at' => 'datetime',
|
||||
'principles_terms_accepted' => 'array',
|
||||
'inactive_at' => 'datetime',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* The name of the guard used by this model.
|
||||
* This is used to determine the default guard for authentication.
|
||||
*/
|
||||
protected string $guard_name = 'web';
|
||||
protected function getDefaultGuardName(): string
|
||||
{
|
||||
return $this->guard_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index name for the model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function searchableAs()
|
||||
{
|
||||
return 'users_index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this model to a searchable array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toSearchableArray()
|
||||
{
|
||||
// Prepare eager loaded relationships
|
||||
$this->load(
|
||||
'languages',
|
||||
'locations.district.translations',
|
||||
'locations.city.translations',
|
||||
'locations.division.translations',
|
||||
'locations.country.translations',
|
||||
'tags.contexts.tags',
|
||||
'tags.contexts.tags.locale',
|
||||
'tags.contexts.category.ancestorsAndSelf',
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
|
||||
//TODO: Update to multilang database structure in future, this ElasticSearch index is already prepared for it
|
||||
'about_nl' => $this->about,
|
||||
'about_en' => $this->about,
|
||||
'about_fr' => $this->about,
|
||||
'about_de' => $this->about,
|
||||
'about_es' => $this->about,
|
||||
'about_short_nl' => $this->about_short,
|
||||
'about_short_en' => $this->about_short,
|
||||
'about_short_fr' => $this->about_short,
|
||||
'about_short_de' => $this->about_short,
|
||||
'about_short_es' => $this->about_short,
|
||||
'motivation_nl' => $this->motivation,
|
||||
'motivation_en' => $this->motivation,
|
||||
'motivation_fr' => $this->motivation,
|
||||
'motivation_de' => $this->motivation,
|
||||
'motivation_es' => $this->motivation,
|
||||
'cyclos_skills' => $this->cyclos_skills, // Legacy column, will not be used in the future
|
||||
'website' => $this->website,
|
||||
|
||||
|
||||
'last_login_at' => $this->last_login_at,
|
||||
'lang_preference' => $this->lang_preference,
|
||||
|
||||
'languages' => $this->languages->map(function ($language) { // map() as languages is a collection
|
||||
return [
|
||||
'id' => $language->id,
|
||||
'name' => $language->name,
|
||||
'lang_code' => $language->lang_code,
|
||||
];
|
||||
}),
|
||||
|
||||
'locations' => $this->locations->map(function ($location) { // map() as locations is a collection
|
||||
return [
|
||||
'id' => $location->id,
|
||||
'district' => $location->district ? $location->district->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'city' => $location->city ? $location->city->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'division' => $location->division ? $location->division->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
'country' => $location->country ? $location->country->translations->map(function ($translation) { // map() as translations is a collection
|
||||
return $translation->name;
|
||||
})->toArray() : [],
|
||||
];
|
||||
}),
|
||||
|
||||
'tags' => $this->tags->map(function ($tag) {
|
||||
return [
|
||||
'contexts' => $tag->contexts
|
||||
->map(function ($context) {
|
||||
return [
|
||||
'tags' => $context->tags->map(function ($tag) {
|
||||
// Include the locale in the field name for tags
|
||||
return [
|
||||
'name_' . $tag->locale->locale => StringHelper::DutchTitleCase($tag->normalized),
|
||||
];
|
||||
}),
|
||||
'categories' => Category::with(['translations' => function ($query) { $query->select('category_id', 'locale', 'name');}])
|
||||
->find([ $context->category->ancestorsAndSelf()->get()->flatMap(function ($related) {
|
||||
$categoryPath = explode('.', $related->path);
|
||||
return $categoryPath;
|
||||
})
|
||||
->unique()->values()->toArray()
|
||||
])->map(function ($category) {
|
||||
// Include the locale in the field name for categories
|
||||
return $category->translations->mapWithKeys(function ($translation) {
|
||||
return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
|
||||
});
|
||||
}),
|
||||
];
|
||||
}),
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the user's profile.
|
||||
* One-to-one
|
||||
*/
|
||||
public function profile()
|
||||
{
|
||||
return $this->hasOne(Profile::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the user's organization(s).
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function organizations()
|
||||
{
|
||||
return $this->belongsToMany(Organization::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the user's bank(s) that it can manage.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function banksManaged()
|
||||
{
|
||||
return $this->belongsToMany(Bank::class, 'bank_user', 'user_id', 'bank_id');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any non-personal profiles.
|
||||
*
|
||||
* Checks whether the user is associated with any organizations, manages any banks,
|
||||
* or holds any administrative assignments. Returns true if at least one of these
|
||||
* related profiles exists; otherwise returns false.
|
||||
*
|
||||
* @return bool True if the user has organization, bank-managed, or admin profiles; false otherwise.
|
||||
*/
|
||||
public function hasOtherProfiles()
|
||||
{
|
||||
return $this->organizations()->exists()
|
||||
|| $this->banksManaged()->exists()
|
||||
|| $this->admins()->exists();
|
||||
}
|
||||
|
||||
|
||||
public function banksClient()
|
||||
{
|
||||
return $this->morphToMany(Bank::class, 'client', 'bank_clients')
|
||||
->wherePivot('relationship_type', 'local');
|
||||
}
|
||||
|
||||
|
||||
public function attachBankClient($bank, $relationshipType = null)
|
||||
{
|
||||
// Set default relationship type if not provided
|
||||
$relationshipType = $relationshipType ?? 'local';
|
||||
|
||||
// Accept either ID or Bank model
|
||||
$bankId = $bank instanceof Bank ? $bank->id : $bank;
|
||||
|
||||
// Verify existence if numeric ID was provided
|
||||
if (is_numeric($bankId)) {
|
||||
if (!Bank::where('id', $bankId)->exists()) {
|
||||
throw new \Exception("Bank with ID {$bankId} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
$this->banksClient()->sync([$bankId => [
|
||||
'relationship_type' => $relationshipType,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
]]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function detachBankClient($bank, $relationshipType = null)
|
||||
{
|
||||
// Set default relationship type if not provided
|
||||
$relationshipType = $relationshipType ?? 'local';
|
||||
|
||||
// Accept either ID or Bank model
|
||||
$bankId = $bank instanceof Bank ? $bank->id : $bank;
|
||||
|
||||
// Verify existence if numeric ID was provided
|
||||
if (is_numeric($bankId)) {
|
||||
if (!Bank::where('id', $bankId)->exists()) {
|
||||
throw new \Exception("Bank with ID {$bankId} does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
// Detach with additional pivot conditions
|
||||
$this->banksClient()->wherePivot('relationship_type', $relationshipType)
|
||||
->detach($bankId);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the user's admin(s) that it can manage.
|
||||
* Many-to-many.
|
||||
*/
|
||||
public function admins()
|
||||
{
|
||||
return $this->belongsToMany(Admin::class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related accounts of the user.
|
||||
* One-to-many polymorphic.
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return $this->morphMany(Account::class, 'accountable');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user has any associated accounts.
|
||||
*
|
||||
* @return bool Returns true if the user has accounts, false otherwise.
|
||||
*/
|
||||
public function hasAccounts()
|
||||
{
|
||||
$accountsExists = DB::table('accounts')
|
||||
->where('accountable_id', $this->id)
|
||||
->where('accountable_type', 'App\Models\User')
|
||||
->exists();
|
||||
return $accountsExists;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get all related languages of the user.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function languages()
|
||||
{
|
||||
return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all related socials of the user.
|
||||
* Many-to-many polymorphic.
|
||||
*/
|
||||
public function socials()
|
||||
{
|
||||
return $this->morphToMany(Social::class, 'sociable')->withPivot('id', 'user_on_social', 'server_of_social');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all related the locations of the user.
|
||||
* One-to-many polymorph.
|
||||
*/
|
||||
public function locations()
|
||||
{
|
||||
return $this->morphMany(Location::class, 'locatable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the user's message settings.
|
||||
*/
|
||||
public function message_settings()
|
||||
{
|
||||
return $this->morphOne(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure the activity log options for the User model.
|
||||
*
|
||||
* This method sets up the logging to:
|
||||
* - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
|
||||
* - Log only when these attributes have been modified (dirty).
|
||||
* - Prevent submission of empty logs.
|
||||
* - Use 'user' as the log name.
|
||||
*
|
||||
* @return \Spatie\Activitylog\LogOptions
|
||||
*/
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly([
|
||||
'name',
|
||||
'last_login_ip',
|
||||
'inactive_at',
|
||||
'deleted_at',
|
||||
])
|
||||
->logOnlyDirty() // Only log attributes that have been changed
|
||||
->dontSubmitEmptyLogs()
|
||||
->useLogName('User');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the User's posts.
|
||||
*/
|
||||
public function posts()
|
||||
{
|
||||
return $this->morphMany(Post::class, 'postable');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all post translations updated by the user.
|
||||
*/
|
||||
public function post_translations_updated()
|
||||
{
|
||||
return $this->hasMany(PostTranslation::class, 'updated_by_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the User's categories.
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
return $this->morphMany(Category::class, 'categoryable');
|
||||
}
|
||||
|
||||
|
||||
public function sendEmailVerificationNotification()
|
||||
{
|
||||
\Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
* @param string $token
|
||||
* @return void
|
||||
*/
|
||||
public function sendPasswordResetNotification($token)
|
||||
{
|
||||
$locale = $this->lang_preference ?? config('app.fallback_locale');
|
||||
$this->notify(new \App\Notifications\UserPasswordResetNotification($token, $locale));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the URL for the user's cover image (avatar).
|
||||
* Adjust the 'avatar_url' field to your database setup.
|
||||
*/
|
||||
public function getCoverUrlAttribute(): ?string
|
||||
{
|
||||
return Storage::url($this->profile_photo_path) ?? null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat:Returns the URL for the user's profile page.
|
||||
* Adjust the 'profile' route as needed for your setup.
|
||||
*/
|
||||
public function getProfileUrlAttribute(): ?string
|
||||
{
|
||||
return route('profile.show_by_type_and_id', ['type' => __('user'), 'id' => $this->id]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Returns the display name for the user.
|
||||
* Modify this to use your preferred name field.
|
||||
*/
|
||||
public function getDisplayNameAttribute(): ?string
|
||||
{
|
||||
if ($this->full_name !== $this->name) {
|
||||
return $this->full_name . ' (' . $this->name . ')';
|
||||
} elseif ($this->name) {
|
||||
return $this->name;
|
||||
} else {
|
||||
return __('User');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for users when creating a new chat or adding members to a group.
|
||||
* Customize the search logic to limit results, such as restricting to friends or eligible users only.
|
||||
*/
|
||||
public function searchChatables(string $query): ?\Illuminate\Support\Collection
|
||||
{
|
||||
$searchableFields = ['name', 'full_name'];
|
||||
|
||||
// Search across all profile types
|
||||
$users = User::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$organizations = \App\Models\Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$banks = \App\Models\Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
$admins = \App\Models\Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
|
||||
foreach ($searchableFields as $field) {
|
||||
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
|
||||
}
|
||||
})->limit(6)->get();
|
||||
|
||||
// Combine all results into a base Collection to avoid serialization issues
|
||||
$results = collect()
|
||||
->merge($users->all())
|
||||
->merge($organizations->all())
|
||||
->merge($banks->all())
|
||||
->merge($admins->all());
|
||||
|
||||
// Filter out profiles based on configuration
|
||||
return $results->filter(function ($profile) {
|
||||
// Check inactive profiles
|
||||
if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check email unverified profiles
|
||||
if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check incomplete profiles
|
||||
if (
|
||||
timebank_config('profile_incomplete.messenger_hidden')
|
||||
&& method_exists($profile, 'hasIncompleteProfile')
|
||||
&& $profile->hasIncompleteProfile($profile)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})->take(6)->values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wirechat: Determine if the user can create new groups.
|
||||
*/
|
||||
public function canCreateGroups(): bool
|
||||
{
|
||||
return timebank_config('profiles.user.messenger_can_create_groups');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has any transactions with another model (User, Organization, Bank).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $model
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTransactionsWith($model): bool
|
||||
{
|
||||
$modelClass = get_class($model);
|
||||
$modelId = $model->id;
|
||||
|
||||
// Check if this user is involved in any transaction where the other model is either the sender or receiver
|
||||
return Transaction::where(function ($query) use ($modelClass, $modelId) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) use ($modelClass, $modelId) {
|
||||
$q->where('accountable_type', $modelClass)
|
||||
->where('accountable_id', $modelId);
|
||||
});
|
||||
})
|
||||
->where(function ($query) {
|
||||
$query->whereHas('accountFrom.accountable', function ($q) {
|
||||
$q->where('accountable_type', User::class)
|
||||
->where('accountable_id', $this->id);
|
||||
})
|
||||
->orWhereHas('accountTo.accountable', function ($q) {
|
||||
$q->where('accountable_type', User::class)
|
||||
->where('accountable_id', $this->id);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message settings for this user
|
||||
*/
|
||||
public function messageSettings()
|
||||
{
|
||||
return $this->morphMany(MessageSetting::class, 'message_settingable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has accepted the principles.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAcceptedPrinciples(): bool
|
||||
{
|
||||
return !is_null($this->principles_terms_accepted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user needs to re-accept principles due to a newer version.
|
||||
* Compares the stored version with the current active principles post.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function needsToReacceptPrinciples(): bool
|
||||
{
|
||||
// First, check if there's a current principles post available
|
||||
// This must come BEFORE checking user acceptance status to avoid redirect loops
|
||||
$currentPost = Post::with(['translations' => function ($query) {
|
||||
$locale = app()->getLocale();
|
||||
$query->where('locale', 'like', $locale . '%')
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(1);
|
||||
}])
|
||||
->whereHas('category', function ($query) {
|
||||
$query->where('type', 'SiteContents\Static\Principles');
|
||||
})
|
||||
->first();
|
||||
|
||||
// Failsafe: If no current principles post exists, no need to accept/re-accept
|
||||
if (!$currentPost || !$currentPost->translations->first()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user hasn't accepted at all, they need to accept
|
||||
if (!$this->hasAcceptedPrinciples()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$currentTranslation = $currentPost->translations->first();
|
||||
$acceptedData = $this->principles_terms_accepted;
|
||||
|
||||
// Compare the updated_at timestamp
|
||||
if (isset($acceptedData['updated_at'])) {
|
||||
$acceptedUpdatedAt = \Carbon\Carbon::parse($acceptedData['updated_at']);
|
||||
$currentUpdatedAt = \Carbon\Carbon::parse($currentTranslation->updated_at);
|
||||
|
||||
return $currentUpdatedAt->isAfter($acceptedUpdatedAt);
|
||||
}
|
||||
|
||||
return true; // If no timestamp stored, require re-acceptance
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user