1507 lines
38 KiB
PHP
1507 lines
38 KiB
PHP
<?php
|
|
|
|
namespace App\Models\Contact;
|
|
|
|
use DateTime;
|
|
use App\Traits\Searchable;
|
|
use Illuminate\Support\Str;
|
|
use App\Helpers\LocaleHelper;
|
|
use App\Models\Account\Photo;
|
|
use App\Models\Journal\Entry;
|
|
use App\Helpers\StorageHelper;
|
|
use App\Helpers\WeatherHelper;
|
|
use Illuminate\Support\Carbon;
|
|
use App\Models\Account\Account;
|
|
use App\Models\Account\Weather;
|
|
use App\Models\Account\Activity;
|
|
use App\Models\Instance\AuditLog;
|
|
use function Safe\preg_match_all;
|
|
use Illuminate\Support\Collection;
|
|
use App\Models\Account\AddressBook;
|
|
use App\Models\Instance\SpecialDate;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use IlluminateAgnostic\Arr\Support\Arr;
|
|
use App\Models\Account\ActivityStatistic;
|
|
use App\Models\Relationship\Relationship;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use App\Models\ModelBindingHasher as Model;
|
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
|
|
|
/**
|
|
* @method static \Illuminate\Database\Eloquent\Builder search()
|
|
* @property \App\Models\Instance\SpecialDate|null $birthdate
|
|
*/
|
|
class Contact extends Model
|
|
{
|
|
use Searchable;
|
|
|
|
/** @var array<string> */
|
|
protected $dates = [
|
|
'last_talked_to',
|
|
'last_consulted_at',
|
|
'stay_in_touch_trigger_date',
|
|
'created_at',
|
|
'updated_at',
|
|
];
|
|
|
|
/**
|
|
* The list of columns we want the Searchable trait to use.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
protected $searchable_columns = [
|
|
'first_name',
|
|
'middle_name',
|
|
'last_name',
|
|
'nickname',
|
|
'description',
|
|
'job',
|
|
];
|
|
|
|
/**
|
|
* The list of columns we want the Searchable trait to select.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
protected $return_from_search = [
|
|
'id',
|
|
'first_name',
|
|
'middle_name',
|
|
'last_name',
|
|
'nickname',
|
|
'description',
|
|
'gender_id',
|
|
'account_id',
|
|
'created_at',
|
|
'updated_at',
|
|
'is_partial',
|
|
'is_starred',
|
|
'avatar_source',
|
|
'avatar_adorable_url',
|
|
'avatar_gravatar_url',
|
|
'avatar_default_url',
|
|
'avatar_photo_id',
|
|
'default_avatar_color',
|
|
];
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
protected $fillable = [
|
|
'first_name',
|
|
'middle_name',
|
|
'last_name',
|
|
'nickname',
|
|
'gender_id',
|
|
'description',
|
|
'account_id',
|
|
'is_partial',
|
|
'job',
|
|
'company',
|
|
'food_preferences',
|
|
'birthday_reminder_id',
|
|
'birthday_special_date_id',
|
|
'is_dead',
|
|
'last_consulted_at',
|
|
'created_at',
|
|
'first_met_additional_info',
|
|
];
|
|
|
|
/**
|
|
* The attributes that aren't mass assignable.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $guarded = ['id'];
|
|
|
|
/**
|
|
* Eager load account with every contact.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
protected $with = [
|
|
'account',
|
|
'avatarPhoto',
|
|
'gender',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be cast to native types.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $casts = [
|
|
'is_partial' => 'boolean',
|
|
'is_dead' => 'boolean',
|
|
'has_avatar' => 'boolean',
|
|
'is_starred' => 'boolean',
|
|
'is_active' => 'boolean',
|
|
'stay_in_touch_frequency' => 'integer',
|
|
];
|
|
|
|
/**
|
|
* The name order attribute that indicates how to format the name of the
|
|
* contact.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $nameOrder = 'firstname_lastname';
|
|
|
|
/**
|
|
* Get the user associated with the contact.
|
|
*
|
|
* @return BelongsTo
|
|
*/
|
|
public function account()
|
|
{
|
|
return $this->belongsTo(Account::class);
|
|
}
|
|
|
|
/**
|
|
* Get the address book associated with the contact.
|
|
*
|
|
* @return BelongsTo
|
|
*/
|
|
public function addressBook()
|
|
{
|
|
return $this->belongsTo(AddressBook::class);
|
|
}
|
|
|
|
/**
|
|
* Get the list of contacts from the same address book as this contact.
|
|
*
|
|
* @return HasMany<self>|null
|
|
*/
|
|
public function siblingContacts(): ?HasMany
|
|
{
|
|
if ($this->account) {
|
|
if ($this->addressBook) {
|
|
return $this->account->contacts($this->addressBook->name);
|
|
}
|
|
|
|
return $this->account->contacts();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the gender of the contact.
|
|
*
|
|
* @return BelongsTo
|
|
*/
|
|
public function gender()
|
|
{
|
|
return $this->belongsTo(Gender::class);
|
|
}
|
|
|
|
/**
|
|
* Get the activity records associated with the contact.
|
|
*
|
|
* @return BelongsToMany
|
|
*/
|
|
public function activities()
|
|
{
|
|
return $this->belongsToMany(Activity::class)->orderBy('happened_at', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the activity records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function activityStatistics()
|
|
{
|
|
return $this->hasMany(ActivityStatistic::class)->orderBy('year', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the debt records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function debts()
|
|
{
|
|
return $this->hasMany(Debt::class);
|
|
}
|
|
|
|
/**
|
|
* Get the gift records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function gifts()
|
|
{
|
|
return $this->hasMany(Gift::class);
|
|
}
|
|
|
|
/**
|
|
* Get the note records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function notes()
|
|
{
|
|
return $this->hasMany(Note::class);
|
|
}
|
|
|
|
/**
|
|
* Get the reminder records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function reminders()
|
|
{
|
|
return $this->hasMany(Reminder::class);
|
|
}
|
|
|
|
/**
|
|
* Get only the active reminder records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function activeReminders()
|
|
{
|
|
return $this->hasMany(Reminder::class)->active();
|
|
}
|
|
|
|
/**
|
|
* Get the task records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function tasks()
|
|
{
|
|
return $this->hasMany(Task::class);
|
|
}
|
|
|
|
/**
|
|
* Get the tags records associated with the contact.
|
|
*
|
|
* @return BelongsToMany
|
|
*/
|
|
public function tags()
|
|
{
|
|
return $this->belongsToMany(Tag::class)->withPivot('account_id')->withTimestamps();
|
|
}
|
|
|
|
/**
|
|
* Get the calls records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function calls()
|
|
{
|
|
return $this->hasMany(Call::class)->orderBy('called_at', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the entries records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function entries()
|
|
{
|
|
return $this->hasMany(Entry::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Relationships records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function relationships()
|
|
{
|
|
return $this->hasMany(Relationship::class, 'contact_is');
|
|
}
|
|
|
|
/**
|
|
* Get the Contact Field records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function contactFields()
|
|
{
|
|
return $this->hasMany(ContactField::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Address Field records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function addresses()
|
|
{
|
|
return $this->hasMany(Address::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Pets records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function pets()
|
|
{
|
|
return $this->hasMany(Pet::class);
|
|
}
|
|
|
|
/**
|
|
* Get the contact records associated with the account.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function specialDates()
|
|
{
|
|
return $this->hasMany(SpecialDate::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Special date represented the birthdate.
|
|
*
|
|
* @return HasOne
|
|
*/
|
|
public function birthdate()
|
|
{
|
|
return $this->hasOne(SpecialDate::class, 'id', 'birthday_special_date_id');
|
|
}
|
|
|
|
/**
|
|
* Get the Special date represented the deceased date.
|
|
*
|
|
* @return HasOne
|
|
*/
|
|
public function deceasedDate()
|
|
{
|
|
return $this->hasOne(SpecialDate::class, 'id', 'deceased_special_date_id');
|
|
}
|
|
|
|
/**
|
|
* Get the Special date represented the date first met.
|
|
*
|
|
* @return HasOne
|
|
*/
|
|
public function firstMetDate()
|
|
{
|
|
return $this->hasOne(SpecialDate::class, 'id', 'first_met_special_date_id');
|
|
}
|
|
|
|
/**
|
|
* Get the Conversation records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function conversations()
|
|
{
|
|
return $this->hasMany(Conversation::class)->orderBy('conversations.happened_at', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the Message records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function messages()
|
|
{
|
|
return $this->hasMany(Message::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Document records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function documents()
|
|
{
|
|
return $this->hasMany(Document::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Photo records associated with the contact.
|
|
*
|
|
* @return BelongsToMany
|
|
*/
|
|
public function photos()
|
|
{
|
|
return $this->belongsToMany(Photo::class)->withTimestamps();
|
|
}
|
|
|
|
/**
|
|
* Get the Life event records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function lifeEvents()
|
|
{
|
|
return $this->hasMany(LifeEvent::class)->orderBy('life_events.happened_at', 'desc');
|
|
}
|
|
|
|
/**
|
|
* Get the Occupation records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function occupations()
|
|
{
|
|
return $this->hasMany(Occupation::class);
|
|
}
|
|
|
|
/**
|
|
* Get the Avatar Photo records associated with the contact.
|
|
*
|
|
* @return HasOne
|
|
*/
|
|
public function avatarPhoto()
|
|
{
|
|
return $this->hasOne(Photo::class, 'id', 'avatar_photo_id');
|
|
}
|
|
|
|
/**
|
|
* Get the Audot log records associated with the contact.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function logs()
|
|
{
|
|
return $this->hasMany(AuditLog::class, 'about_contact_id', 'id');
|
|
}
|
|
|
|
/**
|
|
* Test if this is the 'me' contact.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isMe()
|
|
{
|
|
return $this->id == auth()->user()->me_contact_id;
|
|
}
|
|
|
|
/**
|
|
* Sort the contacts according a given criteria.
|
|
* @param Builder $builder
|
|
* @param string $criteria
|
|
* @return Builder
|
|
*/
|
|
public function scopeSortedBy(Builder $builder, string $criteria): Builder
|
|
{
|
|
switch ($criteria) {
|
|
case 'firstnameAZ':
|
|
return $builder->orderBy('first_name', 'asc');
|
|
case 'firstnameZA':
|
|
return $builder->orderBy('first_name', 'desc');
|
|
case 'lastnameAZ':
|
|
return $builder->orderBy('last_name', 'asc');
|
|
case 'lastnameZA':
|
|
return $builder->orderBy('last_name', 'desc');
|
|
case 'lastactivitydateNewtoOld':
|
|
return $this->sortedByLastActivity($builder, 'desc');
|
|
case 'lastactivitydateOldtoNew':
|
|
return $this->sortedByLastActivity($builder, 'asc');
|
|
default:
|
|
return $builder;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort the contacts using last activity.
|
|
* @param Builder $builder
|
|
* @param string $order
|
|
* @return Builder
|
|
*/
|
|
private function sortedByLastActivity(Builder $builder, string $order): Builder
|
|
{
|
|
$builder->leftJoin('activity_contact', 'contacts.id', '=', 'activity_contact.contact_id');
|
|
$builder->leftJoin('activities', 'activity_contact.activity_id', '=', 'activities.id');
|
|
$builder->groupBy('contacts.id');
|
|
$builder->orderBy('activities.happened_at', $order);
|
|
$builder->select(['*', 'contacts.id as id']);
|
|
|
|
return $builder;
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts who are not only a kid or a
|
|
* significant other without being a contact.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeReal($query)
|
|
{
|
|
return $query->where('is_partial', 0);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts who are active.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', 1);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts who are alive.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeAlive($query)
|
|
{
|
|
return $query->where('is_dead', 0);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts who are dead.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeDead($query)
|
|
{
|
|
return $query->where('is_dead', 1);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts who are not active.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeNotActive($query)
|
|
{
|
|
return $query->where('is_active', 0);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to include contacts whose notes contain the search phrase.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeNotes($query, int $accountId = null, string $needle)
|
|
{
|
|
return $query->orWhereHas('notes', function ($query) use ($accountId, $needle) {
|
|
return $query->where([
|
|
['account_id', $accountId],
|
|
['body', 'like', "%$needle%"],
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope a query to include contacts whose introduction notes contain the search phrase.
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeIntroductionAdditionalInformation($query, string $needle)
|
|
{
|
|
return $query->orWhere([
|
|
['first_met_additional_info', 'like', "%$needle%"],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include contacts from given address book.
|
|
* 'null' value for address book is the default address book.
|
|
*
|
|
* @param Builder $query
|
|
* @param int|null $accountId
|
|
* @param string|null $addressBookName
|
|
* @return Builder
|
|
*/
|
|
public function scopeAddressBook($query, int $accountId = null, string $addressBookName = null)
|
|
{
|
|
$addressBook = null;
|
|
if ($accountId && $addressBookName) {
|
|
$addressBook = AddressBook::where([
|
|
'account_id' => $accountId,
|
|
'name' => $addressBookName,
|
|
])->first();
|
|
}
|
|
|
|
return $query->where('address_book_id', $addressBook ? $addressBook->id : null);
|
|
}
|
|
|
|
/**
|
|
* Mutator first_name.
|
|
* Get the first name of the contact.
|
|
*
|
|
* @param string|null $value
|
|
*/
|
|
public function setFirstNameAttribute($value)
|
|
{
|
|
$this->attributes['first_name'] = trim($value);
|
|
}
|
|
|
|
/**
|
|
* Mutator last_name.
|
|
*
|
|
* It doesn't run ucfirst on purpose.
|
|
*
|
|
* @param string|null $value
|
|
*/
|
|
public function setLastNameAttribute($value)
|
|
{
|
|
$value = $value ? trim($value) : null;
|
|
$this->attributes['last_name'] = $value;
|
|
}
|
|
|
|
/**
|
|
* Set the name order attribute.
|
|
*
|
|
* @param string $value
|
|
* @return void
|
|
*/
|
|
public function nameOrder($value)
|
|
{
|
|
$this->nameOrder = $value;
|
|
}
|
|
|
|
/**
|
|
* Mutator last_name.
|
|
*
|
|
* @param string|null $value
|
|
*/
|
|
public function setNicknameAttribute($value)
|
|
{
|
|
$value = $value ? trim($value) : null;
|
|
$this->attributes['nickname'] = $value;
|
|
}
|
|
|
|
/**
|
|
* Get user's initials.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getInitialsAttribute()
|
|
{
|
|
$name = Str::ascii($this->name, LocaleHelper::getLang());
|
|
preg_match_all('/(?<=\s|^)[a-zA-Z0-9]/i', $name, $initials);
|
|
|
|
return implode('', $initials[0]);
|
|
}
|
|
|
|
/**
|
|
* Get the full name of the contact.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getNameAttribute()
|
|
{
|
|
$completeName = '';
|
|
|
|
if (Auth::check()) {
|
|
$this->nameOrder = auth()->user()->name_order;
|
|
}
|
|
|
|
switch ($this->nameOrder) {
|
|
case 'firstname_lastname':
|
|
$completeName = $this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
break;
|
|
case 'lastname_firstname':
|
|
$completeName = '';
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
$completeName .= ' '.$this->first_name;
|
|
break;
|
|
case 'firstname_lastname_nickname':
|
|
$completeName = $this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $completeName.' ('.$this->nickname.')';
|
|
}
|
|
break;
|
|
case 'firstname_nickname_lastname':
|
|
$completeName = $this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $completeName.' ('.$this->nickname.')';
|
|
}
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
|
|
break;
|
|
case 'lastname_firstname_nickname':
|
|
$completeName = '';
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $this->last_name;
|
|
}
|
|
|
|
$completeName = $completeName.' '.$this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $completeName.' ('.$this->nickname.')';
|
|
}
|
|
break;
|
|
case 'nickname_firstname_lastname':
|
|
$completeName = $this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $this->nickname.' ('.$completeName.')';
|
|
}
|
|
break;
|
|
case 'nickname_lastname_firstname':
|
|
$completeName = '';
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $this->last_name.' ';
|
|
}
|
|
|
|
$completeName = $completeName.$this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $this->nickname.' ('.$completeName.')';
|
|
}
|
|
break;
|
|
case 'lastname_nickname_firstname':
|
|
$completeName = '';
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $this->last_name;
|
|
}
|
|
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $completeName.' ('.$this->nickname.')';
|
|
}
|
|
|
|
$completeName = $completeName.' '.$this->first_name;
|
|
|
|
if (! is_null($this->middle_name)) {
|
|
$completeName = $completeName.' '.$this->middle_name;
|
|
}
|
|
break;
|
|
case 'nickname':
|
|
if (! is_null($this->nickname)) {
|
|
$completeName = $this->nickname;
|
|
}
|
|
|
|
if ($completeName == '') {
|
|
$completeName = $this->first_name;
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$completeName = $completeName.' '.$this->last_name;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ($this->is_dead) {
|
|
$completeName .= ' ⚰';
|
|
}
|
|
|
|
return trim($completeName);
|
|
}
|
|
|
|
/**
|
|
* Get the incomplete name of the contact, like `John D.`.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getIncompleteName()
|
|
{
|
|
$incompleteName = '';
|
|
$incompleteName = $this->first_name;
|
|
|
|
if (! is_null($this->last_name)) {
|
|
$incompleteName .= ' '.mb_substr($this->last_name, 0, 1);
|
|
}
|
|
|
|
if ($this->is_dead) {
|
|
$incompleteName .= ' ⚰';
|
|
}
|
|
|
|
return trim($incompleteName);
|
|
}
|
|
|
|
/**
|
|
* Get the initials of the contact, used for avatars.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getInitials()
|
|
{
|
|
return $this->initials;
|
|
}
|
|
|
|
/**
|
|
* Get the date of the last activity done by this contact.
|
|
*
|
|
* @return \DateTime|null
|
|
*/
|
|
public function getLastActivityDate(): ?DateTime
|
|
{
|
|
if ($this->activities->count() === 0) {
|
|
return null;
|
|
}
|
|
|
|
$lastActivity = $this->activities->sortByDesc('happened_at')->first();
|
|
|
|
return $lastActivity->happened_at;
|
|
}
|
|
|
|
/**
|
|
* Get all the contacts related to the current contact by a specific
|
|
* relationship type group.
|
|
*
|
|
* @param string $type
|
|
* @return Collection|null
|
|
*/
|
|
public function getRelationshipsByRelationshipTypeGroup(string $type): ?Collection
|
|
{
|
|
$relationshipTypeGroup = $this->account->getRelationshipTypeGroupByType($type);
|
|
|
|
if (! $relationshipTypeGroup) {
|
|
return null;
|
|
}
|
|
|
|
return $this->relationships->filter(function ($item) use ($type) {
|
|
return $item->relationshipType->relationshipTypeGroup->name == $type;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set the default avatar color for this object.
|
|
*
|
|
* @param string|null $color
|
|
* @return void
|
|
*/
|
|
public function setAvatarColor($color = null)
|
|
{
|
|
$colors = [
|
|
'#fdb660',
|
|
'#93521e',
|
|
'#bd5067',
|
|
'#b3d5fe',
|
|
'#ff9807',
|
|
'#709512',
|
|
'#5f479a',
|
|
'#e5e5cd',
|
|
];
|
|
|
|
$this->default_avatar_color = $color ?? $colors[mt_rand(0, count($colors) - 1)];
|
|
}
|
|
|
|
/**
|
|
* Set the name of the contact.
|
|
*
|
|
* @param string $firstName
|
|
* @param string $middleName
|
|
* @param string $lastName
|
|
* @return bool
|
|
*/
|
|
public function setName(string $firstName, string $lastName = null, string $middleName = null)
|
|
{
|
|
if ($firstName === '') {
|
|
return false;
|
|
}
|
|
|
|
$this->first_name = $firstName;
|
|
$this->middle_name = $middleName;
|
|
$this->last_name = $lastName;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns the state of the birthday.
|
|
* As it's a Special Date, the date can have several states. We need this
|
|
* info when we populate the Edit contact sheet.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getBirthdayState()
|
|
{
|
|
if (! $this->birthday_special_date_id) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if ($this->birthdate->is_age_based) {
|
|
return 'approximate';
|
|
}
|
|
|
|
// we know at least the day and month
|
|
if ($this->birthdate->is_year_unknown) {
|
|
return 'almost';
|
|
}
|
|
|
|
return 'exact';
|
|
}
|
|
|
|
/**
|
|
* Refresh statistics about activities.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function calculateActivitiesStatistics()
|
|
{
|
|
// Delete the Activities statistics table for this contact
|
|
$this->activityStatistics->each(function ($activityStatistic) {
|
|
$activityStatistic->delete();
|
|
});
|
|
|
|
// Create the statistics again
|
|
$this->activities->groupBy('happened_at.year')
|
|
->map(function (Collection $activities, $year) {
|
|
ActivityStatistic::create([
|
|
'account_id' => $this->account_id,
|
|
'contact_id' => $this->id,
|
|
'year' => $year,
|
|
'count' => $activities->count(),
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all the gifts offered, if any.
|
|
*/
|
|
public function getGiftsOffered()
|
|
{
|
|
return $this->gifts()->offered()->get();
|
|
}
|
|
|
|
/**
|
|
* Get all the gift ideas, if any.
|
|
*/
|
|
public function getGiftIdeas()
|
|
{
|
|
return $this->gifts()->isIdea()->get();
|
|
}
|
|
|
|
/**
|
|
* Get all the tasks in the in progress state, if any.
|
|
*/
|
|
public function getTasksInProgress()
|
|
{
|
|
return $this->tasks()->inProgress()->get();
|
|
}
|
|
|
|
/**
|
|
* Get all the tasks in the in completed state, if any.
|
|
*/
|
|
public function getCompletedTasks()
|
|
{
|
|
return $this->tasks()->completed()->get();
|
|
}
|
|
|
|
/**
|
|
* Get the default avatar URL.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAvatarDefaultURL()
|
|
{
|
|
if (empty($this->avatar_default_url)) {
|
|
return '';
|
|
}
|
|
|
|
if (config('filesystems.default_visibility') === 'public') {
|
|
$matches = Str::of($this->avatar_default_url)->split('/\?/');
|
|
|
|
$url = asset(StorageHelper::disk(config('filesystems.default'))->url($matches[0]));
|
|
if ($matches->count() > 1) {
|
|
$url .= '?'.$matches[1];
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
return route('storage', ['file' => $this->avatar_default_url]);
|
|
}
|
|
|
|
/**
|
|
* Get the adorable avatar URL.
|
|
*
|
|
* @param string|null $value
|
|
* @return string|null
|
|
*/
|
|
public function getAvatarAdorableUrlAttribute(?string $value): ?string
|
|
{
|
|
if (isset($value) && $value !== '') {
|
|
return Str::of($value)
|
|
->after('https://api.adorable.io/avatars/')
|
|
->ltrim('/')
|
|
->start(Str::finish(config('monica.adorable_api'), '/'));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the adorable avatar URL.
|
|
*
|
|
* @param string|null $value
|
|
* @return void
|
|
*/
|
|
public function setAvatarAdorableUrlAttribute(?string $value)
|
|
{
|
|
if (isset($value) && $value !== '') {
|
|
$value = Str::of($value)->replace(Str::finish(config('monica.adorable_api'), '/'), '');
|
|
}
|
|
$this->attributes['avatar_adorable_url'] = $value;
|
|
}
|
|
|
|
/**
|
|
* Returns the URL of the avatar, properly sized.
|
|
* The avatar can come from 4 sources:
|
|
* - default,
|
|
* - Adorable avatar,
|
|
* - Gravatar
|
|
* - or a photo that has been uploaded.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getAvatarURL()
|
|
{
|
|
$avatarURL = '';
|
|
|
|
switch ($this->avatar_source) {
|
|
case 'adorable':
|
|
$avatarURL = $this->avatar_adorable_url;
|
|
break;
|
|
case 'gravatar':
|
|
$avatarURL = $this->avatar_gravatar_url;
|
|
break;
|
|
case 'photo':
|
|
$avatarURL = $this->avatarPhoto()->first()->url();
|
|
break;
|
|
case 'default':
|
|
default:
|
|
$avatarURL = $this->getAvatarDefaultURL();
|
|
break;
|
|
}
|
|
|
|
return $avatarURL;
|
|
}
|
|
|
|
/**
|
|
* Delete avatars files.
|
|
* This does not touch avatar_location or avatar_file_name properties of the contact.
|
|
*/
|
|
public function deleteAvatars()
|
|
{
|
|
if (! $this->has_avatar || $this->avatar_location == 'external') {
|
|
return;
|
|
}
|
|
|
|
$storage = Storage::disk($this->avatar_location);
|
|
$this->deleteAvatarSize($storage);
|
|
$this->deleteAvatarSize($storage, 110);
|
|
$this->deleteAvatarSize($storage, 174);
|
|
}
|
|
|
|
/**
|
|
* Delete avatar file for one size.
|
|
*
|
|
* @param Filesystem $storage
|
|
* @param int $size
|
|
*/
|
|
private function deleteAvatarSize(Filesystem $storage, int $size = null)
|
|
{
|
|
$avatarFileName = $this->avatar_file_name;
|
|
|
|
if (! is_null($size)) {
|
|
$filename = pathinfo($avatarFileName, PATHINFO_FILENAME);
|
|
$extension = pathinfo($avatarFileName, PATHINFO_EXTENSION);
|
|
$avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension;
|
|
}
|
|
|
|
try {
|
|
if ($storage->exists($avatarFileName)) {
|
|
$storage->delete($avatarFileName);
|
|
}
|
|
} catch (FileNotFoundException $e) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the contact has debt (by the contact or the user for this contact).
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasDebt()
|
|
{
|
|
return $this->debts()->count() !== 0;
|
|
}
|
|
|
|
/**
|
|
* Get the list of tags as a string to populate the tags form.
|
|
*/
|
|
public function getTagsAsString()
|
|
{
|
|
return $this->tags->map(function ($tag) {
|
|
return $tag->name;
|
|
})->join(',');
|
|
}
|
|
|
|
/**
|
|
* Is this contact owed money?
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isOwedMoney()
|
|
{
|
|
return $this->totalOutstandingDebtAmount() > 0;
|
|
}
|
|
|
|
/**
|
|
* How much is the debt.
|
|
*
|
|
* @return int amount in storage value
|
|
*/
|
|
public function totalOutstandingDebtAmount(): int
|
|
{
|
|
return $this
|
|
->debts()
|
|
->inProgress()
|
|
->getResults()
|
|
->filter(function ($d) {
|
|
return Arr::has($d->attributes, 'amount');
|
|
})
|
|
->sum(function ($d) {
|
|
$amount = $d->attributes['amount'];
|
|
|
|
return $d->in_debt === 'yes' ? -$amount : $amount;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the contact has information about how they first met.
|
|
* @return bool
|
|
*/
|
|
public function hasFirstMetInformation()
|
|
{
|
|
return ! is_null($this->first_met_additional_info) || ! is_null($this->firstMetDate) || ! is_null($this->first_met_through_contact_id);
|
|
}
|
|
|
|
/**
|
|
* Gets the contact who introduced this person to the user.
|
|
*
|
|
* @return Contact|null
|
|
*/
|
|
public function getIntroducer(): ?self
|
|
{
|
|
if (! $this->first_met_through_contact_id) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
/** @var Contact $contact */
|
|
$contact = self::where('account_id', $this->account_id)
|
|
->findOrFail($this->first_met_through_contact_id);
|
|
} catch (ModelNotFoundException $e) {
|
|
return null;
|
|
}
|
|
|
|
return $contact;
|
|
}
|
|
|
|
/**
|
|
* Sets a Special Date for this contact, for a specific occasion (birthday,
|
|
* decease date,...) of which we know the date.
|
|
*
|
|
* @param string $occasion
|
|
* @param int $year
|
|
* @param int $month
|
|
* @param int $day
|
|
* @return SpecialDate|null
|
|
*/
|
|
public function setSpecialDate($occasion, int $year, int $month, int $day): ?SpecialDate
|
|
{
|
|
if (empty($occasion)) {
|
|
return null;
|
|
}
|
|
|
|
$specialDate = new SpecialDate;
|
|
$specialDate->setToContact($this)->createFromDate($year, $month, $day);
|
|
|
|
switch ($occasion) {
|
|
case 'birthdate':
|
|
$this->birthday_special_date_id = $specialDate->id;
|
|
break;
|
|
case 'deceased_date':
|
|
$this->deceased_special_date_id = $specialDate->id;
|
|
break;
|
|
case 'first_met':
|
|
$this->first_met_special_date_id = $specialDate->id;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return $specialDate;
|
|
}
|
|
|
|
/**
|
|
* Sets a Special Date for this contact, for a specific occasion (birthday,
|
|
* decease date,...) of which we know only the age (meaning it's going to
|
|
* be approximate).
|
|
*/
|
|
public function setSpecialDateFromAge($occasion, int $age)
|
|
{
|
|
if (is_null($occasion)) {
|
|
return;
|
|
}
|
|
|
|
$specialDate = new SpecialDate;
|
|
$specialDate->setToContact($this)->createFromAge($age);
|
|
|
|
switch ($occasion) {
|
|
case 'birthdate':
|
|
$this->birthday_special_date_id = $specialDate->id;
|
|
break;
|
|
case 'deceased_date':
|
|
$this->deceased_special_date_id = $specialDate->id;
|
|
break;
|
|
case 'first_met':
|
|
$this->first_met_special_date_id = $specialDate->id;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return $specialDate;
|
|
}
|
|
|
|
/**
|
|
* Get all the reminders regarding the birthdays of the contacts who have a
|
|
* relationships with the current contact.
|
|
*
|
|
* @return Collection
|
|
*/
|
|
public function getBirthdayRemindersAboutRelatedContacts()
|
|
{
|
|
$relationships = $this->relationships->filter(function ($item) {
|
|
return ! is_null($item->ofContact) &&
|
|
! is_null($item->ofContact->birthday_special_date_id);
|
|
});
|
|
|
|
$reminders = collect();
|
|
foreach ($relationships as $relationship) {
|
|
$reminder = Reminder::where('account_id', $this->account_id)
|
|
->find($relationship->ofContact->birthday_reminder_id);
|
|
|
|
if ($reminder) {
|
|
$reminders->push($reminder);
|
|
}
|
|
}
|
|
|
|
return $reminders;
|
|
}
|
|
|
|
/**
|
|
* Gets the first contact related to this contact if the current contact is
|
|
* partial.
|
|
*
|
|
* @return self|null
|
|
*/
|
|
public function getRelatedRealContact()
|
|
{
|
|
$contact = $this;
|
|
|
|
return self::setEagerLoads([])->where('account_id', $this->account_id)
|
|
->where('id', function ($query) use ($contact) {
|
|
$query->select('of_contact')
|
|
->from('relationships')
|
|
->where([
|
|
'account_id' => $contact->account_id,
|
|
'contact_is' => $contact->id,
|
|
])
|
|
->first();
|
|
})
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Get the link to this contact, or the related real contact.
|
|
* @return string
|
|
*/
|
|
public function getLink()
|
|
{
|
|
$contact = $this->is_partial ? $this->getRelatedRealContact() : $this;
|
|
if (is_null($contact)) {
|
|
$contact = $this;
|
|
}
|
|
|
|
return route('people.show', $contact);
|
|
}
|
|
|
|
/**
|
|
* Get the contacts that have all the provided $tags
|
|
* or if $tags is NONE get contacts that have no tags.
|
|
* @param Builder $query
|
|
* @param mixed $tags string or Tag
|
|
* @return Builder $query
|
|
*/
|
|
public function scopeTags($query, $tags)
|
|
{
|
|
if ($tags == 'NONE') {
|
|
// get tagless contacts
|
|
$query = $query->has('tags', '<', 1);
|
|
} elseif (! empty($tags)) {
|
|
// gets users who have all the tags
|
|
foreach ($tags as $tag) {
|
|
$query = $query->whereHas('tags', function (Builder $query) use ($tag) {
|
|
$query->where('id', $tag->id);
|
|
});
|
|
}
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Indicates the age of the contact at death.
|
|
*
|
|
* @return int|null
|
|
*/
|
|
public function getAgeAtDeath(): ?int
|
|
{
|
|
if (! $this->deceasedDate) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->deceasedDate->is_year_unknown == 1) {
|
|
return null;
|
|
}
|
|
|
|
if (! $this->birthdate) {
|
|
return null;
|
|
}
|
|
|
|
return $this->birthdate->date->diffInYears($this->deceasedDate->date);
|
|
}
|
|
|
|
/**
|
|
* Update the frequency for which user has to be warned to stay in touch
|
|
* with the contact.
|
|
*
|
|
* @param int $frequency
|
|
* @return bool
|
|
*/
|
|
public function updateStayInTouchFrequency($frequency)
|
|
{
|
|
if (! is_int($frequency)) {
|
|
return false;
|
|
}
|
|
|
|
$this->stay_in_touch_frequency = $frequency;
|
|
|
|
if ($frequency == 0) {
|
|
$this->stay_in_touch_frequency = null;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update the date the notification about staying in touch should be sent.
|
|
*
|
|
* @param int $frequency
|
|
* @param Carbon|null $triggerDate
|
|
*/
|
|
public function setStayInTouchTriggerDate($frequency, $triggerDate = null)
|
|
{
|
|
// prevent timestamp update
|
|
$timestamps = $this->timestamps;
|
|
$this->timestamps = false;
|
|
|
|
if ($frequency == 0) {
|
|
$this->stay_in_touch_trigger_date = null;
|
|
} else {
|
|
$triggerDate = $triggerDate ?? now();
|
|
$newTriggerDate = $triggerDate->addDays($frequency);
|
|
$this->stay_in_touch_trigger_date = $newTriggerDate;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
$this->timestamps = $timestamps;
|
|
}
|
|
|
|
/**
|
|
* Get the weather information for this contact, based on the first address
|
|
* on the profile.
|
|
*
|
|
* @return Weather
|
|
*/
|
|
public function getWeather()
|
|
{
|
|
return WeatherHelper::getWeatherForAddress($this->addresses()->first());
|
|
}
|
|
|
|
public function updateConsulted()
|
|
{
|
|
// prevent timestamp update
|
|
$timestamps = $this->timestamps;
|
|
$this->timestamps = false;
|
|
|
|
$this->last_consulted_at = now();
|
|
$this->number_of_views = $this->number_of_views + 1;
|
|
|
|
$this->save();
|
|
|
|
$this->timestamps = $timestamps;
|
|
}
|
|
}
|