Implement creation of budgets
MODIFY
app/Http/Controllers/BudgetController.php
app/Http/Controllers/BudgetController.php
namespace App\Http\Controllers;
+use App\Helper;
use App\Repositories\BudgetRepository;
+use App\Repositories\TagRepository;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\ValidationException;
class BudgetController extends Controller
{
private $budgetRepository;
+ private $tagRepository;
- public function __construct(BudgetRepository $budgetRepository)
+ public function __construct(BudgetRepository $budgetRepository, TagRepository $tagRepository)
{
$this->budgetRepository = $budgetRepository;
+ $this->tagRepository = $tagRepository;
}
public function index()
@@ -20,4 +26,33 @@ class BudgetController extends Controller
'budgets' => $this->budgetRepository->getActive()
]);
}
+
+ public function create()
+ {
+ return view('budgets.create', [
+ 'tags' => session('space')->tags()->orderBy('created_at', 'DESC')->get()
+ ]);
+ }
+
+ public function store(Request $request)
+ {
+ $request->validate($this->budgetRepository->getValidationRules());
+
+ $user = Auth::user();
+ $tag = $this->tagRepository->getById($request->tag_id);
+
+ if (!$user->can('view', $tag)) {
+ throw ValidationException::withMessages(['tag_id' => __('validation.forbidden')]);
+ }
+
+ if ($this->budgetRepository->doesExist(session('space')->id, $request->tag_id)) {
+ return redirect('/budgets/create')
+ ->with('message', 'A budget like this already exists');
+ }
+
+ $amount = Helper::rawNumberToInteger($request->amount);
+ $this->budgetRepository->create(session('space')->id, $request->tag_id, $request->period, $amount);
+
+ return redirect('/budgets');
+ }
}

MODIFY
app/Models/Budget.php
app/Models/Budget.php
@@ -11,6 +11,14 @@ class Budget extends Model
{
use SoftDeletes;
+ protected $fillable = [
+ 'space_id',
+ 'tag_id',
+ 'period',
+ 'amount',
+ 'starts_on'
+ ];
+
public function space()
{
return $this->belongsTo(Space::class);

MODIFY
app/Policies/TagPolicy.php
app/Policies/TagPolicy.php
@@ -7,6 +7,11 @@ use App\Models\Tag;
class TagPolicy
{
+ public function view(User $user, Tag $tag)
+ {
+ return $user->spaces->contains($tag->space_id);
+ }
+
public function edit(User $user, Tag $tag)
{
return $user->spaces->contains($tag->space_id);

MODIFY
app/Repositories/BudgetRepository.php
app/Repositories/BudgetRepository.php
@@ -5,9 +5,50 @@ namespace App\Repositories;
use App\Models\Budget;
use App\Models\Spending;
use Exception;
+use Illuminate\Support\Facades\DB;
class BudgetRepository
{
+ public function getValidationRules()
+ {
+ return [
+ 'tag_id' => 'required|exists:tags,id',
+ 'period' => 'required|in:' . implode(',', $this->getSupportedPeriods()),
+ 'amount' => 'required|regex:/^\d*(\.\d{2})?$/'
+ ];
+ }
+
+ public function getSupportedPeriods(): array
+ {
+ return [
+ 'yearly',
+ 'monthly',
+ 'weekly',
+ 'daily'
+ ];
+ }
+
+ public function doesExist(int $spaceId, int $tagId): bool
+ {
+ return DB::selectOne('
+ SELECT COUNT(*) AS count
+ FROM budgets
+ WHERE
+ space_id = ?
+ AND tag_id = ?
+ AND (
+ starts_on <= NOW()
+ AND (
+ ends_on IS NULL
+ OR ends_on > NOW()
+ )
+ )
+ ', [
+ $spaceId,
+ $tagId
+ ])->count == 1;
+ }
+
public function getActive()
{
$today = date('Y-m-d');
@@ -62,4 +103,26 @@ class BudgetRepository
throw new Exception('No clue what to do with period "' . $budget->period . '"');
}
+
+ public function create(int $spaceId, int $tagId, string $period, int $amount): ?Budget
+ {
+ if ($this->doesExist($spaceId, $tagId)) {
+ throw new Exception(vsprintf('Budget (with space ID being %s and tag ID being %s) already exists', [
+ $spaceId,
+ $tagId
+ ]));
+ }
+
+ if (!in_array($period, $this->getSupportedPeriods())) {
+ throw new Exception('Unknown period "' . $period . '"');
+ }
+
+ return Budget::create([
+ 'space_id' => $spaceId,
+ 'tag_id' => $tagId,
+ 'period' => $period,
+ 'amount' => $amount,
+ 'starts_on' => date('Y-m-d')
+ ]);
+ }
}

MODIFY
app/Repositories/TagRepository.php
app/Repositories/TagRepository.php
@@ -16,6 +16,11 @@ class TagRepository
];
}
+ public function getById(int $id): ?Tag
+ {
+ return Tag::find($id);
+ }
+
public function getMostExpensiveTags(int $spaceId, int $limit = null, int $year = null, int $month = null)
{
$sql = '

ADD
resources/lang/de/fields.php
+<?php
+
+return [
+ 'period' => 'Zeitraum'
+];

MODIFY
resources/lang/en/fields.php
resources/lang/en/fields.php
@@ -15,6 +15,8 @@ return [
'description' => 'Description',
'amount' => 'Amount',
+ 'period' => 'Period',
+
'file' => 'File',
'color' => 'Color'

MODIFY
resources/lang/en/validation.php
resources/lang/en/validation.php
return [
'required' => 'Can\'t be blank',
'confirmed' => 'Doesn\'t match',
+
'max' => [
'string' => 'Can\'t be longer than :max characters'
- ]
+ ],
+
+ 'forbidden' => 'Not permitted to use this resource'
];

MODIFY
resources/lang/nl/fields.php
resources/lang/nl/fields.php
@@ -15,6 +15,8 @@ return [
'description' => 'Omschrijving',
'amount' => 'Bedrag',
+ 'period' => 'Periode',
+
'file' => 'Bestand',
'color' => 'Kleur'

ADD
resources/views/budgets/create.blade.php
+@extends('layout')
+
+@section('title', __('actions.create') . ' Budget')
+
+@section('body')
+ <div class="wrapper my-3">
+ <h2>{{ __('actions.create') }} {{ __('models.budget') }}</h2>
+ <div class="box mt-3">
+ <form method="POST" action="/budgets">
+ {{ csrf_field() }}
+ <div class="box__section">
+ @if (session()->has('message'))
+ <div class="mb-2">{{ session('message') }}</div>
+ @endif
+ <div class="input input--small">
+ <label>{{ __('models.tag') }}</label>
+ <select name="tag_id">
+ @foreach ($tags as $tag)
+ <option value="{{ $tag->id }}">{{ $tag->name }}</option>
+ @endforeach
+ </select>
+ @include('partials.validation_error', ['payload' => 'tag_id'])
+ </div>
+ <div class="input input--small">
+ <label>{{ __('fields.period') }}</label>
+ <select name="period">
+ <option value="yearly">{{ __('calendar.intervals.yearly') }}</option>
+ <option value="monthly" selected>{{ __('calendar.intervals.monthly') }}</option>
+ <option value="weekly">{{ __('calendar.intervals.weekly') }}</option>
+ <option value="daily">{{ __('calendar.intervals.daily') }}</option>
+ </select>
+ @include('partials.validation_error', ['payload' => 'period'])
+ </div>
+ <div class="input input--small">
+ <label>{{ __('fields.amount') }}</label>
+ <input type="text" name="amount" />
+ @include('partials.validation_error', ['payload' => 'amount'])
+ </div>
+ </div>
+ <div class="box__section box__section--highlight row row--right">
+ <div class="row__column row__column--compact row__column--middle">
+ <a href="/budgets">{{ __('actions.cancel') }}</a>
+ </div>
+ <div class="row__column row__column--compact ml-2">
+ <button class="button">{{ __('actions.create') }}</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+@endsection

MODIFY
resources/views/budgets/index.blade.php
resources/views/budgets/index.blade.php
@section('body')
<div class="wrapper my-3">
+ <div class="row">
+ <div class="row__column row__column--middle">
<h2>{{ __('models.budgets') }}</h2>
+ </div>
+ <div class="row__column row__column--compact row__column--middle">
+ <a href="/budgets/create" class="button">{{ __('actions.create') }} {{ __('models.budget') }}</a>
+ </div>
+ </div>
<div class="box mt-3">
@if (!count($budgets))
<div class="box__section text-center">{{ __('general.empty_state', ['resource' => strtolower(__('models.budgets'))]) }}</div>

MODIFY
routes/web.php
routes/web.php
@@ -77,7 +77,11 @@ Route::group(['middleware' => ['auth']], function () {
'show'
]);
- Route::get('/budgets', [BudgetController::class, 'index']);
+ Route::resource('/budgets', BudgetController::class)->only([
+ 'index',
+ 'create',
+ 'store'
+ ]);
Route::resource('/tags', TagController::class)->only([
'index',

Implement creation of budgets