diff --git a/.editorconfig b/.editorconfig index 7751beb91..4d73fbe31 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_size = 4 [*.blade.php] indent_size = 2 -[*.{js,vue,scss,json}] +[*.{js,vue,scss,json,css}] indent_size = 2 [*.md] diff --git a/app/Helpers/AuditLogHelper.php b/app/Helpers/AuditLogHelper.php index a21e6fbf5..154c8d47f 100644 --- a/app/Helpers/AuditLogHelper.php +++ b/app/Helpers/AuditLogHelper.php @@ -265,6 +265,18 @@ class AuditLogHelper $sentence = AuditLogHelper::userNotificationChannelDestroyed($log); break; + case 'loan_created': + $sentence = AuditLogHelper::loanCreated($log, $user); + break; + + case 'loan_updated': + $sentence = AuditLogHelper::loanUpdated($log, $user); + break; + + case 'loan_destroyed': + $sentence = AuditLogHelper::loanDestroyed($log, $user); + break; + default: $sentence = 'No translation'; break; @@ -1211,4 +1223,73 @@ class AuditLogHelper return $sentence; } + + private static function loanCreated(AuditLog $log, User $user): string + { + $contact = Contact::find($log->object->{'contact_id'}); + + if ($contact) { + $sentence = trans('log.loan_created', [ + 'contact_url' => route('contact.show', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + 'contact_name' => NameHelper::formatContactName($user, $contact), + 'loan_name' => $log->object->{'loan_name'}, + ]); + } else { + $sentence = trans('log.loan_created_object_deleted', [ + 'contact_name' => $log->object->{'contact_name'}, + 'loan_name' => $log->object->{'loan_name'}, + ]); + } + + return $sentence; + } + + private static function loanUpdated(AuditLog $log, User $user): string + { + $contact = Contact::find($log->object->{'contact_id'}); + + if ($contact) { + $sentence = trans('log.loan_updated', [ + 'contact_url' => route('contact.show', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + 'contact_name' => NameHelper::formatContactName($user, $contact), + 'loan_name' => $log->object->{'loan_name'}, + ]); + } else { + $sentence = trans('log.loan_updated_object_deleted', [ + 'contact_name' => $log->object->{'contact_name'}, + 'loan_name' => $log->object->{'loan_name'}, + ]); + } + + return $sentence; + } + + private static function loanDestroyed(AuditLog $log, User $user): string + { + $contact = Contact::find($log->object->{'contact_id'}); + + if ($contact) { + $sentence = trans('log.loan_destroyed', [ + 'contact_url' => route('contact.show', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + 'contact_name' => NameHelper::formatContactName($user, $contact), + 'loan_name' => $log->object->{'loan_name'}, + ]); + } else { + $sentence = trans('log.loan_destroyed_object_deleted', [ + 'contact_name' => $log->object->{'contact_name'}, + 'loan_name' => $log->object->{'loan_name'}, + ]); + } + + return $sentence; + } } diff --git a/app/Helpers/ContactLogHelper.php b/app/Helpers/ContactLogHelper.php index 30a4f4d58..2a352332c 100644 --- a/app/Helpers/ContactLogHelper.php +++ b/app/Helpers/ContactLogHelper.php @@ -96,6 +96,16 @@ class ContactLogHelper $sentence = ContactLogHelper::templateUpdated($log); break; + case 'loan_created': + $sentence = ContactLogHelper::loanCreated($log); + break; + case 'loan_updated': + $sentence = ContactLogHelper::loanUpdated($log); + break; + case 'loan_destroyed': + $sentence = ContactLogHelper::loanDestroyed($log); + break; + default: $sentence = 'No translation'; break; @@ -307,4 +317,31 @@ class ContactLogHelper return $sentence; } + + private static function loanCreated(ContactLog $log): string + { + $sentence = trans('contact_log.loan_created', [ + 'loan_name' => $log->object->{'loan_name'}, + ]); + + return $sentence; + } + + private static function loanUpdated(ContactLog $log): string + { + $sentence = trans('contact_log.loan_updated', [ + 'loan_name' => $log->object->{'loan_name'}, + ]); + + return $sentence; + } + + private static function loanDestroyed(ContactLog $log): string + { + $sentence = trans('contact_log.loan_destroyed', [ + 'loan_name' => $log->object->{'loan_name'}, + ]); + + return $sentence; + } } diff --git a/app/Jobs/SetupAccount.php b/app/Jobs/SetupAccount.php index ac73c1483..e8854262f 100644 --- a/app/Jobs/SetupAccount.php +++ b/app/Jobs/SetupAccount.php @@ -283,6 +283,22 @@ class SetupAccount implements ShouldQueue 'template_page_id' => $templatePageSocial->id, 'module_id' => $module->id, ]); + + // Loans + $module = (new CreateModule)->execute([ + 'account_id' => $this->user->account_id, + 'author_id' => $this->user->id, + 'name' => trans('app.module_loans'), + 'type' => Module::TYPE_LOANS, + 'can_be_deleted' => false, + ]); + (new AssociateModuleToTemplatePage)->execute([ + 'account_id' => $this->user->account_id, + 'author_id' => $this->user->id, + 'template_id' => $this->template->id, + 'template_page_id' => $templatePageSocial->id, + 'module_id' => $module->id, + ]); } private function addFirstInformation(): void diff --git a/app/Models/Contact.php b/app/Models/Contact.php index f96454ef2..78e9668ae 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -199,6 +199,30 @@ class Contact extends Model return $this->hasMany(ContactReminder::class); } + /** + * Get the loans associated with the contact as the person who takes the + * loan. + * I know loaner is not a real word, but it's the best I could come up with. + * + * @return BelongsToMany + */ + public function loansAsLoaner() + { + return $this->belongsToMany(Loan::class, 'contact_loan', 'loaner_id'); + } + + /** + * Get the loans associated with the contact as the person who takes the + * loan. + * I know loanee is not a real word, but it's the best I could come up with. + * + * @return BelongsToMany + */ + public function loansAsLoanee() + { + return $this->belongsToMany(Loan::class, 'contact_loan', 'loanee_id'); + } + /** * Get the name of the contact, according to the user preference. * diff --git a/app/Models/ContactFeedItem.php b/app/Models/ContactFeedItem.php index 26c33606c..f2a1ee30f 100644 --- a/app/Models/ContactFeedItem.php +++ b/app/Models/ContactFeedItem.php @@ -21,6 +21,8 @@ class ContactFeedItem extends Model const ACTION_IMPORTANT_DATE_CREATED = 'important_date_created'; const ACTION_IMPORTANT_DATE_UPDATED = 'important_date_updated'; const ACTION_LABEL_ADDED = 'label_added'; + const ACTION_LOAN_CREATED = 'loan_created'; + const ACTION_LOAN_UPDATED = 'loan_updated'; /** * The attributes that are mass assignable. diff --git a/app/Models/ContactImportantDate.php b/app/Models/ContactImportantDate.php index c63fadea1..4850f5b0e 100644 --- a/app/Models/ContactImportantDate.php +++ b/app/Models/ContactImportantDate.php @@ -36,7 +36,6 @@ class ContactImportantDate extends Model 'day', 'month', 'year', - 'type', ]; /** @@ -48,4 +47,14 @@ class ContactImportantDate extends Model { return $this->belongsTo(Contact::class); } + + /** + * Get the important date type associated with the contact date. + * + * @return BelongsTo + */ + public function contactImportantDateType() + { + return $this->belongsTo(ContactImportantDateType::class); + } } diff --git a/app/Models/Loan.php b/app/Models/Loan.php new file mode 100644 index 000000000..7e7ebc5b2 --- /dev/null +++ b/app/Models/Loan.php @@ -0,0 +1,103 @@ + 'boolean', + ]; + + /** + * Get the vault associated with the loan. + * + * @return BelongsTo + */ + public function vault() + { + return $this->belongsTo(Vault::class); + } + + /** + * Get the currency associated with the loan. + * + * @return BelongsTo + */ + public function currency() + { + return $this->belongsTo(Currency::class); + } + + /** + * Get the contact that did the loan. + * + * @return BelongsTo + */ + public function loaners() + { + return $this->belongsToMany(Contact::class, 'contact_loan', 'loan_id', 'loaner_id'); + } + + /** + * Get the contact records the loan was made to. + * + * @return BelongsTo + */ + public function loanees() + { + return $this->belongsToMany(Contact::class, 'contact_loan', 'loan_id', 'loanee_id'); + } + + /** + * Get the loan's feed item. + */ + public function feedItem() + { + return $this->morphOne(ContactFeedItem::class, 'feedable'); + } +} diff --git a/app/Models/Module.php b/app/Models/Module.php index cfb564fb0..e2944af64 100644 --- a/app/Models/Module.php +++ b/app/Models/Module.php @@ -23,6 +23,7 @@ class Module extends Model const TYPE_IMPORTANT_DATES = 'important_dates'; const TYPE_LABELS = 'labels'; const TYPE_REMINDERS = 'reminders'; + const TYPE_LOANS = 'loans'; /** * The attributes that are mass assignable. diff --git a/app/Models/Note.php b/app/Models/Note.php index 86c9d3931..2fb9ead85 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -30,7 +30,7 @@ class Note extends Model * * @return array */ - public function toSearchableArray() + public function toSearchableArray(): array { $array = [ 'id' => $this->id, diff --git a/composer.lock b/composer.lock index efa645aa2..8472138d7 100644 --- a/composer.lock +++ b/composer.lock @@ -8645,16 +8645,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.5.6", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "799dd8c2d2c9c704bb55d2078078cb970cf0f6d1" + "reference": "7fb7e2e1e9f3d59a26a413b2d3d5e47f0edb75ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/799dd8c2d2c9c704bb55d2078078cb970cf0f6d1", - "reference": "799dd8c2d2c9c704bb55d2078078cb970cf0f6d1", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7fb7e2e1e9f3d59a26a413b2d3d5e47f0edb75ac", + "reference": "7fb7e2e1e9f3d59a26a413b2d3d5e47f0edb75ac", "shasum": "" }, "require": { @@ -8680,7 +8680,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.5.6" + "source": "https://github.com/phpstan/phpstan/tree/1.5.7" }, "funding": [ { @@ -8700,7 +8700,7 @@ "type": "tidelift" } ], - "time": "2022-04-15T11:13:37+00:00" + "time": "2022-04-20T12:20:27+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/database/factories/ContactImportantDateFactory.php b/database/factories/ContactImportantDateFactory.php index 4d764fa2b..6b1a4aed7 100644 --- a/database/factories/ContactImportantDateFactory.php +++ b/database/factories/ContactImportantDateFactory.php @@ -4,6 +4,7 @@ namespace Database\Factories; use App\Models\Contact; use App\Models\ContactImportantDate; +use App\Models\ContactImportantDateType; use Illuminate\Database\Eloquent\Factories\Factory; class ContactImportantDateFactory extends Factory @@ -28,7 +29,7 @@ class ContactImportantDateFactory extends Factory 'day' => 29, 'month' => 10, 'year' => 1981, - 'type' => ContactImportantDate::TYPE_BIRTHDATE, + 'contact_important_date_type_id' => ContactImportantDateType::factory(), ]; } } diff --git a/database/factories/LoanFactory.php b/database/factories/LoanFactory.php new file mode 100644 index 000000000..2e5dba714 --- /dev/null +++ b/database/factories/LoanFactory.php @@ -0,0 +1,34 @@ + Vault::factory(), + 'type' => Loan::TYPE_DEBT, + 'name' => $this->faker->word(), + 'description' => $this->faker->sentence(), + 'amount_lent' => $this->faker->randomNumber(), + 'loaned_at' => $this->faker->dateTimeThisCentury(), + ]; + } +} diff --git a/database/migrations/2022_02_09_145139_create_contact_date_table.php b/database/migrations/2022_02_09_145139_create_contact_date_table.php index 56155cc8b..398a84552 100644 --- a/database/migrations/2022_02_09_145139_create_contact_date_table.php +++ b/database/migrations/2022_02_09_145139_create_contact_date_table.php @@ -29,13 +29,14 @@ return new class extends Migration Schema::create('contact_important_dates', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('contact_id'); + $table->unsignedBigInteger('contact_important_date_type_id')->nullable(); $table->string('label'); $table->integer('day')->nullable(); $table->integer('month')->nullable(); $table->integer('year')->nullable(); - $table->string('type')->nullable(); $table->timestamps(); $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('contact_important_date_type_id')->references('id')->on('contact_important_date_types')->onDelete('set null'); }); } diff --git a/database/migrations/2022_03_28_180407_create_currencies_table.php b/database/migrations/2022_03_22_180407_create_currencies_table.php similarity index 93% rename from database/migrations/2022_03_28_180407_create_currencies_table.php rename to database/migrations/2022_03_22_180407_create_currencies_table.php index 3a5512c9d..bab539ff4 100644 --- a/database/migrations/2022_03_28_180407_create_currencies_table.php +++ b/database/migrations/2022_03_22_180407_create_currencies_table.php @@ -23,13 +23,6 @@ return new class extends Migration $table->timestamps(); }); - Schema::create('user_preferred_currency', function (Blueprint $table) { - $table->unsignedBigInteger('currency_id'); - $table->unsignedBigInteger('user_id'); - $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('cascade'); - $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); - }); - Schema::create('account_currencies', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('currency_id'); @@ -216,7 +209,6 @@ return new class extends Migration public function down() { Schema::dropIfExists('currencies'); - Schema::dropIfExists('user_preferred_currency'); Schema::dropIfExists('account_currencies'); } }; diff --git a/database/migrations/2022_03_23_005751_create_loans_table.php b/database/migrations/2022_03_23_005751_create_loans_table.php new file mode 100644 index 000000000..2e7c7166a --- /dev/null +++ b/database/migrations/2022_03_23_005751_create_loans_table.php @@ -0,0 +1,55 @@ +id(); + $table->unsignedBigInteger('vault_id'); + $table->string('type'); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('amount_lent')->nullable(); + $table->unsignedBigInteger('currency_id')->nullable(); + $table->datetime('loaned_at')->nullable(); + $table->boolean('settled')->default(false); + $table->datetime('settled_at')->nullable(); + $table->timestamps(); + $table->foreign('vault_id')->references('id')->on('vaults')->onDelete('cascade'); + $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('set null'); + }); + + Schema::create('contact_loan', function (Blueprint $table) { + $table->unsignedBigInteger('loan_id'); + $table->unsignedBigInteger('loaner_id'); + $table->unsignedBigInteger('loanee_id'); + $table->timestamps(); + $table->foreign('loan_id')->references('id')->on('loans')->onDelete('cascade'); + $table->foreign('loaner_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('loanee_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('loans'); + } +}; diff --git a/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php b/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php index 39e8daa2d..829834283 100644 --- a/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php +++ b/domains/Contact/ManageContact/Web/ViewHelpers/ContactShowViewHelper.php @@ -7,6 +7,7 @@ use App\Models\Module; use App\Models\Contact; use App\Models\TemplatePage; use Illuminate\Support\Collection; +use App\Contact\ManageLoans\Web\ViewHelpers\ModuleLoanViewHelper; use App\Contact\ManageNotes\Web\ViewHelpers\ModuleNotesViewHelper; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use App\Contact\ManageLabels\Web\ViewHelpers\ModuleLabelViewHelper; @@ -153,6 +154,10 @@ class ContactShowViewHelper $data = ModuleRemindersViewHelper::data($contact, $user); } + if ($module->type == Module::TYPE_LOANS) { + $data = ModuleLoanViewHelper::data($contact, $user); + } + $modulesCollection->push([ 'id' => $module->id, 'type' => $module->type, diff --git a/domains/Contact/ManageContactImportantDates/Services/CreateContactImportantDate.php b/domains/Contact/ManageContactImportantDates/Services/CreateContactImportantDate.php index ce1706b81..93cb65af7 100644 --- a/domains/Contact/ManageContactImportantDates/Services/CreateContactImportantDate.php +++ b/domains/Contact/ManageContactImportantDates/Services/CreateContactImportantDate.php @@ -8,10 +8,12 @@ use App\Jobs\CreateContactLog; use App\Models\ContactFeedItem; use App\Interfaces\ServiceInterface; use App\Models\ContactImportantDate; +use App\Models\ContactImportantDateType; class CreateContactImportantDate extends BaseService implements ServiceInterface { private ContactImportantDate $date; + private array $data; /** * Get the validation rules that apply to the service. @@ -25,11 +27,11 @@ class CreateContactImportantDate extends BaseService implements ServiceInterface 'vault_id' => 'required|integer|exists:vaults,id', 'author_id' => 'required|integer|exists:users,id', 'contact_id' => 'required|integer|exists:contacts,id', + 'contact_important_date_type_id' => 'nullable|integer|exists:contact_important_date_types,id', 'label' => 'required|string|max:255', 'day' => 'nullable|integer', 'month' => 'nullable|integer', 'year' => 'nullable|integer', - 'type' => 'nullable|string|max:255', ]; } @@ -56,15 +58,16 @@ class CreateContactImportantDate extends BaseService implements ServiceInterface */ public function execute(array $data): ContactImportantDate { - $this->validateRules($data); + $this->data = $data; + $this->validate(); $this->date = ContactImportantDate::create([ 'contact_id' => $data['contact_id'], + 'contact_important_date_type_id' => $this->valueOrNull($data, 'contact_important_date_type_id'), 'label' => $data['label'], 'day' => $this->valueOrNull($data, 'day'), 'month' => $this->valueOrNull($data, 'month'), 'year' => $this->valueOrNull($data, 'year'), - 'type' => $this->valueOrNull($data, 'type'), ]); $this->log(); @@ -72,6 +75,17 @@ class CreateContactImportantDate extends BaseService implements ServiceInterface return $this->date; } + private function validate(): void + { + $this->validateRules($this->data); + + // make sure the vault matches + if ($this->valueOrNull($this->data, 'contact_important_date_type_id')) { + ContactImportantDateType::where('vault_id', $this->data['vault_id']) + ->findOrFail($this->data['contact_important_date_type_id']); + } + } + private function log(): void { CreateAuditLog::dispatch([ diff --git a/domains/Contact/ManageContactImportantDates/Services/UpdateContactImportantDate.php b/domains/Contact/ManageContactImportantDates/Services/UpdateContactImportantDate.php index 7223443d6..09e6540f6 100644 --- a/domains/Contact/ManageContactImportantDates/Services/UpdateContactImportantDate.php +++ b/domains/Contact/ManageContactImportantDates/Services/UpdateContactImportantDate.php @@ -9,6 +9,7 @@ use App\Jobs\CreateContactLog; use App\Models\ContactFeedItem; use App\Interfaces\ServiceInterface; use App\Models\ContactImportantDate; +use App\Models\ContactImportantDateType; class UpdateContactImportantDate extends BaseService implements ServiceInterface { @@ -28,11 +29,11 @@ class UpdateContactImportantDate extends BaseService implements ServiceInterface 'author_id' => 'required|integer|exists:users,id', 'contact_id' => 'required|integer|exists:contacts,id', 'contact_important_date_id' => 'required|integer|exists:contact_important_dates,id', + 'contact_important_date_type_id' => 'nullable|integer|exists:contact_important_date_types,id', 'label' => 'required|string|max:255', 'day' => 'nullable|integer', 'month' => 'nullable|integer', 'year' => 'nullable|integer', - 'type' => 'nullable|string|max:255', ]; } @@ -73,15 +74,21 @@ class UpdateContactImportantDate extends BaseService implements ServiceInterface $this->date = ContactImportantDate::where('contact_id', $this->contact->id) ->findOrFail($this->data['contact_important_date_id']); + + // make sure the vault matches + if ($this->valueOrNull($this->data, 'contact_important_date_type_id')) { + ContactImportantDateType::where('vault_id', $this->data['vault_id']) + ->findOrFail($this->data['contact_important_date_type_id']); + } } private function update(): void { $this->date->label = $this->data['label']; + $this->date->contact_important_date_type_id = $this->valueOrNull($this->data, 'contact_important_date_type_id'); $this->date->day = $this->valueOrNull($this->data, 'day'); $this->date->month = $this->valueOrNull($this->data, 'month'); $this->date->year = $this->valueOrNull($this->data, 'year'); - $this->date->type = $this->valueOrNull($this->data, 'type'); $this->date->save(); $this->contact->last_updated_at = Carbon::now(); diff --git a/domains/Contact/ManageContactImportantDates/Web/Controllers/ContactImportantDatesController.php b/domains/Contact/ManageContactImportantDates/Web/Controllers/ContactImportantDatesController.php index 58af81cf8..4a6ef2bbd 100644 --- a/domains/Contact/ManageContactImportantDates/Web/Controllers/ContactImportantDatesController.php +++ b/domains/Contact/ManageContactImportantDates/Web/Controllers/ContactImportantDatesController.php @@ -59,7 +59,6 @@ class ContactImportantDatesController extends Controller 'day' => $day, 'month' => $month, 'year' => $year, - 'type' => $request->input('type'), ]); if ($request->input('reminder')) { @@ -114,7 +113,6 @@ class ContactImportantDatesController extends Controller 'day' => $day, 'month' => $month, 'year' => $year, - 'type' => $request->input('type'), ]; $date = (new UpdateContactImportantDate)->execute($data); diff --git a/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ContactImportantDatesViewHelper.php b/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ContactImportantDatesViewHelper.php index 709738f9e..e50e63dca 100644 --- a/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ContactImportantDatesViewHelper.php +++ b/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ContactImportantDatesViewHelper.php @@ -49,7 +49,7 @@ class ContactImportantDatesViewHelper 'id' => $date->id, 'label' => $date->label, 'date' => ImportantDateHelper::formatDate($date, $user), - 'type' => $date->type, + 'type' => $date->contactImportantDateType ? $date->contactImportantDateType->label : null, 'age' => ImportantDateHelper::getAge($date), 'choice' => ImportantDateHelper::determineType($date), 'completeDate' => $completeDate, diff --git a/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ModuleImportantDatesViewHelper.php b/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ModuleImportantDatesViewHelper.php index b97ec1253..7b8312db7 100644 --- a/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ModuleImportantDatesViewHelper.php +++ b/domains/Contact/ManageContactImportantDates/Web/ViewHelpers/ModuleImportantDatesViewHelper.php @@ -17,7 +17,7 @@ class ModuleImportantDatesViewHelper 'id' => $date->id, 'label' => $date->label, 'date' => ImportantDateHelper::formatDate($date, $user), - 'type' => $date->type, + 'type' => $date->contactImportantDateType ? $date->contactImportantDateType->label : null, 'age' => ImportantDateHelper::getAge($date), ]; }); diff --git a/domains/Contact/ManageLoans/Services/CreateLoan.php b/domains/Contact/ManageLoans/Services/CreateLoan.php new file mode 100644 index 000000000..dc0f8ecdd --- /dev/null +++ b/domains/Contact/ManageLoans/Services/CreateLoan.php @@ -0,0 +1,158 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'currency_id' => 'nullable|integer|exists:currencies,id', + 'type' => 'required|string|max:255', + 'name' => 'required|string|max:65535', + 'description' => 'nullable|string|max:65535', + 'loaner_ids' => 'required', + 'loanee_ids' => 'required', + 'amount_lent' => 'nullable|integer', + 'loaned_at' => 'nullable|date_format:Y-m-d', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'author_must_be_vault_editor', + 'contact_must_belong_to_vault', + ]; + } + + /** + * Create a loan. + * + * @param array $data + * @return Loan + */ + public function execute(array $data): Loan + { + $this->data = $data; + $this->validate(); + $this->create(); + + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + + $this->log(); + $this->createFeedItem(); + + return $this->loan; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->loanersCollection = collect(); + foreach ($this->data['loaner_ids'] as $loanerId) { + $this->loanersCollection->push(Contact::where('vault_id', $this->data['vault_id']) + ->findOrFail($loanerId) + ); + } + + $this->loaneesCollection = collect(); + foreach ($this->data['loanee_ids'] as $loaneeId) { + $this->loaneesCollection->push(Contact::where('vault_id', $this->data['vault_id']) + ->findOrFail($loaneeId) + ); + } + } + + private function create(): void + { + $this->loan = Loan::create([ + 'vault_id' => $this->data['vault_id'], + 'type' => $this->data['type'], + 'name' => $this->data['name'], + 'description' => $this->valueOrNull($this->data, 'description'), + 'amount_lent' => $this->valueOrNull($this->data, 'amount_lent'), + 'currency_id' => $this->valueOrNull($this->data, 'currency_id'), + 'loaned_at' => $this->valueOrNull($this->data, 'loaned_at'), + ]); + + foreach ($this->loanersCollection as $loaner) { + foreach ($this->loaneesCollection as $loanee) { + $loaner->loansAsLoaner()->syncWithoutDetaching([$this->loan->id => ['loanee_id' => $loanee->id]]); + } + } + + foreach ($this->loaneesCollection as $loanee) { + foreach ($this->loanersCollection as $loaner) { + $loanee->loansAsLoanee()->syncWithoutDetaching([$this->loan->id => ['loaner_id' => $loaner->id]]); + } + } + } + + private function log(): void + { + CreateAuditLog::dispatch([ + 'account_id' => $this->author->account_id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_created', + 'objects' => json_encode([ + 'contact_id' => $this->contact->id, + 'contact_name' => $this->contact->name, + 'loan_name' => $this->loan->name, + ]), + ])->onQueue('low'); + + CreateContactLog::dispatch([ + 'contact_id' => $this->contact->id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_created', + 'objects' => json_encode([ + 'loan_name' => $this->loan->name, + ]), + ])->onQueue('low'); + } + + private function createFeedItem(): void + { + $feedItem = ContactFeedItem::create([ + 'contact_id' => $this->contact->id, + 'action' => ContactFeedItem::ACTION_LOAN_CREATED, + ]); + $this->loan->feedItem()->save($feedItem); + } +} diff --git a/domains/Contact/ManageLoans/Services/DestroyLoan.php b/domains/Contact/ManageLoans/Services/DestroyLoan.php new file mode 100644 index 000000000..5aeedda4e --- /dev/null +++ b/domains/Contact/ManageLoans/Services/DestroyLoan.php @@ -0,0 +1,94 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'loan_id' => 'required|integer|exists:loans,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'contact_must_belong_to_vault', + 'author_must_be_vault_editor', + ]; + } + + /** + * Destroy a loan. + * + * @param array $data + */ + public function execute(array $data): void + { + $this->validateRules($data); + + $this->loan = Loan::where('vault_id', $data['vault_id'])->findOrFail($data['loan_id']); + + $this->removeContactFeedItem(); + + $this->loan->delete(); + + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + + $this->log(); + } + + private function log(): void + { + CreateAuditLog::dispatch([ + 'account_id' => $this->author->account_id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_destroyed', + 'objects' => json_encode([ + 'contact_id' => $this->contact->id, + 'contact_name' => $this->contact->name, + ]), + ])->onQueue('low'); + + CreateContactLog::dispatch([ + 'contact_id' => $this->contact->id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_destroyed', + 'objects' => json_encode([]), + ])->onQueue('low'); + } + + private function removeContactFeedItem(): void + { + $this->loan->feedItem()->delete(); + } +} diff --git a/domains/Contact/ManageLoans/Services/ToggleLoan.php b/domains/Contact/ManageLoans/Services/ToggleLoan.php new file mode 100644 index 000000000..129d3f298 --- /dev/null +++ b/domains/Contact/ManageLoans/Services/ToggleLoan.php @@ -0,0 +1,78 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'loan_id' => 'required|integer|exists:loans,id', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'contact_must_belong_to_vault', + 'author_must_be_vault_editor', + ]; + } + + /** + * Toggle a loan. + * + * @param array $data + * @return Loan + */ + public function execute(array $data): Loan + { + $this->data = $data; + $this->validate(); + $this->toggle(); + + return $this->loan; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->loan = Loan::where('vault_id', $this->data['vault_id']) + ->findOrFail($this->data['loan_id']); + } + + private function toggle(): void + { + $this->loan->settled = ! $this->loan->settled; + $this->loan->settled_at = Carbon::now(); + $this->loan->save(); + + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + } +} diff --git a/domains/Contact/ManageLoans/Services/UpdateLoan.php b/domains/Contact/ManageLoans/Services/UpdateLoan.php new file mode 100644 index 000000000..d999ec21d --- /dev/null +++ b/domains/Contact/ManageLoans/Services/UpdateLoan.php @@ -0,0 +1,163 @@ + 'required|integer|exists:accounts,id', + 'vault_id' => 'required|integer|exists:vaults,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'loan_id' => 'required|integer|exists:loans,id', + 'currency_id' => 'nullable|integer|exists:currencies,id', + 'type' => 'required|string|max:255', + 'name' => 'required|string|max:65535', + 'description' => 'nullable|string|max:65535', + 'loaner_ids' => 'required', + 'loanee_ids' => 'required', + 'amount_lent' => 'nullable|integer', + 'loaned_at' => 'nullable|date_format:Y-m-d', + ]; + } + + /** + * Get the permissions that apply to the user calling the service. + * + * @return array + */ + public function permissions(): array + { + return [ + 'author_must_belong_to_account', + 'vault_must_belong_to_account', + 'contact_must_belong_to_vault', + 'author_must_be_vault_editor', + ]; + } + + /** + * Update a loan. + * + * @param array $data + * @return Loan + */ + public function execute(array $data): Loan + { + $this->data = $data; + $this->validate(); + $this->update(); + $this->createFeedItem(); + $this->log(); + + return $this->loan; + } + + private function validate(): void + { + $this->validateRules($this->data); + + $this->loan = Loan::where('vault_id', $this->data['vault_id']) + ->findOrFail($this->data['loan_id']); + + $this->loanersCollection = collect(); + foreach ($this->data['loaner_ids'] as $loanerId) { + $this->loanersCollection->push(Contact::where('vault_id', $this->data['vault_id']) + ->findOrFail($loanerId) + ); + } + + $this->loaneesCollection = collect(); + foreach ($this->data['loanee_ids'] as $loaneeId) { + $this->loaneesCollection->push(Contact::where('vault_id', $this->data['vault_id']) + ->findOrFail($loaneeId) + ); + } + } + + private function update(): void + { + $this->loan->type = $this->data['type']; + $this->loan->name = $this->data['name']; + $this->loan->description = $this->valueOrNull($this->data, 'description'); + $this->loan->amount_lent = $this->valueOrNull($this->data, 'amount_lent'); + $this->loan->loaned_at = $this->valueOrNull($this->data, 'loaned_at'); + $this->loan->currency_id = $this->valueOrNull($this->data, 'currency_id'); + $this->loan->save(); + + // remove all the current loaners and loanees + DB::table('contact_loan')->where('loan_id', $this->loan->id)->delete(); + + foreach ($this->loanersCollection as $loaner) { + foreach ($this->loaneesCollection as $loanee) { + $loaner->loansAsLoaner()->syncWithoutDetaching([$this->loan->id => ['loanee_id' => $loanee->id]]); + } + } + + foreach ($this->loaneesCollection as $loanee) { + foreach ($this->loanersCollection as $loaner) { + $loanee->loansAsLoanee()->syncWithoutDetaching([$this->loan->id => ['loaner_id' => $loaner->id]]); + } + } + + $this->contact->last_updated_at = Carbon::now(); + $this->contact->save(); + } + + private function log(): void + { + CreateAuditLog::dispatch([ + 'account_id' => $this->author->account_id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_updated', + 'objects' => json_encode([ + 'contact_id' => $this->contact->id, + 'contact_name' => $this->contact->name, + 'loan_name' => $this->loan->name, + ]), + ])->onQueue('low'); + + CreateContactLog::dispatch([ + 'contact_id' => $this->contact->id, + 'author_id' => $this->author->id, + 'author_name' => $this->author->name, + 'action_name' => 'loan_updated', + 'objects' => json_encode([ + 'loan_name' => $this->loan->name, + ]), + ])->onQueue('low'); + } + + private function createFeedItem(): void + { + $feedItem = ContactFeedItem::create([ + 'contact_id' => $this->contact->id, + 'action' => ContactFeedItem::ACTION_LOAN_UPDATED, + ]); + $this->loan->feedItem()->save($feedItem); + } +} diff --git a/domains/Contact/ManageLoans/Web/Controllers/ContactModuleLoanController.php b/domains/Contact/ManageLoans/Web/Controllers/ContactModuleLoanController.php new file mode 100644 index 000000000..4e0465e6f --- /dev/null +++ b/domains/Contact/ManageLoans/Web/Controllers/ContactModuleLoanController.php @@ -0,0 +1,98 @@ +input('amount_lent')) { + $amount = $request->input('amount_lent') * 100; + } + + $loaners = collect($request->input('loaners'))->pluck('id'); + $loanees = collect($request->input('loanees'))->pluck('id'); + + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'currency_id' => $request->input('currency_id'), + 'type' => $request->input('type'), + 'name' => $request->input('name'), + 'description' => $request->input('description'), + 'loaner_ids' => $loaners, + 'loanee_ids' => $loanees, + 'amount_lent' => $request->input('amount_lent') ? $amount : null, + 'loaned_at' => $request->input('loaned_at'), + ]; + + $loan = (new CreateLoan)->execute($data); + + $contact = Contact::find($contactId); + + return response()->json([ + 'data' => ModuleLoanViewHelper::dtoLoan($loan, $contact, Auth::user()), + ], 201); + } + + public function update(Request $request, int $vaultId, int $contactId, int $loanId) + { + if ($request->input('amount_lent')) { + $amount = $request->input('amount_lent') * 100; + } + + $loaners = collect($request->input('loaners'))->pluck('id'); + $loanees = collect($request->input('loanees'))->pluck('id'); + + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'loan_id' => $loanId, + 'currency_id' => $request->input('currency_id'), + 'type' => $request->input('type'), + 'name' => $request->input('name'), + 'description' => $request->input('description'), + 'loaner_ids' => $loaners, + 'loanee_ids' => $loanees, + 'amount_lent' => $request->input('amount_lent') ? $amount : null, + 'loaned_at' => $request->input('loaned_at'), + ]; + + $loan = (new UpdateLoan)->execute($data); + $contact = Contact::find($contactId); + + return response()->json([ + 'data' => ModuleLoanViewHelper::dtoLoan($loan, $contact, Auth::user()), + ], 200); + } + + public function destroy(Request $request, int $vaultId, int $contactId, int $loanId) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'loan_id' => $loanId, + ]; + + (new DestroyLoan)->execute($data); + + return response()->json([ + 'data' => true, + ], 200); + } +} diff --git a/domains/Contact/ManageLoans/Web/Controllers/ContactModuleToggleLoanController.php b/domains/Contact/ManageLoans/Web/Controllers/ContactModuleToggleLoanController.php new file mode 100644 index 000000000..9868cc8f0 --- /dev/null +++ b/domains/Contact/ManageLoans/Web/Controllers/ContactModuleToggleLoanController.php @@ -0,0 +1,31 @@ + Auth::user()->account_id, + 'author_id' => Auth::user()->id, + 'vault_id' => $vaultId, + 'contact_id' => $contactId, + 'loan_id' => $loanId, + ]; + + $loan = (new ToggleLoan)->execute($data); + $contact = Contact::find($contactId); + + return response()->json([ + 'data' => ModuleLoanViewHelper::dtoLoan($loan, $contact, Auth::user()), + ], 200); + } +} diff --git a/domains/Contact/ManageLoans/Web/ViewHelpers/ModuleLoanViewHelper.php b/domains/Contact/ManageLoans/Web/ViewHelpers/ModuleLoanViewHelper.php new file mode 100644 index 000000000..7128760fd --- /dev/null +++ b/domains/Contact/ManageLoans/Web/ViewHelpers/ModuleLoanViewHelper.php @@ -0,0 +1,87 @@ +loansAsLoaner()->where('settled', false)->get(); + $loansAsLoanee = $contact->loansAsLoanee()->where('settled', false)->get(); + + $loans = $loansAsLoaner->concat($loansAsLoanee)->sortBy('loaned_at')->unique('id'); + + $loansAssociatedWithContactCollection = $loans->map(function ($loan) use ($contact, $user) { + return self::dtoLoan($loan, $contact, $user); + }); + + return [ + 'loans' => $loansAssociatedWithContactCollection, + 'current_date' => Carbon::now()->format('Y-m-d'), + 'url' => [ + 'currencies' => route('currencies.index'), + 'store' => route('contact.loan.store', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + ]), + ], + ]; + } + + public static function dtoLoan(Loan $loan, Contact $contact, User $user): array + { + $loaners = $loan->loaners->unique('id'); + $loanees = $loan->loanees->unique('id'); + $loanersCollection = $loaners->map(function ($loaner) use ($user) { + return [ + 'id' => $loaner->id, + 'name' => $loaner->getName($user), + ]; + }); + $loaneesCollection = $loanees->map(function ($loanee) use ($user) { + return [ + 'id' => $loanee->id, + 'name' => $loanee->getName($user), + ]; + }); + + return [ + 'id' => $loan->id, + 'type' => $loan->type, + 'name' => $loan->name, + 'description' => $loan->description, + 'amount_lent' => $loan->amount_lent / 100, + 'currency_id' => $loan->currency_id, + 'currency_name' => $loan->currency ? $loan->currency->code : null, + 'loaned_at' => $loan->loaned_at->format('Y-m-d'), + 'loaned_at_human_format' => DateHelper::format($loan->loaned_at, $user), + 'loaners' => $loanersCollection, + 'loanees' => $loaneesCollection, + 'settled' => $loan->settled, + 'settled_at_human_format' => $loan->settled_at ? DateHelper::format($loan->settled_at, $user) : null, + 'url' => [ + 'update' => route('contact.loan.update', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'loan' => $loan->id, + ]), + 'toggle' => route('contact.loan.toggle', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'loan' => $loan->id, + ]), + 'destroy' => route('contact.loan.destroy', [ + 'vault' => $contact->vault_id, + 'contact' => $contact->id, + 'loan' => $loan->id, + ]), + ], + ]; + } +} diff --git a/domains/Settings/ManageCurrencies/Web/Controllers/CurrencyController.php b/domains/Settings/ManageCurrencies/Web/Controllers/CurrencyController.php new file mode 100644 index 000000000..67a0acc1c --- /dev/null +++ b/domains/Settings/ManageCurrencies/Web/Controllers/CurrencyController.php @@ -0,0 +1,20 @@ +account); + + return response()->json([ + 'data' => $currenciesCollection, + ], 201); + } +} diff --git a/domains/Settings/ManageCurrencies/Web/Controllers/PersonalizeCurrencyController.php b/domains/Settings/ManageCurrencies/Web/Controllers/PersonalizeCurrencyController.php index baf729127..c0a7aeac4 100644 --- a/domains/Settings/ManageCurrencies/Web/Controllers/PersonalizeCurrencyController.php +++ b/domains/Settings/ManageCurrencies/Web/Controllers/PersonalizeCurrencyController.php @@ -22,6 +22,20 @@ class PersonalizeCurrencyController extends Controller ]); } + public function store(Request $request) + { + $data = [ + 'account_id' => Auth::user()->account_id, + 'author_id' => Auth::user()->id, + ]; + + (new EnableAllCurrencies)->execute($data); + + return response()->json([ + 'data' => true, + ], 201); + } + public function update(Request $request, int $currencyId) { $data = [ @@ -37,20 +51,6 @@ class PersonalizeCurrencyController extends Controller ], 200); } - public function store(Request $request) - { - $data = [ - 'account_id' => Auth::user()->account_id, - 'author_id' => Auth::user()->id, - ]; - - (new EnableAllCurrencies)->execute($data); - - return response()->json([ - 'data' => true, - ], 201); - } - public function destroy(Request $request) { $data = [ @@ -62,6 +62,6 @@ class PersonalizeCurrencyController extends Controller return response()->json([ 'data' => true, - ], 201); + ], 200); } } diff --git a/domains/Settings/ManageCurrencies/Web/ViewHelpers/CurrencyIndexViewHelper.php b/domains/Settings/ManageCurrencies/Web/ViewHelpers/CurrencyIndexViewHelper.php new file mode 100644 index 000000000..cf7d74991 --- /dev/null +++ b/domains/Settings/ManageCurrencies/Web/ViewHelpers/CurrencyIndexViewHelper.php @@ -0,0 +1,23 @@ +currencies() + ->where('active', true)->get()->map(function ($currency) use ($currencyId) { + return [ + 'id' => $currency->id, + 'name' => $currency->code, + 'selected' => $currencyId ? $currencyId === $currency->id : null, + ]; + }); + + return $currenciesCollection; + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 7c298d574..1eca20358 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,6 +6,10 @@ body { color: #343a4b; } +.bg-form { + background-color: #f8fbfd; +} + .icon-breadcrumb { top: -1px; } diff --git a/resources/js/Pages/Settings/Notifications/Partials/Emails.vue b/resources/js/Pages/Settings/Notifications/Partials/Emails.vue index 5a7ca1d1a..79516eebf 100644 --- a/resources/js/Pages/Settings/Notifications/Partials/Emails.vue +++ b/resources/js/Pages/Settings/Notifications/Partials/Emails.vue @@ -33,7 +33,7 @@ select {