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);
}
}