diff --git a/app/Api/V2/Controllers/Model/Bill/SumController.php b/app/Api/V2/Controllers/Model/Bill/SumController.php new file mode 100644 index 0000000000..34378f6064 --- /dev/null +++ b/app/Api/V2/Controllers/Model/Bill/SumController.php @@ -0,0 +1,78 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\Model\Bill; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Api\V2\Request\Generic\DateRequest; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Support\Http\Api\ConvertsExchangeRates; +use Illuminate\Http\JsonResponse; + +/** + * Class SumController + */ +class SumController extends Controller +{ + private BillRepositoryInterface $repository; + use ConvertsExchangeRates; + + /** + * + */ + public function __construct() + { + $this->middleware( + function ($request, $next) { + $this->repository = app(BillRepositoryInterface::class); + return $next($request); + } + ); + } + + /** + * @param DateRequest $request + * @return JsonResponse + */ + public function unpaid(DateRequest $request): JsonResponse + { + $dates = $request->getAll(); + $result = $this->repository->sumUnpaidInRange($dates['start'], $dates['end']); + $converted = $this->cerSum($result); + + // convert to JSON response: + return response()->json($converted); + } + + /** + * @param DateRequest $request + * @return JsonResponse + */ + public function paid(DateRequest $request): JsonResponse + { + $dates = $request->getAll(); + $result = $this->repository->sumPaidInRange($dates['start'], $dates['end']); + $converted = $this->cerSum($result); + + // convert to JSON response: + return response()->json($converted); + } +} diff --git a/app/Api/V2/Controllers/Model/Budget/SumController.php b/app/Api/V2/Controllers/Model/Budget/SumController.php new file mode 100644 index 0000000000..c4b524b87d --- /dev/null +++ b/app/Api/V2/Controllers/Model/Budget/SumController.php @@ -0,0 +1,65 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\Model\Budget; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Api\V2\Request\Generic\DateRequest; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Support\Http\Api\ConvertsExchangeRates; +use Illuminate\Http\JsonResponse; + +/** + * Class SumController + */ +class SumController extends Controller +{ + use ConvertsExchangeRates; + + private BudgetRepositoryInterface $repository; + + /** + * + */ + public function __construct() + { + $this->middleware( + function ($request, $next) { + $this->repository = app(BudgetRepositoryInterface::class); + return $next($request); + } + ); + } + + /** + * @param DateRequest $request + * @return JsonResponse + */ + public function budgeted(DateRequest $request): JsonResponse + { + $data = $request->getAll(); + $result = $this->repository->budgetedInPeriod($data['start'], $data['end']); + $converted = $this->cerSum(array_values($result)); + + return response()->json($converted); + } + +} diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php index 2d8c2ca22d..f4f206b9a8 100644 --- a/app/Handlers/Events/UserEventHandler.php +++ b/app/Handlers/Events/UserEventHandler.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Handlers\Events; use Carbon\Carbon; +use Database\Seeders\ExchangeRateSeeder; use Exception; use FireflyIII\Events\ActuallyLoggedIn; use FireflyIII\Events\DetectedNewIPAddress; @@ -75,6 +76,17 @@ class UserEventHandler return true; } + /** + * @param RegisteredUser $event + * @return bool + */ + public function createExchangeRates(RegisteredUser $event): bool { + $seeder = new ExchangeRateSeeder; + $seeder->run(); + + return true; + } + /** * Fires to see if a user is admin. * diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index 6929f4c470..6a8270b19e 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -24,47 +24,17 @@ namespace FireflyIII\Models; use Eloquent; use FireflyIII\User; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Carbon; /** - * FireflyIII\Models\CurrencyExchangeRate - * - * @property int $id - * @property Carbon|null $created_at - * @property Carbon|null $updated_at - * @property string|null $deleted_at - * @property int $user_id - * @property int $from_currency_id - * @property int $to_currency_id - * @property Carbon $date - * @property string $rate - * @property string|null $user_rate - * @property-read TransactionCurrency $fromCurrency - * @property-read TransactionCurrency $toCurrency - * @property-read User $user - * @method static Builder|CurrencyExchangeRate newModelQuery() - * @method static Builder|CurrencyExchangeRate newQuery() - * @method static Builder|CurrencyExchangeRate query() - * @method static Builder|CurrencyExchangeRate whereCreatedAt($value) - * @method static Builder|CurrencyExchangeRate whereDate($value) - * @method static Builder|CurrencyExchangeRate whereDeletedAt($value) - * @method static Builder|CurrencyExchangeRate whereFromCurrencyId($value) - * @method static Builder|CurrencyExchangeRate whereId($value) - * @method static Builder|CurrencyExchangeRate whereRate($value) - * @method static Builder|CurrencyExchangeRate whereToCurrencyId($value) - * @method static Builder|CurrencyExchangeRate whereUpdatedAt($value) - * @method static Builder|CurrencyExchangeRate whereUserId($value) - * @method static Builder|CurrencyExchangeRate whereUserRate($value) - * @mixin Eloquent + * Class CurrencyExchangeRate */ class CurrencyExchangeRate extends Model { /** @var array Convert these fields to other data types */ protected $casts - = [ + = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'user_id' => 'int', @@ -72,6 +42,7 @@ class CurrencyExchangeRate extends Model 'to_currency_id' => 'int', 'date' => 'datetime', ]; + protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'rate']; /** * @codeCoverageIgnore diff --git a/app/Models/Preference.php b/app/Models/Preference.php index d743f360ad..1e71699c52 100644 --- a/app/Models/Preference.php +++ b/app/Models/Preference.php @@ -83,6 +83,9 @@ class Preference extends Model $user = auth()->user(); /** @var Preference|null $preference */ $preference = $user->preferences()->where('name', $value)->first(); + if (null === $preference) { + $preference = $user->preferences()->where('id', (int) $value)->first(); + } if (null !== $preference) { return $preference; } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 0e867b4732..d046e841c6 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -69,6 +69,7 @@ class EventServiceProvider extends ServiceProvider 'FireflyIII\Handlers\Events\UserEventHandler@sendRegistrationMail', 'FireflyIII\Handlers\Events\UserEventHandler@attachUserRole', 'FireflyIII\Handlers\Events\UserEventHandler@createGroupMembership', + 'FireflyIII\Handlers\Events\UserEventHandler@createExchangeRates', ], // is a User related event. Login::class => [ diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php index 423a7a5451..81490957dd 100644 --- a/app/Repositories/Budget/BudgetLimitRepository.php +++ b/app/Repositories/Budget/BudgetLimitRepository.php @@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; +use JsonException; use Log; /** @@ -304,7 +305,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface * * @return BudgetLimit * @throws FireflyException - * @throws \JsonException + * @throws JsonException */ public function store(array $data): BudgetLimit { @@ -354,7 +355,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface * * @return BudgetLimit * @throws FireflyException - * @throws \JsonException + * @throws JsonException */ public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit { diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 705d1ab03c..855c3aa60a 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -39,6 +39,7 @@ use FireflyIII\Services\Internal\Destroy\BudgetDestroyService; use FireflyIII\User; use Illuminate\Database\QueryException; use Illuminate\Support\Collection; +use JsonException; use Log; use Storage; @@ -80,6 +81,60 @@ class BudgetRepository implements BudgetRepositoryInterface return $search->take($limit)->get(); } + /** + * @inheritDoc + */ + public function budgetedInPeriod(Carbon $start, Carbon $end): array + { + Log::debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d'))); + $return = []; + /** @var BudgetLimitRepository $limitRepository */ + $limitRepository = app(BudgetLimitRepository::class); + $limitRepository->setUser($this->user); + $budgets = $this->getActiveBudgets(); + /** @var Budget $budget */ + foreach ($budgets as $budget) { + Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name)); + $limits = $limitRepository->getBudgetLimits($budget, $start, $end); + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + Log::debug(sprintf('Budget limit #%d', $limit->id)); + $currency = $limit->transactionCurrency; + $return[$currency->id] = $return[$currency->id] ?? [ + 'id' => (string) $currency->id, + 'name' => $currency->name, + 'symbol' => $currency->symbol, + 'code' => $currency->code, + 'decimal_places' => $currency->decimal_places, + 'sum' => '0', + ]; + // same period + if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) { + $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount); + Log::debug(sprintf('Add full amount [1]: %s', $limit->amount)); + continue; + } + // limit is inside of date range + if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) { + $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount); + Log::debug(sprintf('Add full amount [2]: %s', $limit->amount)); + continue; + } + $total = $limit->start_date->diffInDays($limit->end_date) + 1; // include the day itself. + $days = $this->daysInOverlap($limit, $start, $end); + $amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days); + $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); + Log::debug(sprintf('Amount per day: %s (%s over %d days). Total amount for %d days: %s', + bcdiv((string) $limit->amount, (string) $total), + $limit->amount, + $total, + $days, + $amount)); + } + } + return $return; + } + /** * @return bool */ @@ -335,7 +390,7 @@ class BudgetRepository implements BudgetRepositoryInterface * * @return Budget * @throws FireflyException - * @throws \JsonException + * @throws JsonException */ public function store(array $data): Budget { @@ -551,7 +606,7 @@ class BudgetRepository implements BudgetRepositoryInterface * @param Budget $budget * @param array $data * @throws FireflyException - * @throws \JsonException + * @throws JsonException */ private function updateAutoBudget(Budget $budget, array $data): void { @@ -597,4 +652,42 @@ class BudgetRepository implements BudgetRepositoryInterface $autoBudget->save(); } + + /** + * How many days of this budget limit are between start and end? + * + * @param BudgetLimit $limit + * @param Carbon $start + * @param Carbon $end + * @return int + */ + private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int + { + // start1 = $start + // start2 = $limit->start_date + // start1 = $end + // start2 = $limit->end_date + + // limit is larger than start and end (inclusive) + // |-----------| + // |----------------| + if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) { + return $start->diffInDays($end) + 1; // add one day + } + // limit starts earlier and limit ends first: + // |-----------| + // |-------| + if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) { + // return days in the range $start-$limit_end + return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself + } + // limit starts later and limit ends earlier + // |-----------| + // |-------| + if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) { + // return days in the range $limit_start - $end + return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself + } + return 0; + } } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index d06d560530..912351d704 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -34,6 +34,7 @@ use Illuminate\Support\Collection; */ interface BudgetRepositoryInterface { + /** * @param string $query * @param int $limit @@ -50,6 +51,15 @@ interface BudgetRepositoryInterface */ public function budgetStartsWith(string $query, int $limit): Collection; + /** + * Returns the amount that is budgeted in a period. + * + * @param Carbon $start + * @param Carbon $end + * @return array + */ + public function budgetedInPeriod(Carbon $start, Carbon $end): array; + /** * @return bool */ diff --git a/app/Support/Http/Api/ConvertsExchangeRates.php b/app/Support/Http/Api/ConvertsExchangeRates.php new file mode 100644 index 0000000000..4b94e50cbe --- /dev/null +++ b/app/Support/Http/Api/ConvertsExchangeRates.php @@ -0,0 +1,202 @@ +. + */ + +namespace FireflyIII\Support\Http\Api; + +use Carbon\Carbon; +use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\TransactionCurrency; +use Log; + +/** + * Trait ConvertsExchangeRates + */ +trait ConvertsExchangeRates +{ + /** + * For a sum of entries, get the exchange rate to the native currency of + * the user. + * @param array $entries + * @return array + */ + public function cerSum(array $entries): array + { + /** @var TransactionCurrency $native */ + $native = app('amount')->getDefaultCurrency(); + $return = []; + /** @var array $entry */ + foreach ($entries as $entry) { + $currency = $this->getCurrency((int) $entry['id']); + if ($currency->id !== $native->id) { + $amount = $this->convertAmount($entry['sum'], $currency, $native); + $entry['native_sum'] = $amount; + $entry['native_id'] = (string) $native->id; + $entry['native_name'] = $native->name; + $entry['native_symbol'] = $native->symbol; + $entry['native_code'] = $native->code; + $entry['native_decimal_places'] = $native->decimal_places; + } + if ($currency->id === $native->id) { + $entry['native_sum'] = $entry['sum']; + $entry['native_id'] = (string) $native->id; + $entry['native_name'] = $native->name; + $entry['native_symbol'] = $native->symbol; + $entry['native_code'] = $native->code; + $entry['native_decimal_places'] = $native->decimal_places; + } + $return[] = $entry; + + } + return $return; + } + + /** + * @param int $currencyId + * @return TransactionCurrency + */ + private function getCurrency(int $currencyId): TransactionCurrency + { + $result = TransactionCurrency::find($currencyId); + if (null === $result) { + return app('amount')->getDefaultCurrency(); + } + return $result; + } + + /** + * @param string $amount + * @param TransactionCurrency $from + * @param TransactionCurrency $to + * @return string + */ + private function convertAmount(string $amount, TransactionCurrency $from, TransactionCurrency $to, ?Carbon $date = null): string + { + Log::debug(sprintf('Converting %s from %s to %s', $amount, $from->code, $to->code)); + $date = $date ?? Carbon::now(); + $rate = $this->getRate($from, $to, $date); + + return bcmul($amount, $rate); + } + + /** + * @param TransactionCurrency $from + * @param TransactionCurrency $to + * @param Carbon $date + * @return string + */ + private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string + { + Log::debug(sprintf('getRate(%s, %s, "%s")', $from->code, $to->code, $date->format('Y-m-d'))); + /** @var CurrencyExchangeRate $result */ + $result = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', '<=', $date->format('Y-m-d')) + ->orderBy('date', 'DESC') + ->first(); + if (null !== $result) { + $rate = (string) $result->rate; + Log::debug(sprintf('Rate is %s', $rate)); + return $rate; + } + // no result. perhaps the other way around? + /** @var CurrencyExchangeRate $result */ + $result = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $to->id) + ->where('to_currency_id', $from->id) + ->where('date', '<=', $date->format('Y-m-d')) + ->orderBy('date', 'DESC') + ->first(); + if (null !== $result) { + $rate = bcdiv('1', (string) $result->rate); + Log::debug(sprintf('Reversed rate is %s', $rate)); + return $rate; + } + // try euro rates + $result1 = $this->getEuroRate($from, $date); + if ('0' === $result1) { + Log::debug(sprintf('No exchange rate between EUR and %s', $from->code)); + return '0'; + } + $result2 = $this->getEuroRate($to, $date); + if ('0' === $result2) { + Log::debug(sprintf('No exchange rate between EUR and %s', $to->code)); + return '0'; + } + // still need to inverse rate 2: + $result2 = bcdiv('1', $result2); + $rate = bcmul($result1, $result2); + Log::debug(sprintf('Rate %s to EUR is %s', $from->code, $result1)); + Log::debug(sprintf('Rate EUR to %s is %s', $to->code, $result2)); + Log::debug(sprintf('Rate for %s to %s is %s', $from->code, $to->code, $rate)); + return $rate; + } + + /** + * @param TransactionCurrency $currency + * @param Carbon $date + * @return string + */ + private function getEuroRate(TransactionCurrency $currency, Carbon $date): string + { + Log::debug(sprintf('Find rate for %s to Euro', $currency->code)); + $euro = TransactionCurrency::whereCode('EUR')->first(); + if (null === $euro) { + Log::warning('Cannot do indirect conversion without EUR.'); + return '0'; + } + + // try one way: + /** @var CurrencyExchangeRate $result */ + $result = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $currency->id) + ->where('to_currency_id', $euro->id) + ->where('date', '<=', $date->format('Y-m-d')) + ->orderBy('date', 'DESC') + ->first(); + if (null !== $result) { + $rate = (string) $result->rate; + Log::debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate)); + return $rate; + } + // try the other way around and inverse it. + /** @var CurrencyExchangeRate $result */ + $result = auth()->user() + ->currencyExchangeRates() + ->where('from_currency_id', $euro->id) + ->where('to_currency_id', $currency->id) + ->where('date', '<=', $date->format('Y-m-d')) + ->orderBy('date', 'DESC') + ->first(); + if (null !== $result) { + $rate = bcdiv('1', (string) $result->rate); + Log::debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate)); + return $rate; + } + + Log::debug(sprintf('No rate for %s to EUR.', $currency->code)); + return '0'; + } + +} diff --git a/config/cer.php b/config/cer.php new file mode 100644 index 0000000000..8e173a66f8 --- /dev/null +++ b/config/cer.php @@ -0,0 +1,68 @@ +. + */ + + +return [ + // if currencies are added, default rates must be added as well! + // last exchange rate update: 6-6-2022 + // source: https://www.xe.com/currencyconverter/ + 'date' => '2022-06-06', + 'rates' => [ + // europa + ['EUR', 'HUF', 387.9629], + ['EUR', 'GBP', 0.85420754], + ['EUR', 'UAH', 31.659752], + ['EUR', 'PLN', 4.581788], + ['EUR', 'TRY', 17.801397], + ['EUR', 'DKK', 7.4389753], + + // Americas + ['EUR', 'USD', 1.0722281], + ['EUR', 'BRL', 5.0973173], + ['EUR', 'CAD', 1.3459969], + ['EUR', 'MXN', 20.899824], + + // Oceania currencies + ['EUR', 'IDR', 15466.299], + ['EUR', 'AUD', 1.4838549], + ['EUR', 'NZD', 1.6425829], + + // africa + ['EUR', 'EGP', 19.99735], + ['EUR', 'MAD', 10.573307], + ['EUR', 'ZAR', 16.413167], + + // asia + ['EUR', 'JPY', 140.15257], + ['EUR', 'RMB', 7.1194265], + ['EUR', 'RUB', 66.000895], + ['EUR', 'INR', 83.220481], + + // int + ['EUR', 'XBT', 0, 00003417], + ['EUR', 'BCH', 0.00573987], + ['EUR', 'ETH', 0, 00056204], + + ['EUR', 'ILS', 3.5712508], + ['EUR', 'CHF', 1.0323891], + ['EUR', 'HRK', 7.5220845], + ], +]; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 839742aa3c..57b47b7a59 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -41,5 +41,6 @@ class DatabaseSeeder extends Seeder $this->call(LinkTypeSeeder::class); $this->call(ConfigSeeder::class); $this->call(UserRoleSeeder::class); + $this->call(ExchangeRateSeeder::class); } } diff --git a/database/seeders/ExchangeRateSeeder.php b/database/seeders/ExchangeRateSeeder.php new file mode 100644 index 0000000000..08f3fbdada --- /dev/null +++ b/database/seeders/ExchangeRateSeeder.php @@ -0,0 +1,117 @@ +. + */ + +namespace Database\Seeders; + +use FireflyIII\Models\CurrencyExchangeRate; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\User; +use Illuminate\Database\Seeder; +use Illuminate\Support\Collection; +use Log; + +/** + * Class ExchangeRateSeeder + */ +class ExchangeRateSeeder extends Seeder +{ + private Collection $users; + + /** + * @return void + */ + public function run(): void + { + $count = User::count(); + if (0 === $count) { + Log::debug('Will not seed exchange rates yet.'); + return; + } + $users = User::get(); + $date = config('cer.date'); + $rates = config('cer.rates'); + $usable = []; + foreach ($rates as $rate) { + $from = $this->getCurrency($rate[0]); + $to = $this->getCurrency($rate[1]); + if (null !== $from && null !== $to) { + $usable[] = [$from, $to, $rate[2]]; + } + } + unset($rates, $from, $to, $rate); + + /** @var User $user */ + foreach ($users as $user) { + foreach ($usable as $rate) { + if (!$this->hasRate($user, $rate[0], $rate[1], $date)) { + $this->addRate($user, $rate[0], $rate[1], $date, $rate[2]); + } + } + } + } + + /** + * @param string $code + * @return TransactionCurrency|null + */ + private function getCurrency(string $code): ?TransactionCurrency + { + return TransactionCurrency::whereNull('deleted_at')->where('code', $code)->first(); + } + + /** + * @param User $user + * @param TransactionCurrency $from + * @param TransactionCurrency $to + * @param string $date + * @return bool + */ + private function hasRate(User $user, TransactionCurrency $from, TransactionCurrency $to, string $date): bool + { + return $user->currencyExchangeRates() + ->where('from_currency_id', $from->id) + ->where('to_currency_id', $to->id) + ->where('date', $date) + ->count() > 0; + } + + /** + * @param User $user + * @param TransactionCurrency $from + * @param TransactionCurrency $to + * @param string $date + * @param float $rate + * @return void + */ + private function addRate(User $user, TransactionCurrency $from, TransactionCurrency $to, string $date, float $rate): void + { + /** @var User $user */ + CurrencyExchangeRate::create( + [ + 'user_id' => $user->id, + 'from_currency_id' => $from->id, + 'to_currency_id' => $to->id, + 'date' => $date, + 'rate' => $rate, + ] + ); + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4cccea955c..52e5ea1189 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -24,6 +24,7 @@ - - diff --git a/frontend/src/components/dashboard/SpendInsightBox.vue b/frontend/src/components/dashboard/SpendInsightBox.vue new file mode 100644 index 0000000000..c2d0340435 --- /dev/null +++ b/frontend/src/components/dashboard/SpendInsightBox.vue @@ -0,0 +1,152 @@ + + + + + + + diff --git a/frontend/src/pages/dashboard/Dashboard.vue b/frontend/src/pages/dashboard/Dashboard.vue index 743dc83297..28b89956a2 100644 --- a/frontend/src/pages/dashboard/Dashboard.vue +++ b/frontend/src/pages/dashboard/Dashboard.vue @@ -27,7 +27,7 @@
- TODO spend insight +
TODO net worth insight @@ -94,6 +94,7 @@ export default { name: "Dashboard", components: { BillInsightBox: defineAsyncComponent(() => import('../../components/dashboard/BillInsightBox.vue')), + SpendInsightBox: defineAsyncComponent(() => import('../../components/dashboard/SpendInsightBox.vue')), } } diff --git a/frontend/src/store/fireflyiii/actions.js b/frontend/src/store/fireflyiii/actions.js index b863ac912c..79102caf0c 100644 --- a/frontend/src/store/fireflyiii/actions.js +++ b/frontend/src/store/fireflyiii/actions.js @@ -36,97 +36,3 @@ export function resetRange(context) { context.commit('setRange', defaultRange); } -export function setDatesFromViewRange(context) { - let start; - let end; - let viewRange = context.getters.getViewRange; - - let today = new Date; - switch (viewRange) { - case 'last365': - start = startOfDay(subDays(today, 365)); - end = endOfDay(today); - break; - case 'last90': - start = startOfDay(subDays(today, 90)); - end = endOfDay(today); - break; - case 'last30': - start = startOfDay(subDays(today, 30)); - end = endOfDay(today); - break; - case 'last7': - start = startOfDay(subDays(today, 7)); - end = endOfDay(today); - break; - case 'YTD': - start = startOfYear(today); - end = endOfDay(today); - break; - case 'QTD': - start = startOfQuarter(today); - end = endOfDay(today); - break; - case 'MTD': - start = startOfMonth(today); - end = endOfDay(today); - break; - case '1D': - // today: - start = startOfDay(today); - end = endOfDay(today); - break; - case '1W': - // this week: - start = startOfDay(startOfWeek(today, {weekStartsOn: 1})); - end = endOfDay(endOfWeek(today, {weekStartsOn: 1})); - break; - case '1M': - // this month: - start = startOfDay(startOfMonth(today)); - end = endOfDay(endOfMonth(today)); - break; - case '3M': - // this quarter - start = startOfDay(startOfQuarter(today)); - end = endOfDay(endOfQuarter(today)); - break; - case '6M': - // this half-year - if (today.getMonth() <= 5) { - start = new Date(today); - start.setMonth(0); - start.setDate(1); - start = startOfDay(start); - end = new Date(today); - end.setMonth(5); - end.setDate(30); - end = endOfDay(start); - } - if (today.getMonth() > 5) { - start = new Date(today); - start.setMonth(6); - start.setDate(1); - start = startOfDay(start); - end = new Date(today); - end.setMonth(11); - end.setDate(31); - end = endOfDay(start); - } - break; - case '1Y': - // this year - start = new Date(today); - start.setMonth(0); - start.setDate(1); - start = startOfDay(start); - - end = new Date(today); - end.setMonth(11); - end.setDate(31); - end = endOfDay(end); - break; - } - context.commit('setRange', {start: start, end: end}); - context.commit('setDefaultRange', {start: start, end: end}); -} diff --git a/frontend/src/stores/fireflyiii.js b/frontend/src/stores/fireflyiii.js index a517d18fd6..7f792c8095 100644 --- a/frontend/src/stores/fireflyiii.js +++ b/frontend/src/stores/fireflyiii.js @@ -12,19 +12,31 @@ import { subDays } from "date-fns"; -export const useFireflyIIIStore = defineStore('counter', { +export const useFireflyIIIStore = defineStore('firefly-iii', { state: () => ({ - drawerState: true, viewRange: '1M', listPageSize: 10, range: { + drawerState: true, + viewRange: '1M', + listPageSize: 10, + locale: 'en-US', + range: { start: null, end: null - }, defaultRange: { - start: null, end: null - }, currencyCode: 'AAA', currencyId: '0', cacheKey: 'initial' + }, + defaultRange: { + start: null, + end: null + }, + currencyCode: 'AAA', + currencyId: '0', + cacheKey: 'initial' }), getters: { getViewRange(state) { return state.viewRange; }, + getLocale(state) { + return state.locale; + }, getListPageSize(state) { return state.listPageSize; @@ -156,9 +168,6 @@ export const useFireflyIIIStore = defineStore('counter', { // mutators - increment() { - this.counter++ - }, updateViewRange(viewRange) { this.viewRange = viewRange; }, @@ -166,6 +175,9 @@ export const useFireflyIIIStore = defineStore('counter', { updateListPageSize(value) { this.listPageSize = value; }, + setLocale(value) { + this.locale = value; + }, setRange(value) { this.range = value; diff --git a/resources/views/auth/login.twig b/resources/views/auth/login.twig index afeb205707..44381872c1 100644 --- a/resources/views/auth/login.twig +++ b/resources/views/auth/login.twig @@ -67,11 +67,11 @@
{% if config('firefly.authentication_guard') == 'web' %} - {% else %} - {% endif %}
@@ -126,4 +126,11 @@
{% endblock %} - +{% block scripts %} + +{% endblock %} diff --git a/resources/views/layout/v3/session.twig b/resources/views/layout/v3/session.twig index 7e723aff1e..e89d3a2b0b 100644 --- a/resources/views/layout/v3/session.twig +++ b/resources/views/layout/v3/session.twig @@ -7,19 +7,21 @@ {{ 'login_page_title'|_ }} - - - - + + + + {% block content %}{% endblock %} - + - + - + +{% block scripts %} +{% endblock %} diff --git a/routes/api.php b/routes/api.php index 3fbfa4d578..fcca9b92b4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -45,6 +45,29 @@ Route::group( } ); +/** + * V2 API route for bills. + */ +Route::group( + ['namespace' => 'FireflyIII\Api\V2\Controllers\Model\Budget', 'prefix' => 'v2/budgets', + 'as' => 'api.v2.budgets',], + static function () { + Route::get('sum/budgeted', ['uses' => 'SumController@budgeted', 'as' => 'sum.budgeted']); + Route::get('sum/unpaid', ['uses' => 'SumController@unpaid', 'as' => 'sum.unpaid']); + } +); + +/** + * V2 API route for system + */ +Route::group( + ['namespace' => 'FireflyIII\Api\V2\Controllers\System', 'prefix' => 'v2', + 'as' => 'api.v2.system.',], + static function () { + Route::get('preferences/{preference}', ['uses' => 'PreferencesController@get', 'as' => 'preferences.get']); + } +); + /** * Autocomplete controllers */