Skip to content

Commit

Permalink
feat: manage number format (monicahq/chandler#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
djaiss authored Apr 4, 2022
1 parent 7b99a0d commit 694914b
Show file tree
Hide file tree
Showing 13 changed files with 484 additions and 5 deletions.
68 changes: 68 additions & 0 deletions app/Helpers/MonetaryNumberHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace App\Helpers;

use App\Models\User;
use Illuminate\Support\Str;

class MonetaryNumberHelper
{
/**
* Format the number according to the user preferences.
* We don't use the number_format PHP function because it rounds the number.
* We also don't use the Money library because it requires the intl PHP
* extension and we want to keep the software as far away as dependencies
* as possible.
*
* @param User $user
* @param int $number
* @return string
*/
public static function format(User $user, int $number): string
{
$formattedNumber = '';
$numberAsString = (string) $number;
$length = Str::length($numberAsString);
$thousands = Str::substr($numberAsString, 0, $length - 2);
$lengthThousands = Str::length($thousands);
$decimals = Str::substr($numberAsString, $length - 2, 2);

switch ($user->number_format) {
// 1,234.56
case User::NUMBER_FORMAT_TYPE_COMMA_THOUSANDS_DOT_DECIMAL:
for ($i = 0; $i < $lengthThousands; $i++) {
if (($i % 3 == 0) && $i) {
$formattedNumber = ','.$formattedNumber;
}
$formattedNumber = $thousands[$lengthThousands - $i - 1].$formattedNumber;
}
$formattedNumber = $formattedNumber.'.'.$decimals;
break;

// 1 234,56
case User::NUMBER_FORMAT_TYPE_SPACE_THOUSANDS_COMMA_DECIMAL:
for ($i = 0; $i < $lengthThousands; $i++) {
if (($i % 3 == 0) && $i) {
$formattedNumber = ' '.$formattedNumber;
}
$formattedNumber = $thousands[$lengthThousands - $i - 1].$formattedNumber;
}
$formattedNumber = $formattedNumber.','.$decimals;
break;

// 1234.56
case User::NUMBER_FORMAT_TYPE_NO_SPACE_DOT_DECIMAL:
for ($i = 0; $i < $lengthThousands; $i++) {
$formattedNumber = $thousands[$lengthThousands - $i - 1].$formattedNumber;
}
$formattedNumber = $formattedNumber.'.'.$decimals;
break;

default:
$formattedNumber = '';
break;
}

return $formattedNumber;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Http\Controllers\Settings\Preferences;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use App\Services\User\Preferences\StoreNumberFormatPreference;
use App\Http\Controllers\Settings\Preferences\ViewHelpers\PreferencesIndexViewHelper;

class PreferencesNumberFormatController extends Controller
{
public function store(Request $request)
{
$data = [
'account_id' => Auth::user()->account_id,
'author_id' => Auth::user()->id,
'number_format' => $request->input('numberFormat'),
];

$user = (new StoreNumberFormatPreference)->execute($data);

return response()->json([
'data' => PreferencesIndexViewHelper::dtoNumberFormat($user),
], 200);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static function data(User $user): array
'name_order' => self::dtoNameOrder($user),
'date_format' => self::dtoDateFormat($user),
'timezone' => self::dtoTimezone($user),
'number_format' => self::dtoNumberFormat($user),
'url' => [
'settings' => route('settings.index'),
'back' => route('settings.index'),
Expand Down Expand Up @@ -88,4 +89,32 @@ public static function dtoTimezone(User $user): array
],
];
}

public static function dtoNumberFormat(User $user): array
{
$collection = collect();
$collection->push([
'id' => 1,
'format' => '1,234.56',
'value' => User::NUMBER_FORMAT_TYPE_COMMA_THOUSANDS_DOT_DECIMAL,
]);
$collection->push([
'id' => 2,
'format' => '1 234,56',
'value' => User::NUMBER_FORMAT_TYPE_SPACE_THOUSANDS_COMMA_DECIMAL,
]);
$collection->push([
'id' => 3,
'format' => '1234.56',
'value' => User::NUMBER_FORMAT_TYPE_NO_SPACE_DOT_DECIMAL,
]);

return [
'numbers' => $collection,
'number_format' => $user->number_format,
'url' => [
'store' => route('settings.preferences.number.store'),
],
];
}
}
8 changes: 8 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable, HasFactory, HasApiTokens;

/**
* Possible number format types.
*/
const NUMBER_FORMAT_TYPE_COMMA_THOUSANDS_DOT_DECIMAL = '1,234.56';
const NUMBER_FORMAT_TYPE_SPACE_THOUSANDS_COMMA_DECIMAL = '1 234,56';
const NUMBER_FORMAT_TYPE_NO_SPACE_DOT_DECIMAL = '1234.56';

/**
* The attributes that are mass assignable.
*
Expand All @@ -32,6 +39,7 @@ class User extends Authenticatable implements MustVerifyEmail
'invitation_accepted_at',
'name_order',
'date_format',
'number_format',
'timezone',
];

Expand Down
60 changes: 60 additions & 0 deletions app/Services/User/Preferences/StoreNumberFormatPreference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\Services\User\Preferences;

use App\Models\User;
use App\Services\BaseService;
use App\Interfaces\ServiceInterface;

class StoreNumberFormatPreference extends BaseService implements ServiceInterface
{
private array $data;

/**
* Get the validation rules that apply to the service.
*
* @return array
*/
public function rules(): array
{
return [
'account_id' => 'required|integer|exists:accounts,id',
'author_id' => 'required|integer|exists:users,id',
'number_format' => 'required|string|max:255',
];
}

/**
* Get the permissions that apply to the user calling the service.
*
* @return array
*/
public function permissions(): array
{
return [
'author_must_belong_to_account',
];
}

/**
* Store date format preferences for the given user.
*
* @param array $data
* @return User
*/
public function execute(array $data): User
{
$this->data = $data;

$this->validateRules($data);
$this->updateUser();

return $this->author;
}

private function updateUser(): void
{
$this->author->number_format = $this->data['number_format'];
$this->author->save();
}
}
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0",
"php": "^8.0",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"http-interop/http-factory-guzzle": "^1.2",
Expand Down
8 changes: 5 additions & 3 deletions database/migrations/2014_10_12_000000_create_users_table.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use App\Models\User;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
Expand All @@ -21,13 +22,14 @@ public function up()
$table->string('last_name')->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('name_order')->default('%first_name% %last_name%');
$table->string('date_format')->default('MMM DD, YYYY');
$table->string('timezone')->nullable();
$table->string('number_format')->default(User::NUMBER_FORMAT_TYPE_COMMA_THOUSANDS_DOT_DECIMAL);
$table->string('password')->nullable();
$table->boolean('is_account_administrator')->default(false);
$table->string('invitation_code')->nullable();
$table->dateTime('invitation_accepted_at')->nullable();
$table->string('name_order')->default('%first_name% %last_name%');
$table->string('date_format')->default('MMM DD, YYYY');
$table->string('timezone')->nullable();
$table->rememberToken();
$table->timestamps();
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
Expand Down
4 changes: 4 additions & 0 deletions resources/js/Pages/Settings/Preferences/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@

<date-format :data="data.date_format" />

<number-format :data="data.number_format" />

<timezone :data="data.timezone" />
</div>
</main>
Expand All @@ -59,6 +61,7 @@
import Layout from '@/Shared/Layout';
import NameOrder from '@/Pages/Settings/Preferences/Partials/NameOrder';
import DateFormat from '@/Pages/Settings/Preferences/Partials/DateFormat';
import NumberFormat from '@/Pages/Settings/Preferences/Partials/NumberFormat';
import Timezone from '@/Pages/Settings/Preferences/Partials/Timezone';
export default {
Expand All @@ -67,6 +70,7 @@ export default {
NameOrder,
DateFormat,
Timezone,
NumberFormat,
},
props: {
Expand Down
116 changes: 116 additions & 0 deletions resources/js/Pages/Settings/Preferences/Partials/NumberFormat.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<style lang="scss" scoped>
pre {
background-color: #1f2937;
color: #c9ef78;
}
.example {
border-bottom-left-radius: 9px;
border-bottom-right-radius: 9px;
}
</style>

<template>
<div class="mb-16">
<!-- title + cta -->
<div class="mb-3 mt-8 items-center justify-between sm:mt-0 sm:flex">
<h3 class="mb-4 sm:mb-0"><span class="mr-1">💵</span> How should we display monetary values</h3>
<pretty-button v-if="!editMode" :text="'Edit'" @click="enableEditMode" />
</div>

<!-- normal mode -->
<div v-if="!editMode" class="mb-6 rounded-lg border border-gray-200 bg-white">
<p class="px-5 py-2">
<span class="mb-2 block">Current way of displaying numbers:</span>
<span class="mb-2 block rounded bg-slate-100 px-5 py-2 text-sm">{{ localNumberFormat }}</span>
</p>
</div>

<!-- edit mode -->
<form v-if="editMode" class="mb-6 rounded-lg border border-gray-200 bg-white" @submit.prevent="submit()">
<div class="border-b border-gray-200 px-5 py-2">
<errors :errors="form.errors" />

<div v-for="numberFormat in data.numbers" :key="numberFormat.id" class="mb-2 flex items-center">
<input
:id="'input' + numberFormat.id"
v-model="form.numberFormat"
:value="numberFormat.format"
name="date-format"
type="radio"
class="h-4 w-4 border-gray-300 text-sky-500" />
<label :for="'input' + numberFormat.id" class="ml-3 block cursor-pointer text-sm font-medium text-gray-700">
{{ numberFormat.value }}
</label>
</div>
</div>

<!-- actions -->
<div class="flex justify-between p-5">
<pretty-link :text="'Cancel'" :classes="'mr-3'" @click="editMode = false" />
<pretty-button :text="'Save'" :state="loadingState" :icon="'check'" :classes="'save'" />
</div>
</form>
</div>
</template>

<script>
import PrettyButton from '@/Shared/Form/PrettyButton';
import PrettyLink from '@/Shared/Form/PrettyLink';
import Errors from '@/Shared/Form/Errors';
export default {
components: {
PrettyButton,
PrettyLink,
Errors,
},
props: {
data: {
type: Object,
default: null,
},
},
data() {
return {
loadingState: '',
editMode: false,
localNumberFormat: '',
form: {
numberFormat: '',
errors: [],
},
};
},
mounted() {
this.localNumberFormat = this.data.number_format;
this.form.numberFormat = this.data.number_format;
},
methods: {
enableEditMode() {
this.editMode = true;
},
submit() {
this.loadingState = 'loading';
axios
.post(this.data.url.store, this.form)
.then((response) => {
this.flash('Changes saved', 'success');
this.localNumberFormat = this.form.numberFormat;
this.editMode = false;
this.loadingState = null;
})
.catch((error) => {
this.loadingState = null;
this.form.errors = error.response.data;
});
},
},
};
</script>
Loading

0 comments on commit 694914b

Please sign in to comment.