diff --git a/app/Http/Controllers/GoalController.php b/app/Http/Controllers/GoalController.php new file mode 100644 index 0000000..ad0b02e --- /dev/null +++ b/app/Http/Controllers/GoalController.php @@ -0,0 +1,105 @@ +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!"); + } +} diff --git a/app/Http/Controllers/JournalEntryController.php b/app/Http/Controllers/JournalEntryController.php index bff0597..e0db472 100644 --- a/app/Http/Controllers/JournalEntryController.php +++ b/app/Http/Controllers/JournalEntryController.php @@ -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); } /** diff --git a/app/Models/Goal.php b/app/Models/Goal.php new file mode 100644 index 0000000..691e6b7 --- /dev/null +++ b/app/Models/Goal.php @@ -0,0 +1,102 @@ + ['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; + } +} diff --git a/app/Models/JournalEntry.php b/app/Models/JournalEntry.php index ecdbe34..ad678e7 100644 --- a/app/Models/JournalEntry.php +++ b/app/Models/JournalEntry.php @@ -69,7 +69,7 @@ final class JournalEntry extends Model ]; /** - * The attributes that should be cast. + * @inheritdoc */ protected $casts = [ 'calories' => 'float', diff --git a/app/Models/User.php b/app/Models/User.php index 31736f9..36b22c7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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; + } } diff --git a/app/Support/Nutrients.php b/app/Support/Nutrients.php index 1b030d4..8d4ecda 100644 --- a/app/Support/Nutrients.php +++ b/app/Support/Nutrients.php @@ -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.'], ]; /** diff --git a/database/migrations/2021_02_13_052427_create_goals_table.php b/database/migrations/2021_02_13_052427_create_goals_table.php new file mode 100644 index 0000000..a46b999 --- /dev/null +++ b/database/migrations/2021_02_13_052427_create_goals_table.php @@ -0,0 +1,39 @@ +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'); + } +} diff --git a/resources/views/goals/delete.blade.php b/resources/views/goals/delete.blade.php new file mode 100644 index 0000000..a44182b --- /dev/null +++ b/resources/views/goals/delete.blade.php @@ -0,0 +1,28 @@ + + +

+ Delete {{ $goal->goal }} goal? +

+
+ +
+
+
+
+
+ @method('delete') + @csrf +
+ Are you sure what to delete your {{ $goal->summary }} goal? +
+ + Yes, delete + + No, do not delete +
+
+
+
+
+
diff --git a/resources/views/goals/edit.blade.php b/resources/views/goals/edit.blade.php new file mode 100644 index 0000000..94e374b --- /dev/null +++ b/resources/views/goals/edit.blade.php @@ -0,0 +1,79 @@ + + +

+ {{ ($goal->exists ? 'Edit' : 'Add') }} Goal +

+
+ +
+
+
+
+
+ @if ($goal->exists)@method('put')@endif + @csrf +
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + +
+
+
+ +
+ + {{ ($goal->exists ? 'Save' : 'Add') }} + +
+
+
+
+
+
+
diff --git a/resources/views/goals/index.blade.php b/resources/views/goals/index.blade.php new file mode 100644 index 0000000..c84e01d --- /dev/null +++ b/resources/views/goals/index.blade.php @@ -0,0 +1,65 @@ + + +
+

+
{{ Auth::user()->name }}'s Goals
+
+ +
{{ $date->format('D, j M Y') }}
+ +
+

+ + + + + Add Goal + +
+
+ +
+
+
+
+
+ @forelse($goals['present'] as $goal) +
+ + + + + + + + + + + +
{{ $goal->summary }}
+
+ @empty +
No goals set.
+ @endforelse +
+
+
+
+
+
diff --git a/resources/views/goals/show.blade.php b/resources/views/goals/show.blade.php new file mode 100644 index 0000000..853f079 --- /dev/null +++ b/resources/views/goals/show.blade.php @@ -0,0 +1,40 @@ + + +

+ {{ $goal->summary }} + + + + + + + + + + + +

+
+
+
+
+
+
+
From
+
{{ $goal->from?->toDateString() ?? 'Any' }}
+
To
+
{{ $goal->to?->toDateString() ?? 'Any' }}
+
Frequency
+
{{ \Illuminate\Support\Str::ucfirst($frequencyOptions[$goal->frequency]['label']) }}
+
Trackable
+
{{ \Illuminate\Support\Str::ucfirst($nameOptions[$goal->name]['label']) }}
+
Goal
+
{{ $goal->goal }}{{ $nameOptions[$goal->name]['unit'] }}
+
+
+
+
+
+
diff --git a/resources/views/journal-entries/index.blade.php b/resources/views/journal-entries/index.blade.php index b4a1a46..3851411 100644 --- a/resources/views/journal-entries/index.blade.php +++ b/resources/views/journal-entries/index.blade.php @@ -37,22 +37,65 @@
-
-

{{ $date->format('D, j M Y') }}

-
{{ $entries->count() }} {{ \Illuminate\Support\Pluralizer::plural('entry', $entries->count()) }}
-
-
Calories
-
{{ round($entries->sum('calories'), 2) }}
-
Fat
-
{{ round($entries->sum('fat'), 2) }}g
-
Cholesterol
-
{{ round($entries->sum('cholesterol'), 2) }}mg
-
Sodium
-
{{ round($entries->sum('sodium'), 2) }}mg
-
Carbohydrates
-
{{ round($entries->sum('carbohydrates'), 2) }}g
-
Protein
-
{{ round($entries->sum('protein'), 2) }}g
+
+
+

{{ $date->format('D, j M Y') }}

+
{{ $entries->count() }} {{ \Illuminate\Support\Pluralizer::plural('entry', $entries->count()) }}
+
+
% Daily goal
+
+
+ Calories + {{ number_format($sums['calories']) }} +
+
+ {{ $dailyGoals['calories'] ?? 'N/A' }} +
+
+
+
+ Fat + {{ number_format($sums['fat']) }}g +
+
+ {{ $dailyGoals['fat'] ?? 'N/A' }} +
+
+
+
+ Cholesterol + {{ number_format($sums['cholesterol']) }}mg +
+
+ {{ $dailyGoals['cholesterol'] ?? 'N/A' }} +
+
+
+
+ Sodium + {{ number_format($sums['sodium']) }}mg +
+
+ {{ $dailyGoals['sodium'] ?? 'N/A' }} +
+
+
+
+ Carbohydrates + {{ number_format($sums['carbohydrates']) }}g +
+
+ {{ $dailyGoals['carbohydrates'] ?? 'N/A' }} +
+
+
+
+ Protein + {{ number_format($sums['protein']) }}g +
+
+ {{ $dailyGoals['protein'] ?? 'N/A' }} +
diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index 268f123..229c0e6 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -62,6 +62,10 @@
{{ Auth::user()->email }}
+
+ Goals +
+
diff --git a/routes/web.php b/routes/web.php index 89af6ab..7a27799 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ 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';