refactor: replace custom CRM with Monica fork
Some checks failed
Build & Push Monica Image to Gitea Registry / build-and-push (push) Failing after 9s

This commit is contained in:
Kroonk
2026-05-22 16:19:55 +02:00
parent 38cc4c09ca
commit a213a1dba0
2215 changed files with 242567 additions and 6015 deletions

View File

@@ -0,0 +1,751 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\User\User;
use App\Models\Contact\Tag;
use App\Models\Journal\Day;
use App\Models\User\Module;
use Illuminate\Support\Str;
use App\Models\Contact\Call;
use App\Models\Contact\Debt;
use App\Models\Contact\Gift;
use App\Models\Contact\Note;
use App\Models\Contact\Task;
use App\Traits\Subscription;
use App\Models\Journal\Entry;
use App\Models\Contact\Gender;
use App\Models\Contact\Address;
use App\Models\Contact\Contact;
use App\Models\Contact\Message;
use App\Models\Contact\Document;
use App\Models\Contact\Reminder;
use App\Models\Contact\LifeEvent;
use App\Models\Instance\AuditLog;
use App\Services\User\CreateUser;
use App\Models\Contact\Occupation;
use Illuminate\Support\Facades\DB;
use App\Models\Contact\ContactField;
use App\Models\Contact\Conversation;
use App\Models\Contact\ReminderRule;
use App\Models\Instance\SpecialDate;
use App\Models\Journal\JournalEntry;
use App\Models\Contact\LifeEventType;
use App\Models\Contact\ReminderOutbox;
use Illuminate\Database\Eloquent\Model;
use App\Models\Contact\ContactFieldType;
use App\Models\Contact\LifeEventCategory;
use App\Models\Relationship\Relationship;
use App\Models\Relationship\RelationshipType;
use App\Models\Relationship\RelationshipTypeGroup;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Services\Auth\Population\PopulateModulesTable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Services\Auth\Population\PopulateLifeEventsTable;
use App\Services\Auth\Population\PopulateContactFieldTypesTable;
/**
* @property int $reminders_count
* @property int $notes_count
* @property int $activities_count
* @property int $gifts_count
* @property int $tasks_count
*/
class Account extends Model
{
use Subscription, HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'number_of_invitations_sent',
'api_key',
'default_time_reminder_is_sent',
'default_gender_id',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'has_access_to_paid_version_for_free' => 'boolean',
];
/**
* Get the activity records associated with the account.
*
* @return HasMany
*/
public function activities()
{
return $this->hasMany(Activity::class);
}
/**
* Get the contact records associated with the account.
*
* @return HasMany
*/
public function allContacts()
{
return $this->hasMany(Contact::class);
}
/**
* Get the addressBook's contacts.
*
* @param string|null $addressBookName
* @return HasMany<Contact>
*/
public function contacts(string $addressBookName = null)
{
$contacts = $this->allContacts();
return $addressBookName
? $contacts->addressBook($this->id, $addressBookName)
: $contacts->addressBook();
}
/**
* Get the invitations associated with the account.
*
* @return HasMany
*/
public function invitations()
{
return $this->hasMany(Invitation::class);
}
/**
* Get the debt records associated with the account.
*
* @return HasMany
*/
public function debts()
{
return $this->hasMany(Debt::class);
}
/**
* Get the gift records associated with the account.
*
* @return HasMany
*/
public function gifts()
{
return $this->hasMany(Gift::class);
}
/**
* Get the note records associated with the account.
*
* @return HasMany
*/
public function notes()
{
return $this->hasMany(Note::class)->orderBy('created_at', 'desc');
}
/**
* Get the reminder records associated with the account.
*
* @return HasMany
*/
public function reminders()
{
return $this->hasMany(Reminder::class);
}
/**
* Get the reminder outboxes records associated with the account.
*
* @return HasMany
*/
public function reminderOutboxes()
{
return $this->hasMany(ReminderOutbox::class);
}
/**
* Get the task records associated with the account.
*
* @return HasMany
*/
public function tasks()
{
return $this->hasMany(Task::class);
}
/**
* Get the user records associated with the account.
*
* @return HasMany
*/
public function users()
{
return $this->hasMany(User::class);
}
/**
* Get the relationship records associated with the account.
*
* @return HasMany
*/
public function relationships()
{
return $this->hasMany(Relationship::class);
}
/**
* Get the activity statistics record associated with the account.
*
* @return HasMany
*/
public function activityStatistics()
{
return $this->hasMany(ActivityStatistic::class);
}
/**
* Get the activity type records associated with the account.
*
* @return HasMany
*/
public function activityTypes()
{
return $this->hasMany(ActivityType::class);
}
/**
* Get the activity type category records associated with the account.
*
* @return HasMany
*/
public function activityTypeCategories()
{
return $this->hasMany(ActivityTypeCategory::class);
}
/**
* Get the task records associated with the account.
*
* @return HasMany
*/
public function entries()
{
return $this->hasMany(Entry::class);
}
/**
* Get the import jobs records associated with the account.
*
* @return HasMany
*/
public function importjobs()
{
return $this->hasMany(ImportJob::class)->orderBy('created_at', 'desc');
}
/**
* Get the import job reports records associated with the account.
*
* @return HasMany
*/
public function importJobReports()
{
return $this->hasMany(ImportJobReport::class);
}
/**
* Get the tags records associated with the account.
*
* @return HasMany
*/
public function tags()
{
return $this->hasMany(Tag::class)->orderBy('name', 'asc');
}
/**
* Get the calls records associated with the account.
*
* @return HasMany
*/
public function calls()
{
return $this->hasMany(Call::class)->orderBy('called_at', 'desc');
}
/**
* Get the Contact Field types records associated with the account.
*
* @return HasMany
*/
public function contactFieldTypes()
{
return $this->hasMany(ContactFieldType::class);
}
/**
* Get the Contact Field records associated with the contact.
*
* @return HasMany
*/
public function contactFields()
{
return $this->hasMany(ContactField::class);
}
/**
* Get the Journal Entries records associated with the account.
*
* @return HasMany
*/
public function journalEntries()
{
return $this->hasMany(JournalEntry::class)->orderBy('date', 'desc');
}
/**
* Get the special dates records associated with the account.
*
* @return HasMany
*/
public function specialDates()
{
return $this->hasMany(SpecialDate::class);
}
/**
* Get the Days records associated with the account.
*
* @return HasMany
*/
public function days()
{
return $this->hasMany(Day::class);
}
/**
* Get the Genders records associated with the account.
*
* @return HasMany
*/
public function genders()
{
return $this->hasMany(Gender::class);
}
/**
* Get the Reminder Rules records associated with the account.
*
* @return HasMany
*/
public function reminderRules()
{
return $this->hasMany(ReminderRule::class);
}
/**
* Get the relationship types records associated with the account.
*
* @return HasMany
*/
public function relationshipTypes()
{
return $this->hasMany(RelationshipType::class);
}
/**
* Get the relationship type groups records associated with the account.
*
* @return HasMany
*/
public function relationshipTypeGroups()
{
return $this->hasMany(RelationshipTypeGroup::class);
}
/**
* Get the modules records associated with the account.
*
* @return HasMany
*/
public function modules()
{
return $this->hasMany(Module::class);
}
/**
* Get the Conversation records associated with the account.
*
* @return HasMany
*/
public function conversations()
{
return $this->hasMany(Conversation::class);
}
/**
* Get the Message records associated with the account.
*
* @return HasMany
*/
public function messages()
{
return $this->hasMany(Message::class);
}
/**
* Get the Document records associated with the account.
*
* @return HasMany
*/
public function documents()
{
return $this->hasMany(Document::class);
}
/**
* Get the Life Event Category records associated with the account.
*
* @return HasMany
*/
public function lifeEventCategories()
{
return $this->hasMany(LifeEventCategory::class);
}
/**
* Get the Life Event Type records associated with the account.
*
* @return HasMany
*/
public function lifeEventTypes()
{
return $this->hasMany(LifeEventType::class);
}
/**
* Get the Life Event records associated with the account.
*
* @return HasMany
*/
public function lifeEvents()
{
return $this->hasMany(LifeEvent::class);
}
/**
* Get the Photos records associated with the account.
*
* @return HasMany
*/
public function photos()
{
return $this->hasMany(Photo::class);
}
/**
* Get the Weather records associated with the account.
*
* @return HasMany
*/
public function weathers()
{
return $this->hasMany(Weather::class);
}
/**
* Get the Places records associated with the account.
*
* @return HasMany
*/
public function places()
{
return $this->hasMany(Place::class);
}
/**
* Get the Addresses records associated with the account.
*
* @return HasMany
*/
public function addresses()
{
return $this->hasMany(Address::class);
}
/**
* Get the Address Books records associated with the account.
*
* @return HasMany
*/
public function addressBooks()
{
return $this->hasMany(AddressBook::class);
}
/**
* Get the Address Book Subscriptions records associated with the account.
*
* @return HasMany
*/
public function addressBookSubscriptions()
{
return $this->hasMany(AddressBookSubscription::class);
}
/**
* Get the Company records associated with the account.
*
* @return HasMany
*/
public function companies()
{
return $this->hasMany(Company::class);
}
/**
* Get the Occupation records associated with the account.
*
* @return HasMany
*/
public function occupations()
{
return $this->hasMany(Occupation::class);
}
/**
* * Get the Audit log records associated with the account.
*
* @return HasMany
*/
public function auditLogs()
{
return $this->hasMany(AuditLog::class);
}
/**
* Populates the Activity Type table right after an account is
* created.
*/
public function populateActivityTypeTable()
{
$defaultActivityTypeCategories = DB::table('default_activity_type_categories')->get();
foreach ($defaultActivityTypeCategories as $defaultActivityTypeCategory) {
$activityTypeCategoryId = DB::table('activity_type_categories')->insertGetId([
'account_id' => $this->id,
'translation_key' => $defaultActivityTypeCategory->translation_key,
]);
$defaultActivityTypes = DB::table('default_activity_types')
->where('default_activity_type_category_id', $defaultActivityTypeCategory->id)
->get();
foreach ($defaultActivityTypes as $defaultActivityType) {
DB::table('activity_types')->insert([
'account_id' => $this->id,
'activity_type_category_id' => $activityTypeCategoryId,
'translation_key' => $defaultActivityType->translation_key,
]);
}
}
}
/**
* Populates the default genders in a new account.
*
* @return void
*/
public function populateDefaultGendersTable()
{
Gender::create(['type' => Gender::MALE, 'name' => trans('app.gender_male'), 'account_id' => $this->id]);
Gender::create(['type' => Gender::FEMALE, 'name' => trans('app.gender_female'), 'account_id' => $this->id]);
Gender::create(['type' => Gender::OTHER, 'name' => trans('app.gender_none'), 'account_id' => $this->id]);
}
/**
* Populates the default reminder rules in a new account.
*
* @return void
*/
public function populateDefaultReminderRulesTable()
{
ReminderRule::create(['number_of_days_before' => 7, 'account_id' => $this->id, 'active' => 1]);
ReminderRule::create(['number_of_days_before' => 30, 'account_id' => $this->id, 'active' => 1]);
}
/**
* Populates the default relationship types in a new account.
*
* @return void
*/
public function populateRelationshipTypeGroupsTable($ignoreTableAlreadyMigrated = false)
{
$defaultRelationshipTypeGroups = DB::table('default_relationship_type_groups')->get();
foreach ($defaultRelationshipTypeGroups as $defaultRelationshipTypeGroup) {
if (! $ignoreTableAlreadyMigrated || $defaultRelationshipTypeGroup->migrated == 0) {
DB::table('relationship_type_groups')->insert([
'account_id' => $this->id,
'name' => $defaultRelationshipTypeGroup->name,
'delible' => $defaultRelationshipTypeGroup->delible,
]);
}
}
}
/**
* Populate the relationship types table based on the default ones.
*
* @return void
*/
public function populateRelationshipTypesTable($migrateOnlyNewTypes = false)
{
if ($migrateOnlyNewTypes) {
$defaultRelationshipTypes = DB::table('default_relationship_types')->where('migrated', 0)->get();
} else {
$defaultRelationshipTypes = DB::table('default_relationship_types')->get();
}
foreach ($defaultRelationshipTypes as $defaultRelationshipType) {
$defaultRelationshipTypeGroup = DB::table('default_relationship_type_groups')
->where('id', $defaultRelationshipType->relationship_type_group_id)
->first();
$relationshipTypeGroup = $this->getRelationshipTypeGroupByType($defaultRelationshipTypeGroup->name);
if ($relationshipTypeGroup) {
RelationshipType::create([
'account_id' => $this->id,
'name' => $defaultRelationshipType->name,
'name_reverse_relationship' => $defaultRelationshipType->name_reverse_relationship,
'relationship_type_group_id' => $relationshipTypeGroup->id,
'delible' => $defaultRelationshipType->delible,
]);
}
}
}
/**
* Create a new account and associate a new User.
*
* @param string $first_name
* @param string $last_name
* @param string $email
* @param string $password
* @param string $ipAddress
* @return self
*/
public static function createDefault($first_name, $last_name, $email, $password, $ipAddress = null, $lang = null)
{
// create new account
$account = new self;
$account->api_key = Str::random(30);
$account->created_at = now();
$account->save();
try {
// create the first user for this account
$user = app(CreateUser::class)->execute([
'account_id' => $account->id,
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $email,
'password' => $password,
'locale' => $lang,
'ip_address' => $ipAddress,
]);
} catch (\Exception $e) {
$account->delete();
throw $e;
}
$account->populateDefaultFields();
return $account;
}
/**
* Populates all the default column that should be there when a new account
* is created or reset.
*/
public function populateDefaultFields()
{
app(PopulateContactFieldTypesTable::class)->execute([
'account_id' => $this->id,
'migrate_existing_data' => true,
]);
$this->populateDefaultGendersTable();
$this->populateDefaultReminderRulesTable();
$this->populateRelationshipTypeGroupsTable();
$this->populateRelationshipTypesTable();
$this->populateActivityTypeTable();
app(PopulateLifeEventsTable::class)->execute([
'account_id' => $this->id,
'migrate_existing_data' => true,
]);
app(PopulateModulesTable::class)->execute([
'account_id' => $this->id,
'migrate_existing_data' => true,
]);
}
/**
* Gets the RelationshipType object matching the given type.
*
* @param string $relationshipTypeName
* @return RelationshipType|null
*/
public function getRelationshipTypeByType(string $relationshipTypeName)
{
return $this->relationshipTypes->where('name', $relationshipTypeName)->first();
}
/**
* Gets the RelationshipType object matching the given type.
*
* @param string $relationshipTypeGroupName
* @return RelationshipTypeGroup|null
*/
public function getRelationshipTypeGroupByType(string $relationshipTypeGroupName)
{
return $this->relationshipTypeGroups->where('name', $relationshipTypeGroupName)->first();
}
/**
* Get the first available locale in an account. This gets the first user
* in the account and reads his locale.
*
* @return string|null
*
* @throws ModelNotFoundException
*/
public function getFirstLocale(): ?string
{
try {
$user = $this->users()->firstOrFail();
} catch (ModelNotFoundException $e) {
return null;
}
return $user->locale;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Helpers\DateHelper;
use App\Traits\Journalable;
use App\Models\Contact\Contact;
use App\Models\Journal\JournalEntry;
use Illuminate\Database\Eloquent\Model;
use App\Models\Instance\Emotion\Emotion;
use App\Interfaces\IsJournalableInterface;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use App\Http\Resources\Contact\ContactShort as ContactShortResource;
/**
* @property int|null $activity_type_id
*/
class Activity extends Model implements IsJournalableInterface
{
use Journalable, HasUuid;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'activities';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['happened_at'];
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = [
'account',
'type',
'contacts',
];
/**
* Get the account record associated with the activity.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the activity.
*
* @return BelongsToMany
*/
public function contacts()
{
return $this->belongsToMany(Contact::class);
}
/**
* Get the activity type record associated with the activity.
*
* @return BelongsTo
*/
public function type()
{
return $this->belongsTo(ActivityType::class, 'activity_type_id');
}
/**
* Get all of the activities journal entries.
*/
public function journalEntries()
{
return $this->morphMany(JournalEntry::class, 'journalable');
}
/**
* Get the emotion records associated with the activity.
*
* @return BelongsToMany
*/
public function emotions()
{
return $this->belongsToMany(Emotion::class, 'emotion_activity', 'activity_id', 'emotion_id')
->withPivot('account_id')
->withTimestamps();
}
/**
* Get the summary for this activity.
*
* @return string or null
*/
public function getSummary()
{
return $this->summary;
}
/**
* Get the key of the title of the activity.
*
* @return string or null
*/
public function getTitle()
{
return $this->type ? $this->type->translation_key : null;
}
/**
* Get all the contacts this activity is associated with.
*/
public function getContactsForAPI()
{
$attendees = $this->contacts->filter(function ($contact) {
// This should not be possible!
return $contact->account_id === $this->account_id;
});
return ContactShortResource::collection($attendees);
}
/**
* Gets the information about the activity for the journal.
*
* @return array
*/
public function getInfoForJournalEntry()
{
return [
'type' => 'activity',
'id' => $this->id,
'activity_type' => (! is_null($this->type) ? $this->type->name : null),
'summary' => $this->summary,
'description' => $this->description,
'day' => $this->happened_at->day,
'day_name' => mb_convert_case(DateHelper::getShortDay($this->happened_at), MB_CASE_TITLE, 'UTF-8'),
'month' => $this->happened_at->month,
'month_name' => mb_convert_case(DateHelper::getShortMonth($this->happened_at), MB_CASE_UPPER, 'UTF-8'),
'year' => $this->happened_at->year,
'attendees' => $this->getContactsForAPI(),
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models\Account;
use App\Models\Contact\Contact;
use Illuminate\Database\Eloquent\Model;
class ActivityStatistic extends Model
{
protected $table = 'activity_statistics';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'contact_id',
'year',
'count',
];
/**
* Get the account record associated with the activity statistic.
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the activity statistic.
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivityType extends Model
{
use HasUuid;
protected $table = 'activity_types';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'activity_type_category_id',
'account_id',
'translation_key',
];
/**
* Get the account record associated with the activity type.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the activity type category record associated with the activity types.
*
* @return BelongsTo
*/
public function category()
{
return $this->belongsTo(ActivityTypeCategory::class, 'activity_type_category_id');
}
/**
* Get the activity records associated with the activity type.
*
* @return HasMany
*/
public function activities()
{
return $this->hasMany(Activity::class);
}
/**
* Get the activity type's attribute.
*/
public function getNameAttribute($value)
{
if ($this->translation_key && ! $value) {
return trans('people.activity_type_'.$this->translation_key);
}
return $value;
}
/**
* Reset all associated activities with this category type.
*
* @return void
*/
public function resetAssociationWithActivities()
{
foreach ($this->activities as $activity) {
$activity->activity_type_id = null;
$activity->save();
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivityTypeCategory extends Model
{
use HasUuid;
protected $table = 'activity_type_categories';
protected $appends = ['name'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'translation_key',
'account_id',
];
/**
* Get the account record associated with the activity type group.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the activity type records associated with the category.
*
* @return HasMany
*/
public function activityTypes()
{
return $this->hasMany(ActivityType::class);
}
/**
* Get the activity type category's attribute.
*
* @return string
* @psalm-suppress InvalidReturnStatement
*/
public function getNameAttribute($value)
{
if ($this->translation_key && ! $value) {
return trans('people.activity_type_category_'.$this->translation_key);
}
return $value;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\User\User;
use App\Models\Contact\Contact;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AddressBook extends Model
{
use HasFactory, HasUuid;
protected $table = 'addressbooks';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'user_id',
'name',
'description',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
];
/**
* Get the account record associated with the address book.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the address book.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get all contacts for this address book.
*
* @return HasMany
*/
public function contacts()
{
return $this->hasMany(Contact::class);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\User\User;
use function safe\json_decode;
use function safe\json_encode;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Builder;
use App\Services\DavClient\Utils\Dav\DavClient;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AddressBookSubscription extends Model
{
use HasFactory, HasUuid;
protected $table = 'addressbook_subscriptions';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'user_id',
'address_book_id',
'name',
'uri',
'capabilities',
'username',
'password',
'readonly',
'syncToken',
'localSyncToken',
'frequency',
'last_synchronized_at',
'active',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'last_synchronized_at',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'readonly' => 'boolean',
'active' => 'boolean',
'localSyncToken' => 'integer',
];
/**
* Eager load account with every contact.
*
* @var array<string>
*/
protected $with = [
'user',
];
/**
* Get the account record associated with the subscription.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the subscription.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the addressbook record associated with the subscription.
*
* @return BelongsTo
*/
public function addressBook()
{
return $this->belongsTo(AddressBook::class);
}
/**
* Get capabilities.
*
* @param string $value
* @return array
*/
public function getCapabilitiesAttribute($value)
{
return json_decode($value, true);
}
/**
* Set capabilities.
*
* @param string $value
* @return void
*/
public function setCapabilitiesAttribute($value)
{
$this->attributes['capabilities'] = json_encode($value);
}
/**
* Get password.
*
* @param string $value
* @return string
*/
public function getPasswordAttribute($value)
{
return decrypt($value);
}
/**
* Set password.
*
* @param string $value
* @return void
*/
public function setPasswordAttribute($value)
{
$this->attributes['password'] = encrypt($value);
}
/**
* Scope a query to only include active subscriptions.
*
* @param Builder $query
* @return Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
/**
* Get a new client.
*
* @return DavClient
*/
public function getClient(): DavClient
{
return app(DavClient::class)
->setBaseUri($this->uri)
->setCredentials($this->username, $this->password);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models\Account;
use Illuminate\Database\Eloquent\Model;
class ApiUsage extends Model
{
protected $table = 'api_usage';
/**
* Log a request made through the API.
*/
public function log(\Illuminate\Http\Request $request)
{
$this->url = $request->fullUrl();
$this->method = $request->getMethod();
$this->client_ip = $request->getClientIp();
$this->save();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Models\Account;
use App\Models\Contact\Occupation;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Company extends Model
{
protected $table = 'companies';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'weather_json',
'account_id',
'name',
'website',
'number_of_employees',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the company.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the Occupation records associated with the contact.
*
* @return HasMany
*/
public function occupations()
{
return $this->hasMany(Occupation::class);
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Models\User\User;
use Illuminate\Database\Eloquent\Model;
use App\Notifications\ExportAccountDone;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class ExportJob extends Model
{
use HasUuid, HasFactory;
public const EXPORT_TODO = 'todo';
public const EXPORT_DOING = 'doing';
public const EXPORT_DONE = 'done';
public const EXPORT_FAILED = 'failed';
/**
* Export as SQL format.
*
* @var string
*/
public const SQL = 'sql';
/**
* Export as JSON format.
*
* @var string
*/
public const JSON = 'json';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'uuid',
'account_id',
'user_id',
'type',
'status',
'filesystem',
'filename',
'started_at',
'ended_at',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'started_at',
'ended_at',
];
/**
* Get the account record associated with the import job.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the import job.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Start the export job.
*
* @return void
*/
public function start(): void
{
$this->status = self::EXPORT_DOING;
$this->started_at = now();
$this->save();
}
/**
* End the export job.
*
* @return void
*/
public function end(): void
{
$this->status = self::EXPORT_DONE;
$this->ended_at = now();
$this->save();
$this->user->notify(new ExportAccountDone($this));
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace App\Models\Account;
use App\Models\User\User;
use Sabre\VObject\Reader;
use Illuminate\Support\Arr;
use App\Helpers\AccountHelper;
use Sabre\VObject\Component\VCard;
use App\Services\VCard\ImportVCard;
use League\Flysystem\UnableToReadFile;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToDeleteFile;
use Illuminate\Validation\ValidationException;
use Sabre\VObject\Splitter\VCard as VCardReader;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property Account $account
* @property int $account_id
* @property User $user
* @property int $user_id
* @property bool $failed
* @property string $failed_reason
* @property string $filename
* @property int $contacts_found
* @property int $contacts_skipped
* @property int $contacts_imported
* @property \Illuminate\Support\Carbon|null $started_at
* @property \Illuminate\Support\Carbon|null $ended_at
*/
class ImportJob extends Model
{
const VCARD_SKIPPED = true;
const VCARD_IMPORTED = false;
protected $table = 'import_jobs';
/**
* The physical vCard file on disk.
*
* @var resource
*/
public $physicalFile;
/**
* All individual entries in the vCard file.
*
* @var VCardReader
*/
public $entries = null;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['started_at', 'ended_at'];
/**
* Get the account record associated with the import job.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the import job.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the import jobs reports records associated with the account.
*
* @return HasMany
*/
public function importJobReports()
{
return $this->hasMany(ImportJobReport::class);
}
/**
* Process an import job.
*
* @return void
*/
public function process($behaviour = ImportVCard::BEHAVIOUR_ADD)
{
$this->initJob();
if (! $this->failed && $this->getPhysicalFile()) {
$this->getEntries();
$this->processEntries($behaviour);
}
$this->deletePhysicalFile();
if (! $this->failed) {
$this->endJob();
}
}
/**
* Perform preliminary steps to start the import job.
*
* @return void
*/
private function initJob(): void
{
if (AccountHelper::hasLimitations($this->account)) {
$this->fail(trans('auth.not_authorized'));
}
$this->started_at = now();
$this->contacts_imported = 0;
$this->contacts_skipped = 0;
$this->save();
}
/**
* Perform the steps to finalize the import job.
*
* @return void
*/
private function endJob(): void
{
$this->ended_at = now();
$this->save();
}
/**
* Mark the import job as failed.
*
* @param string $reason
* @return void
*/
private function fail(string $reason): void
{
$this->failed = true;
if (! $this->failed_reason) {
$this->failed_reason = $reason;
}
$this->endJob();
}
/**
* Get the physical file (the vCard file).
*
* @return bool
*/
private function getPhysicalFile(): bool
{
try {
$this->physicalFile = Storage::disk(config('filesystems.default'))->readStream($this->filename);
} catch (UnableToReadFile $exception) {
$this->fail(trans('settings.import_vcard_file_not_found'));
return false;
}
return true;
}
/**
* Delete the physical file from the disk.
*
* @return bool
*/
private function deletePhysicalFile(): bool
{
try {
if (Storage::disk(config('filesystems.default'))->delete($this->filename) === false) {
$this->fail(trans('settings.import_vcard_file_not_found'));
return false;
}
} catch (UnableToDeleteFile $exception) {
$this->fail(trans('settings.import_vcard_file_not_found'));
return false;
}
return true;
}
/**
* Get the number of matches in the vCard file.
*
* @return void
*/
private function getEntries()
{
if ($this->physicalFile !== null) {
$this->entries = new VCardReader($this->physicalFile, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES);
}
}
/**
* Process all entries contained in the vCard file.
*
* @param string $behaviour
* @return void
*/
private function processEntries($behaviour = ImportVCard::BEHAVIOUR_ADD)
{
while (true) {
try {
/** @var VCard|null */
$entry = $this->entries !== null ? $this->entries->getNext() : null;
if (! $entry) {
// file end
break;
}
$this->contacts_found++;
} catch (\Throwable $e) {
$this->skipEntry('?', (string) $e);
continue;
}
$this->processSingleEntry($entry, $behaviour);
}
if ($this->contacts_found == 0) {
$this->fail(trans('settings.import_vcard_file_no_entries'));
}
}
/**
* Process a single vCard entry.
*
* @param string|VCard $entry
* @param string $behaviour
* @return void
*/
private function processSingleEntry($entry, $behaviour = ImportVCard::BEHAVIOUR_ADD): void
{
try {
$result = app(ImportVCard::class)->execute([
'account_id' => $this->account_id,
'user_id' => $this->user_id,
'entry' => $entry,
'behaviour' => $behaviour,
]);
} catch (ValidationException $e) {
$this->fail(implode(',', $e->validator->errors()->all()));
return;
}
if (Arr::has($result, 'error') && ! empty($result['error'])) {
$this->skipEntry($result['name'], $result['reason']);
return;
}
$this->contacts_imported++;
$this->fileImportJobReport($result['name'], self::VCARD_IMPORTED);
}
/**
* Skip the current entry.
*
* @param string $name
* @param string $reason
* @return void
*/
private function skipEntry($name, $reason = null): void
{
$this->fileImportJobReport($name, self::VCARD_SKIPPED, $reason);
$this->contacts_skipped++;
}
/**
* File an import job report for the current entry.
*
* @param string $name
* @param bool $status
* @param string $reason
* @return void
*/
private function fileImportJobReport($name, $status, $reason = null): void
{
$importJobReport = new ImportJobReport;
$importJobReport->account_id = $this->account_id;
$importJobReport->user_id = $this->user_id;
$importJobReport->import_job_id = $this->id;
$importJobReport->contact_information = trim($name);
$importJobReport->skipped = $status;
$importJobReport->skip_reason = $reason;
$importJobReport->save();
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Models\Account;
use App\Models\User\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property Account $account
* @property int $account_id
* @property User $user
* @property int $user_id
* @property int $import_job_id
* @property string $contact_information
* @property bool $skipped
* @property string $skip_reason
*/
class ImportJobReport extends Model
{
protected $table = 'import_job_reports';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the import job report.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the user record associated with the import job report.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the import job record associated with the gift.
*
* @return BelongsTo
*/
public function importJob()
{
return $this->belongsTo(ImportJob::class);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models\Account;
use App\Models\User\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property Account $account
* @property User $invitedBy
* @property string $invitation_key
*/
class Invitation extends Model
{
use Notifiable;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the invitation.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the task.
*
* @return BelongsTo
*/
public function invitedBy()
{
return $this->belongsTo(User::class, 'invited_by_user_id');
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Models\Account;
use App\Traits\HasUuid;
use App\Helpers\StorageHelper;
use App\Models\Contact\Contact;
use App\Models\ModelBinding as Model;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class Photo extends Model
{
use HasUuid;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'photos';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the photo.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contacts record associated with the photo.
*
* @return BelongsToMany
*/
public function contacts()
{
return $this->belongsToMany(Contact::class)->withTimestamps();
}
/**
* Get the first contact record associated with the photo.
*
* @return Contact
*/
public function contact()
{
return $this->contacts->first();
}
/**
* Gets the full path of the photo.
*
* @return string
*/
public function url()
{
if (config('filesystems.default_visibility') === 'public') {
return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename));
}
return route('storage', ['file' => $this->new_filename]);
}
/**
* Gets the data-url format of the photo.
*
* @return string|null
*/
public function dataUrl(): ?string
{
try {
$url = $this->new_filename;
$file = StorageHelper::disk(config('filesystems.default'))->get($url);
return (string) Image::make($file)->encode('data-url');
} catch (FileNotFoundException $e) {
return null;
}
}
/**
* Delete the model from the database.
*
* @return bool|null
*/
public function delete()
{
try {
Storage::disk(config('filesystems.default'))
->delete($this->new_filename);
} catch (FileNotFoundException $e) {
// continue
}
return parent::delete();
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Models\Account;
use Illuminate\Support\Str;
use App\Models\Contact\Contact;
use App\Helpers\CountriesHelper;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* A Place is not the same as an address.
* An Address in Monica is a way of contacting the contact. An Address is linked
* to a Place. But Places can exist without the Address object.
* Places will be linked to activities, for instance.
*/
class Place extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'places';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the place.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the Weather record associated with the place.
*
* @return HasMany
*/
public function weathers()
{
return $this->hasMany(Weather::class);
}
/**
* Get the address as a sentence.
*
* @return string|null
*/
public function getAddressAsString(): ?string
{
$address = '';
if (! is_null($this->street)) {
$address = $this->street;
}
if (! is_null($this->city)) {
$address .= ' '.$this->city;
}
if (! is_null($this->province)) {
$address .= ' '.$this->province;
}
if (! is_null($this->postal_code)) {
$address .= ' '.$this->postal_code;
}
if (! is_null($this->country)) {
$address .= ' '.$this->getCountryName();
}
if (empty($address)) {
return null;
}
// trim extra whitespaces inside the address
return Str::of($address)->replaceMatches('/\s+/', ' ');
}
/**
* Get the country of the place.
*
* @return string|null
*/
public function getCountryName(): ?string
{
if ($this->country) {
return CountriesHelper::get($this->country);
}
return null;
}
/**
* Get an URL for Google Maps for the place.
*
* @return string
*/
public function getGoogleMapAddress()
{
$place = $this->getAddressAsString();
$place = urlencode($place);
return "https://www.google.com/maps/place/{$place}";
}
/**
* Get the Google Maps url for the latitude/longitude.
*
* @return string
*/
public function getGoogleMapsAddressWithLatitude()
{
return 'http://maps.google.com/maps?q='.$this->latitude.','.+$this->longitude;
}
}

View File

@@ -0,0 +1,245 @@
<?php
namespace App\Models\Account;
use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Weather extends Model
{
protected $table = 'weather';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'place_id',
'weather_json',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'weather_json' => 'array',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the weather data.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the place record associated with the weather data.
*
* @return BelongsTo
*/
public function place()
{
return $this->belongsTo(Place::class);
}
/**
* Get the weather code.
*
* @return string|null
*/
public function getSummaryCodeAttribute(): ?string
{
$json = $this->weather_json;
// currently.icon: Darksky version
if (! ($icon = Arr::get($json, 'currently.icon'))) {
if (($text = Arr::get($json, 'current.condition.text')) === 'Partly cloudy') {
$icon = ((bool) Arr::get($json, 'current.is_day')) ? 'partly-cloudy-day' : 'partly-cloudy-night';
} else {
$icon = (string) Str::of($text)->lower()->replace(' ', '-');
}
}
return $icon;
}
/**
* Get the weather summary.
*
* @return string|null
*/
public function getSummaryAttribute(): ?string
{
$summary_code = $this->summary_code;
if (empty($summary_code)) {
return null;
}
return (string) Str::of(trans('app.weather_'.$summary_code));
}
/**
* Get the weather location.
*
* @return string|null
*/
public function getLocationAttribute(): ?string
{
return Arr::get($this->weather_json, 'location.name');
}
/**
* Get the weather update date.
*
* @return Carbon
*/
public function getDateAttribute(): ?Carbon
{
if (($timestamp = Arr::get($this->weather_json, 'current.last_updated_epoch')) !== null) {
return Carbon::createFromTimestamp($timestamp);
}
return null;
}
/**
* Get the weather icon.
*
* @return string
*
* @codeCoverageIgnore
*/
public function getEmojiAttribute(): string
{
switch ($this->summary_code) {
case 'sunny':
case 'clear-day':
$string = '🌞';
break;
case 'clear':
case 'clear-night':
$string = '🌃';
break;
case 'light-drizzle':
case 'patchy-light-drizzle':
case 'patchy-light-rain':
case 'light-rain':
case 'moderate-rain-at-times':
case 'moderate-rain':
case 'patchy-rain-possible':
case 'heavy-rain-at-times':
case 'heavy-rain':
case 'light-freezing-rain':
case 'moderate-or-heavy-freezing-rain':
case 'light-sleet':
case 'moderate-or-heavy-rain-shower':
case 'light-rain-shower':
case 'torrential-rain-shower':
case 'rain':
$string = '🌧️';
break;
case 'snow':
case 'blowing-snow':
case 'patchy-light-snow':
case 'light-snow':
case 'patchy-moderate-snow':
case 'moderate-snow':
case 'patchy-heavy-snow':
case 'heavy-snow':
case 'light-snow-showers':
case 'moderate-or-heavy-snow-showers':
$string = '❄️';
break;
case 'patchy-snow-possible':
case 'patchy-sleet-possible':
case 'moderate-or-heavy-sleet':
case 'light-sleet-showers':
case 'moderate-or-heavy-sleet-showers':
case 'sleet':
$string = '🌨️';
break;
case 'wind':
$string = '💨';
break;
case 'fog':
case 'mist':
case 'blizzard':
case 'freezing-fog':
$string = '🌫️';
break;
case 'overcast':
case 'cloudy':
$string = '☁️';
break;
case 'partly-cloudy-day':
$string = '⛅';
break;
case 'partly-cloudy-night':
$string = '🎑';
break;
case 'freezing-drizzle':
case 'heavy-freezing-drizzle':
case 'patchy-freezing-drizzle-possible':
case 'ice-pellets':
case 'light-showers-of-ice-pellets':
case 'moderate-or-heavy-showers-of-ice-pellets':
$string = '🧊';
break;
case 'thundery-outbreaks-possible':
case 'patchy-light-rain-with-thunder':
case 'moderate-or-heavy-rain-with-thunder':
case 'patchy-light-snow-with-thunder':
case 'moderate-or-heavy-snow-with-thunder':
$string = '⛈️';
break;
default:
$string = '🌈';
break;
}
return $string;
}
/**
* Get the temperature attribute.
* Temperature is fetched in Celsius. It needs to be
* converted to Fahrenheit depending on the user.
*
* @param string $scale
* @return string
*/
public function temperature($scale = 'celsius')
{
$json = $this->weather_json;
$temperature = Arr::get($json, 'currently.temperature') ?? Arr::get($json, 'current.temp_c');
if ($scale === 'fahrenheit') {
$temperature = Arr::get($json, 'current.temp_f', 9 / 5 * $temperature + 32);
}
$temperature = round($temperature, 1);
$numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::DECIMAL);
return $numberFormatter->format($temperature);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Place;
use App\Models\Account\Account;
use App\Interfaces\LabelInterface;
use App\Models\ModelBindingWithContact as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* An Address is where the contact lives (or lived).
* The actual address (street name etc…) is represented with a Place object.
*/
class Address extends Model implements LabelInterface
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['contact'];
protected $table = 'addresses';
/**
* Get the account record associated with the address.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the address.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the place record associated with the address.
*
* @return BelongsTo
*/
public function place()
{
return $this->belongsTo(Place::class);
}
/**
* Get the label associated with the contact.
*
* @return BelongsToMany
*/
public function labels()
{
return $this->belongsToMany(ContactFieldLabel::class);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\Instance\Emotion\Emotion;
use App\Models\ModelBindingWithContact as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property Contact $contact
*/
class Call extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['called_at'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'contact_called' => 'boolean',
];
/**
* Eager load with every call.
*/
protected $with = [
'account',
'contact',
];
/**
* Get the account record associated with the call.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the call.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the emotion records associated with the call.
*
* @return BelongsToMany
*/
public function emotions()
{
return $this->belongsToMany(Emotion::class, 'emotion_call', 'call_id', 'emotion_id')
->withPivot('account_id', 'contact_id')
->withTimestamps();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Interfaces\LabelInterface;
use App\Models\ModelBindingWithContact as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class ContactField extends Model implements LabelInterface
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['contact'];
/**
* Get the account record associated with the contact field.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the contact field.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the label associated with the contact.
*
* @return BelongsToMany
*/
public function labels()
{
return $this->belongsToMany(ContactFieldLabel::class);
}
/**
* Get the type associated with the contact field.
*
* @return BelongsTo
*/
public function contactFieldType()
{
return $this->belongsTo(ContactFieldType::class);
}
/**
* Scope a query to only include contact field of email type.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeEmail($query)
{
return $query->whereHas('contactFieldType', function ($query) {
$query->where('type', '=', ContactFieldType::EMAIL);
});
}
/**
* Scope a query to only include contact field of phone type.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePhone($query)
{
return $query->whereHas('contactFieldType', function ($query) {
$query->where('type', '=', ContactFieldType::PHONE);
});
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\Contact;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property string $label
* @property string $label_i18n
*/
class ContactFieldLabel extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'contact_field_labels';
/** @var array<string> */
public static $standardLabels = [
'home',
'work',
'cell',
'fax',
'pager',
'main',
'other',
];
/**
* Get the account record associated with the contact field type.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ContactFieldType extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'contact_field_types';
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'delible' => 'boolean',
];
/**
* Email type contact field.
*
* @var string
*/
public const EMAIL = 'email';
/**
* Phone type contact field.
*
* @var string
*/
public const PHONE = 'phone';
/**
* Get the account record associated with the contact field type.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the conversations associated with the contact field type.
*
* @return HasMany
*/
public function conversations()
{
return $this->hasMany(Conversation::class);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBindingHasher as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Conversation extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['happened_at'];
/**
* Get the account record associated with the conversation.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the conversation.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the contact field type record associated with the conversation.
*
* @return BelongsTo
*/
public function contactFieldType()
{
return $this->belongsTo(ContactFieldType::class);
}
/**
* Get the Message records associated with the conversation.
*
* @return HasMany
*/
public function messages()
{
return $this->hasMany(Message::class);
}
}

103
app/Models/Contact/Debt.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Traits\AmountFormatter;
use App\Models\Settings\Currency;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\ModelBindingHasherWithContact as Model;
/**
* @property Account $account
* @property Contact $contact
* @property int $amount
*
* @method static Builder due()
* @method static Builder owed()
* @method static Builder inProgress()
*/
class Debt extends Model
{
use AmountFormatter, HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Eager load with every debt.
*/
protected $with = [
'account',
'contact',
];
/**
* Get the account record associated with the debt.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the debt.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the currency record associated with the debt.
*
* @return BelongsTo
*/
public function currency()
{
return $this->belongsTo(Currency::class);
}
/**
* Limit results to unpaid/unreceived debt.
*
* @param Builder $query
* @return Builder
*/
public function scopeInProgress(Builder $query)
{
return $query->where('status', 'inprogress');
}
/**
* Limit results to due debt.
*
* @param Builder $query
* @return Builder
*/
public function scopeDue(Builder $query)
{
return $query->where('in_debt', 'yes');
}
/**
* Limit results to owed debt.
*
* @param Builder $query
* @return Builder
*/
public function scopeOwed(Builder $query)
{
return $query->where('in_debt', 'no');
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Helpers\StorageHelper;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class Document extends Model
{
use HasUuid;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'documents';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'number_of_downloads' => 'integer',
];
/**
* Get the account record associated with the document.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the document.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the download link.
*
* @return string
*/
public function getDownloadLink(): string
{
if (config('filesystems.default_visibility') === 'public') {
return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename));
}
return route('storage', ['file' => $this->new_filename]);
}
/**
* Gets the data-url format of the document.
*
* @return string|null
*/
public function dataUrl(): ?string
{
try {
$url = $this->new_filename;
$file = StorageHelper::disk(config('filesystems.default'))->get($url);
return sprintf('data:%s;base64,%s',
$this->mime_type,
base64_encode($file)
);
} catch (FileNotFoundException $e) {
return null;
}
}
/**
* Delete the model from the database.
*
* @return bool|null
*/
public function delete()
{
try {
Storage::disk(config('filesystems.default'))
->delete($this->new_filename);
} catch (FileNotFoundException $e) {
// continue
}
return parent::delete();
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Gender extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'type',
'account_id',
];
/**
* Male type gender.
*
* @var string
*/
public const MALE = 'M';
/**
* Female type gender.
*
* @var string
*/
public const FEMALE = 'F';
/**
* Other type gender.
*
* @var string
*/
public const OTHER = 'O';
/**
* Unknown type gender.
*
* @var string
*/
public const UNKNOWN = 'U';
/**
* None type gender.
*
* @var string
*/
public const NONE = 'N';
public const LIST = ['M', 'F', 'O', 'U', 'N'];
/**
* Get the account record associated with the gender.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact records associated with the gender.
*
* @return HasMany
*/
public function contacts()
{
return $this->hasMany(Contact::class);
}
/**
* Is this gender the default account one?.
*
* @return bool
*/
public function isDefault(): bool
{
return $this->account->default_gender_id === $this->id;
}
}

155
app/Models/Contact/Gift.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Photo;
use App\Models\Account\Account;
use App\Traits\AmountFormatter;
use Illuminate\Database\Eloquent\Builder;
use App\Models\ModelBindingWithContact as Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property Account $account
* @property Contact $contact
* @property Contact|null $recipient
* @property string $name
* @property string $comment
* @property string $url
* @property Contact $is_for
*
* @method static Builder offered()
* @method static Builder isIdea()
*/
class Gift extends Model
{
use AmountFormatter, HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'date',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
];
/**
* Get the account record associated with the gift.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the gift.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the contact record associated with the gift.
*
* @return HasOne
*/
public function recipient()
{
return $this->hasOne(Contact::class, 'id', 'is_for');
}
/**
* Get the photos record associated with the gift.
*
* @return BelongsToMany
*/
public function photos()
{
return $this->belongsToMany(Photo::class)->withTimestamps();
}
/**
* Limit results to already offered gifts.
*
* @param Builder $query
* @return Builder
*/
public function scopeOffered(Builder $query)
{
return $query->where('status', 'offered');
}
/**
* Limit results to gifts at the idea stage.
*
* @param Builder $query
* @return Builder
*/
public function scopeIsIdea(Builder $query)
{
return $query->where('status', 'idea');
}
/**
* Check whether the gift is meant for a particular member
* of the contact's family.
*
* @return bool
*/
public function hasParticularRecipient()
{
return $this->is_for !== null && $this->is_for !== 0;
}
/**
* Set the recipient for the gift.
*
* @param int $value
* @return void
*/
public function setRecipientAttribute($value): void
{
$this->attributes['is_for'] = $value;
}
/**
* Get the name of the recipient for this gift.
*
* @return string|null
*/
public function getRecipientNameAttribute(): ?string
{
if ($this->hasParticularRecipient()) {
$recipient = $this->recipient;
if (! is_null($recipient)) {
return $recipient->first_name;
}
}
return null;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LifeEvent extends Model
{
use HasUuid;
protected $table = 'life_events';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'note',
'happened_at',
'account_id',
'contact_id',
'reminder_id',
'life_event_type_id',
'happened_at_month_unknown',
'happened_at_day_unknown',
];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['happened_at'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'happened_at_month_unknown' => 'boolean',
'happened_at_day_unknown' => 'boolean',
];
/**
* Get the account record associated with the life event.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the life event.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the life event type record associated with the life event.
*
* @return BelongsTo
*/
public function lifeEventType()
{
return $this->belongsTo(LifeEventType::class, 'life_event_type_id');
}
/**
* Get the reminder record associated with the life event.
*
* @return BelongsTo
*/
public function reminder()
{
return $this->belongsTo(Reminder::class);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LifeEventCategory extends Model
{
use HasUuid;
protected $table = 'life_event_categories';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'account_id',
'default_life_event_category_key',
'core_monica_data',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'core_monica_data' => 'boolean',
];
/**
* Get the account record associated with the life event category.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the life event type records associated with the category.
*
* @return HasMany
*/
public function lifeEventTypes()
{
return $this->hasMany(LifeEventType::class);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LifeEventType extends Model
{
use HasUuid;
protected $table = 'life_event_types';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'account_id',
'life_event_category_id',
'default_life_event_type_key',
'core_monica_data',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'core_monica_data' => 'boolean',
];
/**
* Get the account record associated with the life event type.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the life event category record associated with the life event type.
*
* @return BelongsTo
*/
public function lifeEventCategory()
{
return $this->belongsTo(LifeEventCategory::class, 'life_event_category_id');
}
/**
* Get the Life event records associated with the life event Type.
*
* @return HasMany
*/
public function lifeEvents()
{
return $this->hasMany(LifeEvent::class);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBindingHasher as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Message extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['written_at'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'written_by_me' => 'boolean',
];
/**
* Get the account record associated with the message.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the message.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the Conversation records associated with the message.
*
* @return BelongsTo
*/
public function conversation()
{
return $this->belongsTo(Conversation::class);
}
}

124
app/Models/Contact/Note.php Normal file
View File

@@ -0,0 +1,124 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Helpers\DateHelper;
use App\Models\Account\Account;
use Illuminate\Database\Eloquent\Builder;
use App\Models\ModelBindingWithContact as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property Account $account
* @property Contact $contact
* @property string $parsed_body
* @property string $body
* @property bool $is_favorited
* @property \Illuminate\Support\Carbon|null $favorited_at
*/
class Note extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'is_favorited' => 'boolean',
];
protected $dates = [
'favorited_at',
];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'contact_id',
'body',
'is_favorited',
];
/**
* Eager load with every note.
*/
protected $with = [
'account',
'contact',
];
/**
* Get the account record associated with the note.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the note.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Limit notes to favorited ones.
*
* @param Builder $query
* @return Builder
*/
public function scopeFavorited(Builder $query)
{
return $query->where('is_favorited', true);
}
/**
* Get the description of a note.
*
* @return string
*/
public function getBody()
{
return $this->body;
}
/**
* Gets the activity date for this note.
*
* @return string
*/
public function getCreatedAt()
{
return DateHelper::getShortDate($this->created_at);
}
/**
* Gets the content of the activity and formats it for the email.
*
* @return string
*/
public function getContent()
{
return wordwrap($this->getBody(), 75);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Models\Contact;
use App\Models\Account\Account;
use App\Models\Account\Company;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Occupation extends Model
{
protected $table = 'occupations';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'contact_id',
'company_id',
'title',
'description',
'salary',
'salary_unit',
'currently_works_here',
'start_date',
'end_date',
];
/**
* Valid value for salary unit.
*
* @var array
*/
public static $salaryUnits = [
'year', 'month', 'week', 'day', 'hour',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'start_date' => 'datetime:Y-m-d',
'end_date' => 'datetime:Y-m-d',
];
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the occupation.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the occupation.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the company record associated with the occupation.
*
* @return BelongsTo
*/
public function company()
{
return $this->belongsTo(Company::class);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models\Contact;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Pet extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the account record associated with the pet.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the pet.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the contact record associated with the pet.
*
* @return BelongsTo
*/
public function petCategory()
{
return $this->belongsTo(PetCategory::class);
}
/**
* Set the name to null if it's an empty string.
*
* @param string $value
* @return void
*/
public function setNameAttribute($value)
{
$this->attributes['name'] = $value ?: null;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models\Contact;
use Illuminate\Database\Eloquent\Model;
class PetCategory extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'pet_categories';
/**
* Scope a query to only include pet categories that are considered `common`.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeCommon($query)
{
return $query->where('is_common', 1);
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace App\Models\Contact;
use Carbon\Carbon;
use App\Traits\HasUuid;
use App\Models\User\User;
use App\Helpers\DateHelper;
use App\Models\Account\Account;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\ModelBindingHasherWithContact as Model;
/**
* A reminder has two states: active and inactive.
* An inactive reminder is basically a one_time reminder that has already be
* sent once and has been marked inactive so we don't schedule it again.
*
* @property string $next_expected_date_human_readable
* @property string $next_expected_date
*/
class Reminder extends Model
{
use HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'is_birthday' => 'boolean',
'delible' => 'boolean',
'inactive' => 'boolean',
'initial_date' => 'date:Y-m-d',
];
/**
* Valid value for frequency type.
*
* @var array
*/
public static $frequencyTypes = [
'one_time', 'week', 'month', 'year',
];
/**
* Get the account record associated with the reminder.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the reminder.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Get the Reminder Outbox records associated with the account.
*
* @return HasMany
*/
public function reminderOutboxes()
{
return $this->hasMany(ReminderOutbox::class);
}
/**
* Scope a query to only include active reminders.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('inactive', false);
}
/**
* Test if this reminder is the contact's birthday reminder.
*
* @return bool
*/
public function isBirthdayReminder(): bool
{
return $this->contact !== null
&& $this->contact->birthday_reminder_id === $this->id;
}
/**
* Calculate the next expected date for this reminder.
*
* @return Carbon
*/
public function calculateNextExpectedDate($date = null)
{
if (is_null($date)) {
$date = $this->initial_date;
}
while ($date->isPast()) {
$date = DateHelper::addTimeAccordingToFrequencyType($date, $this->frequency_type, $this->frequency_number);
}
if ($date->isToday()) {
$date = DateHelper::addTimeAccordingToFrequencyType($date, $this->frequency_type, $this->frequency_number);
}
return $date;
}
/**
* Calculate the next expected date using user timezone for this reminder.
*
* @return Carbon
*/
public function calculateNextExpectedDateOnTimezone()
{
$date = $this->initial_date;
$date = Carbon::create($date->year, $date->month, $date->day, 0, 0, 0,
DateHelper::getTimezone() ?? config('app.timezone'));
return $this->calculateNextExpectedDate($date);
}
/**
* Schedule the reminder to be sent.
*
* @param User $user
* @return void
*/
public function schedule(User $user)
{
// remove any existing scheduled reminders
$this->reminderOutboxes->each->delete();
// when should we send this reminder?
$triggerDate = $this->calculateNextExpectedDate();
// schedule the reminder in the outbox, one for each user of the account
ReminderOutbox::create([
'account_id' => $this->account_id,
'reminder_id' => $this->id,
'user_id' => $user->id,
'planned_date' => $triggerDate,
'nature' => 'reminder',
]);
$this->scheduleNotifications($triggerDate, $user);
}
/**
* Create all the notifications that are supposed to be sent
* 30 and 7 days prior to the actual reminder.
*
* @param Carbon $triggerDate
* @param User $user
* @return void
*/
public function scheduleNotifications(Carbon $triggerDate, User $user)
{
$date = $triggerDate->toDateString();
$reminderRules = $this->account->reminderRules()->where('active', 1)->get();
foreach ($reminderRules as $reminderRule) {
$datePrior = Carbon::createFromFormat('Y-m-d', $date)
->subDays($reminderRule->number_of_days_before);
if ($datePrior->lessThanOrEqualTo(now())) {
continue;
}
ReminderOutbox::create([
'account_id' => $this->account_id,
'reminder_id' => $this->id,
'user_id' => $user->id,
'planned_date' => $datePrior->toDateString(),
'nature' => 'notification',
'notification_number_days_before' => $reminderRule->number_of_days_before,
]);
}
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models\Contact;
use App\Models\User\User;
use App\Models\Account\Account;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use App\Models\ModelBindingHasherWithContact as Model;
/**
* @property Account $account
* @property int $account_id
* @property Contact $contact
* @property User $user
* @property int $user_id
* @property Reminder|null $reminder
* @property int $reminder_id
* @property string $nature
* @property \Illuminate\Support\Carbon|null $planned_date
* @property int $notification_number_days_before
*/
class ReminderOutbox extends Model
{
protected $table = 'reminder_outbox';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'planned_date',
];
/**
* Get the account record associated with the reminder.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the reminder record associated with the reminder.
*
* @return BelongsTo
*/
public function reminder()
{
return $this->belongsTo(Reminder::class);
}
/**
* Get the user record associated with the reminder.
*
* @return BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models\Contact;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ReminderRule extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'reminder_rules';
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'active' => 'boolean',
];
/**
* Get the account record associated with the reminder.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Models\Contact;
use App\Models\Account\Account;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'name',
'name_slug',
'account_id',
];
/**
* Get the account record associated with the tag.
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contacts record associated with the tag.
*/
public function contacts()
{
return $this->belongsToMany(Contact::class)->withPivot('account_id')->withTimestamps();
}
/**
* Get the tags with the contact count.
*/
public static function contactsCount()
{
return DB::table('contact_tag')->selectRaw('COUNT(tag_id) AS contact_count, name, tag_id AS id')
->join('tags', function ($join) {
$join->on('tags.id', '=', 'contact_tag.tag_id')
->on('tags.account_id', '=', 'contact_tag.account_id');
})
->join('contacts', function ($join) {
$join->on('contacts.id', '=', 'contact_tag.contact_id')
->on('contacts.account_id', '=', 'contact_tag.account_id');
})
->where([
'tags.account_id' => auth()->user()->account_id,
'contacts.address_book_id' => null,
])
->groupBy('tag_id')
->get()
->sortByCollator('name');
}
}

101
app/Models/Contact/Task.php Normal file
View File

@@ -0,0 +1,101 @@
<?php
namespace App\Models\Contact;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property Account $account
* @property Contact|null $contact
* @property string $title
* @property string $description
* @property string $uuid
* @property bool $completed
* @property \Carbon\Carbon|null $completed_at
*
* @method static Builder completed()
* @method static Builder inProgress()
*/
class Task extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'completed_at',
'archived_at',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'completed' => 'boolean',
'archived' => 'boolean',
];
/**
* Eager load with every task.
*/
protected $with = [
'account',
'contact',
];
/**
* Get the account record associated with the task.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the task.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Limit tasks to completed ones.
*
* @param Builder $query
* @return Builder
*/
public function scopeCompleted(Builder $query)
{
return $query->where('completed', true);
}
/**
* Limit tasks to in-progress ones.
*
* @param Builder $query
* @return Builder
*/
public function scopeInProgress(Builder $query)
{
return $query->where('completed', false);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Models\Instance;
use App\Models\User\User;
use function Safe\json_decode;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AuditLog extends Model
{
protected $table = 'audit_logs';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'author_id',
'about_contact_id',
'author_name',
'action',
'objects',
'should_appear_on_dashboard',
'audited_at',
];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'audited_at',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'should_appear_on_dashboard' => 'boolean',
];
/**
* Get the Account record associated with the audit log.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the User record associated with the audit log.
*
* @return BelongsTo
*/
public function author()
{
return $this->belongsTo(User::class);
}
/**
* Get the Contact record associated with the audit log.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class, 'about_contact_id');
}
/**
* Get the JSON object.
*
* @param mixed $value
* @return mixed
*/
public function getObjectAttribute($value)
{
return json_decode($this->objects);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models\Instance;
use Illuminate\Database\Eloquent\Model;
class Cron extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'command',
'last_run',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'last_run' => 'datetime',
];
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models\Instance\Emotion;
use App\Models\Contact\Call;
use App\Models\Account\Activity;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* An emotion (ex: Adoration) is defined into 3 categories:
* - Primary: Love
* - Secondary: Affection
* - Tertiary: Adoration.
*/
class Emotion extends Model
{
protected $table = 'emotions';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the primary emotion record associated with the emotion.
*
* @return BelongsTo
*/
public function primary()
{
return $this->belongsTo(PrimaryEmotion::class, 'emotion_primary_id');
}
/**
* Get the secondary emotion record associated with the emotion.
*
* @return BelongsTo
*/
public function secondary()
{
return $this->belongsTo(SecondaryEmotion::class, 'emotion_secondary_id');
}
/**
* Get the call records associated with the emotion.
*
* @return BelongsToMany
*/
public function calls()
{
return $this->belongsToMany(Call::class, 'emotion_call', 'emotion_id', 'call_id')
->withPivot('account_id', 'contact_id')
->withTimestamps();
}
/**
* Get the activity records associated with the emotion.
*
* @return BelongsToMany
*/
public function activities()
{
return $this->belongsToMany(Activity::class, 'emotion_activity', 'emotion_id', 'activity_id')
->withPivot('account_id')
->withTimestamps();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models\Instance\Emotion;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* An emotion (ex: Adoration) is defined into 3 categories:
* - Primary: Love
* - Secondary: Affection
* - Tertiary: Adoration.
*/
class PrimaryEmotion extends Model
{
protected $table = 'emotions_primary';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the emotion records associated with the primary emotion.
*
* @return HasMany
*/
public function emotions()
{
return $this->hasMany(Emotion::class, 'emotion_primary_id');
}
/**
* Get the secondary records associated with the primary emotion.
*
* @return HasMany
*/
public function secondaries()
{
return $this->hasMany(SecondaryEmotion::class, 'emotion_primary_id');
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models\Instance\Emotion;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* An emotion (ex: Adoration) is defined into 3 categories:
* - Primary: Love
* - Secondary: Affection
* - Tertiary: Adoration.
*/
class SecondaryEmotion extends Model
{
protected $table = 'emotions_secondary';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the primary emotion record associated with the secondary emotion.
*
* @return BelongsTo
*/
public function primary()
{
return $this->belongsTo(PrimaryEmotion::class, 'emotion_primary_id');
}
/**
* Get the emotion records associated with the secondary emotion.
*
* @return HasMany
*/
public function emotions()
{
return $this->hasMany(Emotion::class);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models\Instance;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
class Instance extends Model
{
/**
* Once migrations have been run to add a new default contact field type,
* we need to mark the field as being migrated so we don't create another
* default contact field type if another migration will change this table
* in the future.
*/
public function markDefaultContactFieldTypeAsMigrated()
{
DB::table('default_contact_field_types')
->update(['migrated' => 1]);
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Models\Instance;
use App\Traits\HasUuid;
use App\Helpers\DateHelper;
use Illuminate\Support\Carbon;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* A special date is a date that is not necessarily based on a year that we know.
* This happens when we add a birthdate for instance. It can be based:
* * on a real date, where we know the day, month and year
* * on a date where we just know the day and the month but not the year
* * on an age (we know this person is 33 but we don't know his birthdate,
* so we'll give an estimation).
*
* Instead of adding a lot of logic in the Contact table, we've decided to
* create this class that will deal with this complexity.
*
* @property bool $is_age_based
* @property bool $is_year_unknown
* @property \Carbon\Carbon|null $date
*/
class SpecialDate extends Model
{
use HasUuid;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'special_dates';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = ['contact'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = ['date'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'contact_id',
'account_id',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'is_age_based' => 'boolean',
'is_year_unknown' => 'boolean',
];
/**
* Get the account record associated with the special date.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the special date.
*
* @return BelongsTo
*/
public function contact()
{
return $this->belongsTo(Contact::class);
}
/**
* Returns a short version of the date, taking into account if the year is
* unknown or not. This will return either `July 21` or `July 21, 2017`.
*/
public function toShortString()
{
if ($this->is_year_unknown) {
return DateHelper::getShortDateWithoutYear($this->date);
}
return DateHelper::getShortDate($this->date);
}
/**
* Returns the age that the date represents, if the date is set and if it's
* not based on a year we don't know.
*
* @return int|null
*/
public function getAge(): ?int
{
if (is_null($this->date)) {
return null;
}
if ($this->is_year_unknown) {
return null;
}
return $this->date->diffInYears(now());
}
/**
* Create a SpecialDate from an age.
*
* @param int $age
*/
public function createFromAge(int $age)
{
$this->is_age_based = true;
$this->date = now(DateHelper::getTimezone())->subYears($age)->month(1)->day(1);
$this->save();
return $this;
}
/**
* Create a SpecialDate from an actual date, that might not contain a year.
*
* @param int $year
* @param int $month
* @param int $day
*/
public function createFromDate(int $year, int $month, int $day)
{
// year 0 represents the `unknown` choice in the dropdown representing
// the years
if ($year != 0) {
$date = Carbon::createFromDate($year, $month, $day);
$this->is_year_unknown = false;
} else {
$date = Carbon::createFromDate(now()->year, $month, $day);
$this->is_year_unknown = true;
}
$this->date = $date;
$this->save();
return $this;
}
/**
* Associates a special date to a contact.
*
* @param Contact $contact
*/
public function setToContact(Contact $contact)
{
$this->account_id = $contact->account_id;
$this->contact_id = $contact->id;
$this->save();
return $this;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Models\Instance;
use Illuminate\Database\Eloquent\Model;
class Statistic extends Model
{
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models\Journal;
use App\Traits\HasUuid;
use App\Helpers\DateHelper;
use App\Traits\Journalable;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use App\Interfaces\IsJournalableInterface;
class Day extends Model implements IsJournalableInterface
{
use Journalable, HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $dates = [
'date',
];
/**
* Get the account record associated with the debt.
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get all the information of the Entry for the journal.
*
* @return array
*/
public function getInfoForJournalEntry()
{
return [
'type' => 'day',
'id' => $this->id,
'rate' => $this->rate,
'comment' => $this->comment,
'date' => $this->date,
'day' => $this->date->day,
'day_name' => mb_convert_case(DateHelper::getShortDay($this->date), MB_CASE_TITLE, 'UTF-8'),
'month' => $this->date->month,
'month_name' => mb_convert_case(DateHelper::getShortMonth($this->date), MB_CASE_UPPER, 'UTF-8'),
'year' => $this->date->year,
'happens_today' => $this->date->isToday(),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Models\Journal;
use App\Traits\HasUuid;
use App\Helpers\DateHelper;
use App\Traits\Journalable;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use App\Interfaces\IsJournalableInterface;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property \Carbon\Carbon $date
*/
class Entry extends Model implements IsJournalableInterface
{
use Journalable, HasUuid;
protected $table = 'entries';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'title',
'post',
];
/**
* Get the account record associated with the entry.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the Entry date.
*
* @param string $value
* @return \Carbon\Carbon
*/
public function getDateAttribute($value)
{
// Default to created_at, but show journalEntry->date if the entry type is JournalEntry
return $this->journalEntry ? $this->journalEntry->date : $this->created_at;
}
/**
* Get all the information of the Entry for the journal.
*
* @return array
*/
public function getInfoForJournalEntry()
{
return [
'type' => 'entry',
'id' => $this->id,
'title' => $this->title,
'post' => $this->post,
'day' => $this->date->day,
'day_name' => mb_convert_case(DateHelper::getShortDay($this->date), MB_CASE_TITLE, 'UTF-8'),
'month' => $this->date->month,
'month_name' => mb_convert_case(DateHelper::getShortMonth($this->date), MB_CASE_UPPER, 'UTF-8'),
'year' => $this->date->year,
'date' => $this->date,
'created_at' => DateHelper::getShortDateWithTime($this->created_at),
];
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Models\Journal;
use App\Helpers\DateHelper;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Builder;
use App\Interfaces\IsJournalableInterface;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property Account $account
* @property User $invitedBy
* @property int $account_id
* @property IsJournalableInterface $journalable
* @property int $journalable_id
* @property string $journalable_type
* @property \Carbon\Carbon|null $date
*/
class JournalEntry extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'journal_entries';
protected $dates = [
'date',
];
/**
* Eager load with every entry.
*/
protected $with = [
'journalable',
];
/**
* Get all of the owning "journal-able" models.
*
* @return MorphTo
*/
public function journalable()
{
return $this->morphTo();
}
/**
* Get the account record associated with the journal entry.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Adds a new entry in the journal.
*
* @param \App\Interfaces\IsJournalableInterface $resourceToLog
* @return self
*/
public static function add(IsJournalableInterface $resourceToLog): self
{
$journal = new self;
$journal->account_id = $resourceToLog->account_id;
$journal->date = now(DateHelper::getTimezone());
if ($resourceToLog instanceof \App\Models\Account\Activity) {
$journal->date = $resourceToLog->happened_at;
} elseif ($resourceToLog instanceof \App\Models\Journal\Entry) {
$journal->date = $resourceToLog->attributes['date'];
}
$journal->save();
$resourceToLog->journalEntries()->save($journal);
return $journal;
}
/**
* Update an entry in the journal.
*
* @param \App\Interfaces\IsJournalableInterface $resourceToLog
* @return self
*/
public function edit(IsJournalableInterface $resourceToLog): self
{
if ($resourceToLog instanceof \App\Models\Journal\Entry) {
$this->date = $resourceToLog->attributes['date'];
}
$this->save();
return $this;
}
/**
* Get the information about the object represented by the Journal Entry.
*
* @return array
*/
public function getObjectData()
{
// Instantiating the object
/** @var IsJournalableInterface */
$correspondingObject = $this->journalable;
return $correspondingObject->getInfoForJournalEntry();
}
/**
* Filter by real entry (day rate or journal entry).
*
* @param Builder $query
* @return Builder
*/
public function scopeEntry(Builder $query): Builder
{
return $query->where('journalable_type', '!=', 'App\Models\Account\Activity');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Model;
abstract class ModelBinding extends Model
{
/**
* Resolve binding.
*
* @param string $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null): ?Model
{
if (Auth::guest()) {
return null;
}
return $this->where('account_id', Auth::user()->account_id)
->where($this->getRouteKeyName(), $value)
->firstOrFail();
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use App\Traits\Hasher;
use App\Interfaces\Hashing;
abstract class ModelBindingHasher extends ModelBinding implements Hashing
{
use Hasher;
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use App\Traits\Hasher;
use App\Interfaces\Hashing;
abstract class ModelBindingHasherWithContact extends ModelBindingWithContact implements Hashing
{
use Hasher;
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Illuminate\Database\Eloquent\Model;
abstract class ModelBindingWithContact extends Model
{
/**
* Resolve binding with contact relation.
*
* @param string $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null): ?Model
{
/** @var \App\Models\Contact\Contact|null */
$contact = Route::current()->parameter('contact');
if (Auth::guest() || is_null($contact)) {
return null;
}
return $this->where('account_id', Auth::user()->account_id)
->where('contact_id', $contact->id)
->where($this->getRouteKeyName(), $value)
->firstOrFail();
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models\Relationship;
use App\Traits\HasUuid;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* A relationship defines relations between contacts.
*/
class Relationship extends Model
{
use HasUuid;
/**
* All of the relationships to be touched.
*
* @var array
*/
protected $touches = [
'contactIs',
'ofContact',
];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'contact_is',
'of_contact',
'relationship_type_id',
];
/**
* Get the account record associated with the relationship.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the relationship.
*
* @return BelongsTo
*/
public function contactIs()
{
return $this->belongsTo(Contact::class, 'contact_is');
}
/**
* Get the contact record connected with the relationship.
*
* @return BelongsTo
*/
public function ofContact()
{
return $this->belongsTo(Contact::class, 'of_contact');
}
/**
* Get the relationship type record associated with the relationship.
*
* @return BelongsTo
*/
public function relationshipType()
{
return $this->belongsTo(RelationshipType::class, 'relationship_type_id');
}
/**
* Get the reverser relationship of this one.
*
* @return self|null
*/
public function reverseRelationship(): ?self
{
$reverseRelationshipType = $this->relationshipType->reverseRelationshipType();
if ($reverseRelationshipType) {
return self::where([
'account_id'=> $this->account_id,
'contact_is' => $this->of_contact,
'of_contact' => $this->contact_is,
'relationship_type_id' => $reverseRelationshipType->id,
])->first();
}
return null;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Models\Relationship;
use App\Helpers\AccountHelper;
use App\Models\Contact\Gender;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RelationshipType extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'relationship_types';
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'delible' => 'boolean',
];
/**
* Get the account record associated with the reminder.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the relationship type group record associated with the reminder.
*
* @return BelongsTo
*/
public function relationshipTypeGroup()
{
return $this->belongsTo(RelationshipTypeGroup::class);
}
/**
* Get the reverser relationship type of this one.
*
* @return self|null
*/
public function reverseRelationshipType()
{
return $this->account->getRelationshipTypeByType($this->name_reverse_relationship);
}
/**
* Get the i18n version of the name attribute, like "Significant other".
*
* @psalm-suppress InvalidReturnType
* @psalm-suppress InvalidReturnStatement
*
* @param Contact $contact
* @param bool $includeOpposite
* @param string $gender
* @return string|null|\Illuminate\Contracts\Translation\Translator
*/
public function getLocalizedName(Contact $contact = null, bool $includeOpposite = false, string $gender = null)
{
$defaultGender = AccountHelper::getDefaultGender($this->account);
if (is_null($gender)) {
$gender = $defaultGender;
}
$femaleVersion = trans('app.relationship_type_'.$this->name.'_female');
$maleVersion = trans('app.relationship_type_'.$this->name.'_male');
if ($maleVersion === 'app.relationship_type_'.$this->name.'_male') {
$maleVersion = trans('app.relationship_type_'.$this->name);
}
if (! is_null($contact)) {
$maleVersionWithName = trans('app.relationship_type_'.$this->name.'_male_with_name', ['name' => $contact->name]);
if ($maleVersionWithName === 'app.relationship_type_'.$this->name.'_male_with_name') {
$maleVersionWithName = trans('app.relationship_type_'.$this->name.'_with_name');
}
$femaleVersionWithName = trans('app.relationship_type_'.$this->name.'_female_with_name', ['name' => $contact->name]);
// include the reverse of the relation in the string (masculine/feminine)
// this is used in the dropdown of the relationship types when creating
// or deleting a relationship.
if ($includeOpposite) {
// in some language, masculine and feminine version of a relationship type is the same.
// we need to keep just one version in that case.
if ($femaleVersion === $maleVersion) {
// `Maazarin's significant other`
return $maleVersionWithName;
}
return $defaultGender === Gender::FEMALE ?
// `Maazarin's aunt/uncle`
$femaleVersionWithName.'/'.$maleVersion :
// `Maazarin's uncle/aunt`
$maleVersionWithName.'/'.$femaleVersion;
} else {
return $gender === Gender::FEMALE ?
// `Maazarin's aunt`
$femaleVersionWithName :
// `Maazarin's uncle`
$maleVersionWithName;
}
}
return $gender === Gender::FEMALE ?
// `aunt`
$femaleVersion :
// `uncle`
$maleVersion;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Models\Relationship;
use App\Models\Account\Account;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class RelationshipTypeGroup extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
protected $table = 'relationship_type_groups';
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'delible' => 'boolean',
];
/**
* Get the account record associated with the reminder.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models\Settings;
use Illuminate\Database\Eloquent\Model;
class Currency extends Model
{
protected $table = 'currencies';
public $timestamps = false;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models\Settings;
use App\Models\User\User;
use Illuminate\Database\Eloquent\Model;
class Term extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* Get the user record associated with the term.
*/
public function users()
{
return $this->belongsToMany(User::class)->withPivot('user_id')->withTimestamps();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Models\User;
use Parsedown;
use App\Helpers\DateHelper;
use Illuminate\Database\Eloquent\Model;
class Changelog extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be mutated to dates.
*
* @var array<string>
*/
protected $dates = [
'created_at',
];
/**
* Get the user records associated with the tag.
*/
public function users()
{
return $this->belongsToMany(User::class)->withPivot('read', 'upvote')->withTimestamps();
}
/**
* Return the markdown parsed description.
*
* @return string
*/
public function getDescriptionAttribute($value)
{
return (new Parsedown())->text($value);
}
/**
* Return the created_at date in a friendly format.
*
* @return string
*/
public function getCreatedAtAttribute($value)
{
return DateHelper::getShortDate($value);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Models\User;
use App\Models\Account\Account;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* A module is a section of information that appears in the Contact sheet, like
* 'Activities' or 'Notes'.
*/
class Module extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'active' => 'boolean',
'delible' => 'boolean',
];
/**
* Get the account record associated with the module.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Scope a query to only include modules that are active.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', true);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Models\User;
use App\Models\ModelBinding as Model;
use Illuminate\Database\Eloquent\Builder;
class RecoveryCode extends Model
{
protected $table = 'recovery_codes';
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'user_id',
'recovery',
];
/**
* Scope a query to only include unused code.
*
* @param Builder $query
* @return Builder
*/
public function scopeUnused($query)
{
return $query->where('used', 0);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models\User;
use App\Models\ModelBinding as Model;
class SyncToken extends Model
{
protected $table = 'synctoken';
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'account_id',
'user_id',
'name',
'timestamp',
];
}

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

@@ -0,0 +1,308 @@
<?php
namespace App\Models\User;
use Carbon\Carbon;
use App\Traits\HasUuid;
use App\Helpers\FormHelper;
use App\Jobs\SendVerifyEmail;
use App\Models\Settings\Term;
use App\Models\Account\Account;
use App\Models\Contact\Contact;
use App\Helpers\ComplianceHelper;
use App\Models\Settings\Currency;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Contracts\Translation\HasLocalePreference;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable implements MustVerifyEmail, HasLocalePreference
{
use Notifiable, HasApiTokens, HasUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array<string>|bool
*/
protected $guarded = ['id'];
/**
* The attributes that are mass assignable.
*
* @var array<string>
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
'timezone',
'locale',
'currency_id',
'fluid_container',
'temperature_scale',
'name_order',
'google2fa_secret',
];
/**
* Eager load account with every user.
*/
protected $with = ['account'];
/**
* The attributes that should be hidden for arrays.
*
* @var array<int, string>
*/
protected $hidden = [
'password', 'remember_token', 'google2fa_secret',
];
/**
* The attributes that should be cast to native types.
*
* @var array<string, string>
*/
protected $casts = [
'profile_new_life_event_badge_seen' => 'boolean',
'admin' => 'boolean',
'fluid_container' => 'boolean',
];
/**
* Available names order.
*
* @var array
*/
public const NAMES_ORDER = [
'firstname_lastname',
'lastname_firstname',
'firstname_lastname_nickname',
'firstname_nickname_lastname',
'lastname_firstname_nickname',
'lastname_nickname_firstname',
'nickname_firstname_lastname',
'nickname_lastname_firstname',
'nickname_bracketed_firstname_lastname',
'nickname',
];
/**
* Get the account record associated with the user.
*
* @return BelongsTo
*/
public function account()
{
return $this->belongsTo(Account::class);
}
/**
* Get the contact record associated with the 'me' contact.
*
* @return HasOne
*/
public function me()
{
return $this->hasOne(Contact::class, 'id', 'me_contact_id');
}
/**
* Get the term records associated with the user.
*
* @return BelongsToMany
*/
public function terms()
{
return $this->belongsToMany(Term::class)->withPivot('ip_address')->withTimestamps();
}
/**
* Get the recovery codes associated with the user.
*
* @return HasMany
*/
public function recoveryCodes()
{
return $this->hasMany(RecoveryCode::class);
}
/**
* Gets the currency for this user.
*
* @return BelongsTo
*/
public function currency()
{
return $this->belongsTo(Currency::class);
}
/**
* Assigns a default value just in case the sort order is empty.
*
* @param string $value
* @return string
*/
public function getContactsSortOrderAttribute($value): string
{
return ! empty($value) ? $value : 'firstnameAZ';
}
/**
* Indicates if the layout is fluid or not for the UI.
*
* @return string
*/
public function getFluidLayout(): string
{
if ($this->fluid_container) {
return 'container-fluid';
} else {
return 'container';
}
}
/**
* Get users's full name. The name is formatted according to the user's
* preference, either "Firstname Lastname", or "Lastname Firstname".
*
* @return string
*/
public function getNameAttribute(): string
{
$completeName = '';
if (FormHelper::getNameOrderForForms($this) === 'firstname') {
$completeName = $this->first_name;
if ($this->last_name !== '') {
$completeName = $completeName.' '.$this->last_name;
}
} else {
if ($this->last_name !== '') {
$completeName = $this->last_name;
}
$completeName = $completeName.' '.$this->first_name;
}
return $completeName;
}
/**
* Ecrypt the user's google_2fa secret.
*
* @param string $value
* @return void
*/
public function setGoogle2faSecretAttribute($value): void
{
$this->attributes['google2fa_secret'] = encrypt($value);
}
/**
* Decrypt the user's google_2fa secret.
*
* @param string|null $value
* @return string|null
*/
public function getGoogle2faSecretAttribute($value): ?string
{
return is_null($value) ? null : decrypt($value);
}
/**
* Indicate if the user has accepted the most current terms and privacy.
*
* @param string|null $value
* @return bool
*/
public function getPolicyCompliantAttribute($value): bool
{
return ComplianceHelper::isCompliantWithCurrentTerm($this);
}
/**
* Indicate whether the user should be reminded at this time.
* This is affected by the user settings regarding the hour of the day he
* wants to be reminded.
*
* @param Carbon|null $date
* @return bool
*/
public function isTheRightTimeToBeReminded($date)
{
if (is_null($date)) {
return false;
}
$now = now($this->timezone);
$isTheRightTime = true;
// compare date with current date for the user
if (! $date->isSameDay($now)) {
$isTheRightTime = false;
}
// compare current hour for the user with the hour they want to be
// reminded as per the hour set on the profile
if (! $now->isSameHour($this->account->default_time_reminder_is_sent)) {
$isTheRightTime = false;
}
return $isTheRightTime;
}
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification(): void
{
/** @var int $count */
$count = Account::count();
if (config('monica.signup_double_optin') && $count > 1) {
SendVerifyEmail::dispatch($this);
}
}
/**
* Get the preferred locale of the entity.
*
* @return string|null
*/
public function preferredLocale()
{
return $this->locale;
}
/**
* Try using a recovery code.
*
* @param string $recovery
* @return bool
*/
public function recoveryChallenge(string $recovery): bool
{
$recoveryCodes = $this->recoveryCodes()->unused()->get();
foreach ($recoveryCodes as $recoveryCode) {
if ($recoveryCode->recovery === $recovery) {
$recoveryCode->used = true;
$recoveryCode->save();
return true;
}
}
return false;
}
}