mirror of https://github.com/kcal-app/kcal.git
Add Goal controller, routes, and resources
This commit is contained in:
parent
74fe2be70e
commit
eee1cfaaf1
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Goal;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class GoalController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('goals.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return $this->edit(new Goal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
return $this->update($request, new Goal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Goal $goal): View
|
||||
{
|
||||
return view('goals.show')->with('goal', $goal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Goal $goal): View
|
||||
{
|
||||
return view('goals.edit')
|
||||
->with('goal', $goal)
|
||||
->with('attributeOptions', Goal::getAttributeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Goal $goal): RedirectResponse
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'attribute' => ['required', 'string'],
|
||||
'goal' => ['required', 'numeric'],
|
||||
]);
|
||||
$goal->fill(array_filter($attributes))
|
||||
->user()->associate(Auth::user());
|
||||
$goal->save();
|
||||
session()->flash('message', "Goal updated!");
|
||||
return redirect()->route('goals.show', $goal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm removal of specified resource.
|
||||
*/
|
||||
public function delete(Goal $goal): View
|
||||
{
|
||||
return view('goals.delete')->with('goal', $goal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Goal $goal): RedirectResponse
|
||||
{
|
||||
$goal->delete();
|
||||
return redirect(route('goals.index'))
|
||||
->with('message', "Goal deleted!");
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Nutrients;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* App\Models\Goal
|
||||
|
|
@ -60,4 +62,23 @@ class Goal extends Model
|
|||
public function user(): BelongsTo {
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options for the "attribute" column.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAttributeOptions(): array {
|
||||
$options = [];
|
||||
foreach (Nutrients::$all as $nutrient) {
|
||||
foreach (['daily', 'weekly', 'monthly', 'yearly'] as $frequency) {
|
||||
$key = "{$nutrient['value']}_{$frequency}";
|
||||
$options[$key] = [
|
||||
'value' => $key,
|
||||
'label' => Str::ucfirst("{$frequency} {$nutrient['value']}")
|
||||
];
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Delete {{ $goal->attribute }} Goal?
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<form method="POST" action="{{ route('goals.destroy', $goal) }}">
|
||||
@method('delete')
|
||||
@csrf
|
||||
<div class="text-lg pb-3">
|
||||
Are you sure what to delete your <span class="font-extrabold">{{ $goal->attribute }}</span> goal?
|
||||
</div>
|
||||
<x-inputs.button class="bg-red-800 hover:bg-red-700">
|
||||
Yes, delete
|
||||
</x-inputs.button>
|
||||
<a class="ml-3 text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ route('goals.show', $goal) }}">No, do not delete</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ ($goal->exists ? 'Edit' : 'Add') }} Goal
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<form method="POST" action="{{ ($goal->exists ? route('goals.update', $goal) : route('goals.store')) }}">
|
||||
@if ($goal->exists)@method('put')@endif
|
||||
@csrf
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||
<!-- From -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="from" value="From"/>
|
||||
<x-inputs.input name="from"
|
||||
type="date"
|
||||
class="block w-full"
|
||||
:value="old('from', $goal->from)" />
|
||||
</div>
|
||||
|
||||
<!-- To -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="to" value="To"/>
|
||||
<x-inputs.input name="to"
|
||||
type="date"
|
||||
class="block w-full"
|
||||
:value="old('to', $goal->to)" />
|
||||
</div>
|
||||
|
||||
<!-- Attribute -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="attribute" value="Attribute"/>
|
||||
|
||||
<x-inputs.select name="attribute"
|
||||
class="block mt-1 w-full"
|
||||
:options="$attributeOptions"
|
||||
:selectedValue="old('attribute', $goal->attribute)">
|
||||
<option value=""></option>
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
||||
<!-- Goal -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="goal" value="Goal"/>
|
||||
|
||||
<x-inputs.input name="goal"
|
||||
type="number"
|
||||
step="any"
|
||||
class="block mt-1 w-full"
|
||||
:value="old('goal', $goal->goal)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-inputs.button class="ml-3">
|
||||
{{ ($goal->exists ? 'Save' : 'Add') }}
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-2xl text-gray-800 leading-tight">Goals</h2>
|
||||
<a href="{{ route('goals.create') }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Add Goal
|
||||
</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
TODO: Goals index.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight flex flex-auto">
|
||||
TODO: GOAL NAME
|
||||
<a class="ml-2 text-gray-500 hover:text-gray-700 hover:border-gray-300 text-sm"
|
||||
href="{{ route('goals.edit', $goal) }}">
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="h-6 w-6 text-red-500 hover:text-red-700 hover:border-red-300 float-right text-sm"
|
||||
href="{{ route('goals.delete', $goal) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</h2>
|
||||
</x-slot>
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
TODO: GOAL SHOW PAGE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
|
@ -62,6 +62,10 @@
|
|||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-dropdown-link :href="route('goals.index')">Goals</x-dropdown-link>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}" x-data>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\FoodController;
|
||||
use App\Http\Controllers\GoalController;
|
||||
use App\Http\Controllers\IngredientPickerController;
|
||||
use App\Http\Controllers\JournalEntryController;
|
||||
use App\Http\Controllers\RecipeController;
|
||||
|
|
@ -27,8 +28,12 @@ Route::get('/', function (): RedirectResponse {
|
|||
Route::resource('foods', FoodController::class)->middleware(['auth']);
|
||||
Route::get('/foods/{food}/delete', [FoodController::class, 'delete'])->middleware(['auth'])->name('foods.delete');
|
||||
|
||||
// Recipes.
|
||||
Route::resource('recipes', RecipeController::class)->middleware(['auth']);
|
||||
// Goals.
|
||||
Route::resource('goals', GoalController::class)->middleware(['auth']);
|
||||
Route::get('/goals/{goal}/delete', [GoalController::class, 'delete'])->middleware(['auth'])->name('goals.delete');
|
||||
|
||||
// Ingredient picker.
|
||||
Route::get('/ingredient-picker/search', [IngredientPickerController::class, 'search'])->middleware(['auth'])->name('ingredient-picker.search');
|
||||
|
||||
// Journal entries.
|
||||
Route::get('/journal-entries/create/from-nutrients', [JournalEntryController::class, 'createFromNutrients'])->middleware(['auth'])->name('journal-entries.create.from-nutrients');
|
||||
|
|
@ -36,8 +41,7 @@ Route::post('/journal-entries/create/from-nutrients', [JournalEntryController::c
|
|||
Route::resource('journal-entries', JournalEntryController::class)->middleware(['auth']);
|
||||
Route::get('/journal-entries/{journalEntry}/delete', [JournalEntryController::class, 'delete'])->middleware(['auth'])->name('journal-entries.delete');
|
||||
|
||||
// Custom.
|
||||
// TODO: Change this to a custom JSON API endpoint.
|
||||
Route::get('/ingredient-picker/search', [IngredientPickerController::class, 'search'])->middleware(['auth'])->name('ingredient-picker.search');
|
||||
// Recipes.
|
||||
Route::resource('recipes', RecipeController::class)->middleware(['auth']);
|
||||
|
||||
require __DIR__.'/auth.php';
|
||||
|
|
|
|||
Loading…
Reference in New Issue