mirror of https://github.com/kcal-app/kcal.git
Add goals support (#4)
This commit is contained in:
parent
33a8591c72
commit
414629b469
|
@ -0,0 +1,105 @@
|
|||
<?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\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class GoalController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
if ($request->date) {
|
||||
$date = Carbon::createFromFormat('Y-m-d', $request->date);
|
||||
}
|
||||
else {
|
||||
$date = Carbon::now();
|
||||
}
|
||||
return view('goals.index')
|
||||
->with('date', $date)
|
||||
->with('goals', Auth::user()->getGoalsByTime($date));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
->with('nameOptions', Goal::getNameOptions())
|
||||
->with('frequencyOptions', Goal::$frequencyOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Goal $goal): View
|
||||
{
|
||||
return view('goals.edit')
|
||||
->with('goal', $goal)
|
||||
->with('nameOptions', Goal::getNameOptions())
|
||||
->with('frequencyOptions', Goal::$frequencyOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Goal $goal): RedirectResponse
|
||||
{
|
||||
$attributes = $request->validate([
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'frequency' => ['nullable', 'string'],
|
||||
'name' => ['required', 'string'],
|
||||
'goal' => ['required', 'numeric'],
|
||||
]);
|
||||
$goal->fill($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!");
|
||||
}
|
||||
}
|
|
@ -29,12 +29,39 @@ class JournalEntryController extends Controller
|
|||
public function index(Request $request): View
|
||||
{
|
||||
$date = $request->date ?? Carbon::now()->toDateString();
|
||||
$date = Carbon::rawCreateFromFormat('Y-m-d', $date);
|
||||
|
||||
// Get entries and nutrient sums for the day.
|
||||
$entries = JournalEntry::where([
|
||||
'user_id' => Auth::user()->id,
|
||||
'date' => $date->toDateString(),
|
||||
])->get();
|
||||
$sums = [];
|
||||
foreach (Nutrients::$all as $nutrient) {
|
||||
$sums[$nutrient['value']] = round($entries->sum($nutrient['value']));
|
||||
}
|
||||
|
||||
// Get daily goals data for user.
|
||||
$goals = Auth::user()->getGoalsByTime($date);
|
||||
$dailyGoals = [];
|
||||
foreach (Nutrients::$all as $nutrient) {
|
||||
$goal = $goals['present']
|
||||
->where('frequency', 'daily')
|
||||
->where('name', $nutrient['value'])
|
||||
->first();
|
||||
if ($goal) {
|
||||
$dailyGoals[$goal->name] = round($sums[$goal->name] / $goal->goal * 100);
|
||||
if ($dailyGoals[$goal->name] > 0) {
|
||||
$dailyGoals[$goal->name] .= '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view('journal-entries.index')
|
||||
->with('entries', JournalEntry::where([
|
||||
'user_id' => Auth::user()->id,
|
||||
'date' => $date,
|
||||
])->get())
|
||||
->with('date', Carbon::createFromFormat('Y-m-d', $date));
|
||||
->with('entries', $entries)
|
||||
->with('sums', $sums)
|
||||
->with('dailyGoals', $dailyGoals)
|
||||
->with('date', $date);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Nutrients;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\Goal
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $user_id
|
||||
* @property \datetime|null $from
|
||||
* @property \datetime|null $to
|
||||
* @property string|null $frequency
|
||||
* @property string $name
|
||||
* @property float $goal
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read string $summary
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereFrequency($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereFrom($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereGoal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereTo($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Goal whereUserId($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
final class Goal extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Supported options for thr frequency attribute.
|
||||
*/
|
||||
public static array $frequencyOptions = [
|
||||
'daily' => ['value' => 'daily', 'label' => 'daily'],
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected $fillable = [
|
||||
'frequency',
|
||||
'from',
|
||||
'goal',
|
||||
'name',
|
||||
'to',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected $casts = [
|
||||
'from' => 'datetime:Y-m-d',
|
||||
'goal' => 'float',
|
||||
'to' => 'datetime:Y-m-d',
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected $appends = [
|
||||
'summary',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the User this goal belongs to.
|
||||
*/
|
||||
public function user(): BelongsTo {
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function getSummaryAttribute(): string {
|
||||
$nameOptions = self::getNameOptions();
|
||||
return number_format($this->goal) . "{$nameOptions[$this->name]['unit']} {$nameOptions[$this->name]['label']} {$this->frequency}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options for the "name" column.
|
||||
*/
|
||||
public static function getNameOptions(): array {
|
||||
$options = [];
|
||||
foreach (Nutrients::$all as $nutrient) {
|
||||
$options[$nutrient['value']] = [
|
||||
'value' => $nutrient['value'],
|
||||
'label' => $nutrient['label'],
|
||||
'unit' => $nutrient['unit'],
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
|
@ -69,7 +69,7 @@ final class JournalEntry extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected $casts = [
|
||||
'calories' => 'float',
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* App\Models\User
|
||||
|
@ -32,6 +35,8 @@ use Illuminate\Notifications\Notifiable;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Goal[] $goals
|
||||
* @property-read int|null $goals_count
|
||||
*/
|
||||
final class User extends Authenticatable
|
||||
{
|
||||
|
@ -60,4 +65,38 @@ final class User extends Authenticatable
|
|||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the User's goals.
|
||||
*/
|
||||
public function goals(): HasMany {
|
||||
return $this->hasMany(Goal::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User's past, present, and future goals.
|
||||
*
|
||||
* @return \App\Models\Goal[]
|
||||
*/
|
||||
public function getGoalsByTime(?Carbon $date = null): array {
|
||||
$now = $date ?? Carbon::now();
|
||||
$goals = ['past' => new Collection(), 'present' => new Collection(), 'future' => new Collection()];
|
||||
Goal::all()->where('user_id', Auth::user()->id)
|
||||
->each(function ($item) use(&$goals, $now) {
|
||||
if ($item->to && $now->isAfter($item->to)) {
|
||||
$goals['past'][$item->id] = $item;
|
||||
}
|
||||
elseif ($item->from && $now->isBefore($item->from)) {
|
||||
$goals['future'][$item->id] = $item;
|
||||
}
|
||||
elseif (
|
||||
empty($item->from)
|
||||
|| empty($item->to)
|
||||
|| $now->isBetween($item->from, $item->to)
|
||||
) {
|
||||
$goals['present'][$item->id] = $item;
|
||||
}
|
||||
});
|
||||
return $goals;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,21 +10,21 @@ class Nutrients
|
|||
public static float $gramsPerOunce = 28.349523125;
|
||||
|
||||
public static array $all = [
|
||||
['value' => 'calories', 'unit' => null],
|
||||
['value' => 'fat', 'unit' => 'g'],
|
||||
['value' => 'cholesterol', 'unit' => 'mg'],
|
||||
['value' => 'sodium', 'unit' => 'mg'],
|
||||
['value' => 'carbohydrates', 'unit' => 'g'],
|
||||
['value' => 'protein', 'unit' => 'g'],
|
||||
'calories' => ['value' => 'calories', 'label' => 'calories', 'unit' => null],
|
||||
'carbohydrates' => ['value' => 'carbohydrates', 'label' => 'carbohydrates', 'unit' => 'g'],
|
||||
'cholesterol' => ['value' => 'cholesterol', 'label' => 'cholesterol', 'unit' => 'mg'],
|
||||
'fat' => ['value' => 'fat', 'label' => 'fat', 'unit' => 'g'],
|
||||
'protein' => ['value' => 'protein', 'label' => 'protein', 'unit' => 'g'],
|
||||
'sodium' => ['value' => 'sodium', 'label' => 'sodium', 'unit' => 'mg'],
|
||||
];
|
||||
|
||||
public static array $units = [
|
||||
['value' => 'tsp', 'label' => 'tsp.'],
|
||||
['value' => 'tbsp', 'label' => 'tbsp.'],
|
||||
['value' => 'cup', 'label' => 'cup'],
|
||||
['value' => 'oz', 'label' => 'oz'],
|
||||
['value' => 'gram', 'label' => 'grams'],
|
||||
['value' => 'serving', 'label' => 'servings'],
|
||||
'cup' => ['value' => 'cup', 'label' => 'cup'],
|
||||
'gram' => ['value' => 'gram', 'label' => 'grams'],
|
||||
'oz' => ['value' => 'oz', 'label' => 'oz'],
|
||||
'serving' => ['value' => 'serving', 'label' => 'servings'],
|
||||
'tbsp' => ['value' => 'tbsp', 'label' => 'tbsp.'],
|
||||
'tsp' => ['value' => 'tsp', 'label' => 'tsp.'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateGoalsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('goals', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete();
|
||||
$table->date('from')->nullable();
|
||||
$table->date('to')->nullable();
|
||||
$table->string('frequency')->nullable();
|
||||
$table->string('name');
|
||||
$table->unsignedFloat('goal');
|
||||
$table->timestamps();
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('goals');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
Delete {{ $goal->goal }} 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->summary }}</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,79 @@
|
|||
<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?->toDateString())" />
|
||||
</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?->toDateString())" />
|
||||
</div>
|
||||
|
||||
<!-- Frequency -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="frequency" value="Frequency" />
|
||||
<x-inputs.select name="frequency"
|
||||
class="block w-full"
|
||||
:options="$frequencyOptions"
|
||||
:selectedValue="old('frequency', $goal->frequency)">
|
||||
</x-inputs.select>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="flex-auto">
|
||||
<x-inputs.label for="name" value="Trackable" />
|
||||
<x-inputs.select name="name"
|
||||
class="block w-full"
|
||||
:options="$nameOptions"
|
||||
:selectedValue="old('name', $goal->name)"
|
||||
required>
|
||||
</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 w-full"
|
||||
:value="old('goal', $goal->goal)"
|
||||
required />
|
||||
</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,65 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="leading-tight text-center">
|
||||
<div class="text-2xl font-semibold text-gray-800">{{ Auth::user()->name }}'s Goals</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div>
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ route(Route::current()->getName(), ['date' => $date->copy()->subDay(1)->toDateString()]) }}">
|
||||
<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 18a8 8 0 100-16 8 8 0 000 16zm.707-10.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L9.414 11H13a1 1 0 100-2H9.414l1.293-1.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-base text-gray-500">{{ $date->format('D, j M Y') }}</div>
|
||||
<div>
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ route(Route::current()->getName(), ['date' => $date->copy()->addDay(1)->toDateString()]) }}">
|
||||
<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 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<div class="space-y-4">
|
||||
@forelse($goals['present'] as $goal)
|
||||
<div class="flex space-x-2 items-center">
|
||||
<a class="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="text-red-500 hover:text-red-700 hover:border-red-300 text-sm"
|
||||
href="{{ route('goals.delete', $goal) }}">
|
||||
<svg class="h-6 w-6" 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>
|
||||
<div class="text-lg font-bold">{{ $goal->summary }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div>No goals set.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
|
@ -0,0 +1,40 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight flex flex-auto">
|
||||
{{ $goal->summary }}
|
||||
<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">
|
||||
<div class="grid grid-cols-2 gap-y-1 gap-x-3 max-w-md inline-grid">
|
||||
<div class="font-bold">From</div>
|
||||
<div>{{ $goal->from?->toDateString() ?? 'Any' }}</div>
|
||||
<div class="font-bold">To</div>
|
||||
<div>{{ $goal->to?->toDateString() ?? 'Any' }}</div>
|
||||
<div class="font-bold">Frequency</div>
|
||||
<div>{{ \Illuminate\Support\Str::ucfirst($frequencyOptions[$goal->frequency]['label']) }}</div>
|
||||
<div class="font-bold">Trackable</div>
|
||||
<div>{{ \Illuminate\Support\Str::ucfirst($nameOptions[$goal->name]['label']) }}</div>
|
||||
<div class="font-bold">Goal</div>
|
||||
<div>{{ $goal->goal }}{{ $nameOptions[$goal->name]['unit'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
|
@ -37,22 +37,65 @@
|
|||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<div class="flex align-top flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<div class="w-full sm:w-2/5 md:w-1/3 lg:w-1/4">
|
||||
<h3 class="font-semibold text-xl text-gray-800">{{ $date->format('D, j M Y') }}</h3>
|
||||
<div class="text-gray-700">{{ $entries->count() }} {{ \Illuminate\Support\Pluralizer::plural('entry', $entries->count()) }}</div>
|
||||
<div class="grid grid-cols-2 text-sm border-t-8 border-black pt-2">
|
||||
<div class="font-extrabold text-lg border-b-4 border-black">Calories</div>
|
||||
<div class="font-extrabold text-right text-lg border-b-4 border-black">{{ round($entries->sum('calories'), 2) }}</div>
|
||||
<div class="font-bold border-b border-gray-300">Fat</div>
|
||||
<div class="text-right border-b border-gray-300">{{ round($entries->sum('fat'), 2) }}g</div>
|
||||
<div class="font-bold border-b border-gray-300">Cholesterol</div>
|
||||
<div class="text-right border-b border-gray-300">{{ round($entries->sum('cholesterol'), 2) }}mg</div>
|
||||
<div class="font-bold border-b border-gray-300">Sodium</div>
|
||||
<div class="text-right border-b border-gray-300">{{ round($entries->sum('sodium'), 2) }}mg</div>
|
||||
<div class="font-bold border-b border-gray-300">Carbohydrates</div>
|
||||
<div class="text-right border-b border-gray-300">{{ round($entries->sum('carbohydrates'), 2) }}g</div>
|
||||
<div class="font-bold">Protein</div>
|
||||
<div class="text-right">{{ round($entries->sum('protein'), 2) }}g</div>
|
||||
<div class="w-full sm:w-5/12 lg:w-4/12">
|
||||
<div class="flex justify-between items-baseline">
|
||||
<h3 class="font-semibold text-lg text-gray-800">{{ $date->format('D, j M Y') }}</h3>
|
||||
<div class="text-gray-700">{{ $entries->count() }} {{ \Illuminate\Support\Pluralizer::plural('entry', $entries->count()) }}</div>
|
||||
</div>
|
||||
<div class="text-right border-t-8 border-black text-sm pt-2">% Daily goal</div>
|
||||
<div class="flex justify-between items-baseline border-b-4 border-black">
|
||||
<div>
|
||||
<span class="font-extrabold text-2xl">Calories</span>
|
||||
<span class="text-lg">{{ number_format($sums['calories']) }}</span>
|
||||
</div>
|
||||
<div class="font-extrabold text-right text-lg">
|
||||
{{ $dailyGoals['calories'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-baseline border-b border-gray-300 text-sm">
|
||||
<div>
|
||||
<span class="font-bold">Fat</span>
|
||||
{{ number_format($sums['fat']) }}g
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ $dailyGoals['fat'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-baseline border-b border-gray-300 text-sm">
|
||||
<div>
|
||||
<span class="font-bold">Cholesterol</span>
|
||||
{{ number_format($sums['cholesterol']) }}mg
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ $dailyGoals['cholesterol'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-baseline border-b border-gray-300 text-sm">
|
||||
<div>
|
||||
<span class="font-bold">Sodium</span>
|
||||
{{ number_format($sums['sodium']) }}mg
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ $dailyGoals['sodium'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-baseline border-b border-gray-300 text-sm">
|
||||
<div>
|
||||
<span class="font-bold">Carbohydrates</span>
|
||||
{{ number_format($sums['carbohydrates']) }}g
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ $dailyGoals['carbohydrates'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-baseline text-sm">
|
||||
<div>
|
||||
<span class="font-bold">Protein</span>
|
||||
{{ number_format($sums['protein']) }}g
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ $dailyGoals['protein'] ?? 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full sm:w-3/5 md:w-2/3 lg:w-3/4 flex flex-col space-y-4">
|
||||
|
|
|
@ -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