orderBy('vintage', 'desc') ->paginate(15); return view('wines.index', compact('wines')); } /** * Display wine catalog for e-shop (with search and filters) */ public function catalog(Request $request) { $query = Wine::with('grapeVariety') ->where('status', 'ready') // Only show wines marked as available for sale ->where('bottles_in_stock', '>', 0); // Only show wines in stock // Search by wine name if ($request->filled('search')) { $query->where('wine_name', 'like', '%' . $request->search . '%'); } // Filter by grape variety if ($request->filled('variety') && $request->variety !== 'all') { $query->where('grape_variety_id', $request->variety); } // Filter by vintage if ($request->filled('vintage') && $request->vintage !== 'all') { $query->where('vintage', $request->vintage); } // Filter by wine type if ($request->filled('wine_type') && $request->wine_type !== 'all') { $query->where('wine_type', $request->wine_type); } // Filter by sweetness if ($request->filled('sweetness') && $request->sweetness !== 'all') { $query->where('sweetness', $request->sweetness); } $wines = $query->orderBy('vintage', 'desc')->paginate(12); // Get all grape varieties for filter dropdown (distinct by name to avoid duplicates) $grapeVarieties = GrapeVariety::all() ->unique('variety_name') ->sortBy('variety_name') ->values(); // Get all unique vintages for filter dropdown $vintages = Wine::where('status', 'ready') ->where('bottles_in_stock', '>', 0) ->distinct() ->pluck('vintage') ->sort() ->values(); // Get all unique wine types for filter dropdown $wineTypes = Wine::where('status', 'ready') ->where('bottles_in_stock', '>', 0) ->whereNotNull('wine_type') ->distinct() ->pluck('wine_type') ->values(); // Get all unique sweetness levels for filter dropdown $sweetnessLevels = Wine::where('status', 'ready') ->where('bottles_in_stock', '>', 0) ->whereNotNull('sweetness') ->distinct() ->pluck('sweetness') ->values(); return view('catalog.index', compact('wines', 'grapeVarieties', 'vintages', 'wineTypes', 'sweetnessLevels')); } /** * Show the form for creating a new wine. */ public function create() { $grapeVarieties = GrapeVariety::all(); return view('wines.create', compact('grapeVarieties')); } /** * Store a newly created wine in storage. */ public function store(StoreWineRequest $request) { $validated = $request->validated(); $wine = Wine::create($validated); return redirect()->route('wines.show', $wine) ->with('success', 'Wine created successfully.'); } /** * Display the specified wine. */ public function show($id) { $wine = Wine::with('grapeVariety', 'harvests.plannedTask')->findOrFail($id); // Check if this is a catalog request (from the route) if (request()->route()->getName() === 'catalog.show') { return view('catalog.show', compact('wine')); } return view('wines.show', compact('wine')); } /** * Show the form for editing the specified wine. */ public function edit(Wine $wine) { $grapeVarieties = GrapeVariety::all(); return view('wines.edit', compact('wine', 'grapeVarieties')); } /** * Update the specified wine in storage. */ public function update(UpdateWineRequest $request, Wine $wine) { $validated = $request->validated(); // Handle image upload if ($request->hasFile('image')) { // Delete old image if exists if ($wine->image_url) { \Storage::disk('public')->delete($wine->image_url); } $validated['image_url'] = $request->file('image')->store('wines', 'public'); } // Remove the 'image' key from validated data (we only want 'image_url') unset($validated['image']); $wine->update($validated); return redirect()->route('wines.show', $wine) ->with('success', 'Wine updated successfully.'); } /** * Remove the specified wine from storage. */ public function destroy(Wine $wine) { $wineName = $wine->wine_name; $wine->delete(); return redirect()->route('winemaker.cellar.index') ->with('success', "Wine '{$wineName}' has been permanently removed from the cellar."); } /** * Display cellar wine inventory (for winemaker) */ public function cellarIndex() { $wines = Wine::with('grapeVariety') ->orderBy('vintage', 'desc') ->orderBy('wine_name') ->get(); return view('winemaker.cellar.index', compact('wines')); } /** * Display wines available for sales management (for winemaker) */ public function salesIndex() { $wines = Wine::with('grapeVariety') ->where('bottles_in_stock', '>', 0) ->orderBy('vintage', 'desc') ->orderBy('wine_name') ->paginate(15); return view('winemaker.sales.index', compact('wines')); } /** * Add wine to sales (mark as available) */ public function addToSales($id) { $wine = Wine::findOrFail($id); if ($wine->status === 'in_production') { return redirect()->back()->with('error', 'Cannot add wine that is still in production to catalog.'); } if ($wine->bottles_in_stock <= 0) { return redirect()->back()->with('error', 'Cannot add wine without stock to catalog.'); } if (!$wine->price_per_bottle) { return redirect()->back()->with('error', 'Please set a price before adding wine to catalog.'); } $wine->update(['status' => 'ready']); return redirect()->back()->with('success', 'Wine has been added to the catalog.'); } /** * Remove wine from sales (mark as not available) */ public function removeFromSales($id) { $wine = Wine::findOrFail($id); $wine->update(['status' => 'aging']); return redirect()->back()->with('success', 'Wine has been removed from the catalog.'); } /** * Show form to create blended wine from multiple harvests */ public function createBlendForm() { $availableHarvests = Harvest::with(['varietyVariation.grapeVariety', 'wineProductions', 'plannedTask']) ->where(function ($query) { $query->whereHas('plannedTask', function ($sub) { $sub->whereNotNull('execution_date'); })->orWhereDoesntHave('plannedTask'); }) ->get() ->filter(function ($harvest) { return $harvest->remaining_weight > 0.01; }) ->sortByDesc(function ($harvest) { return $harvest->plannedTask?->execution_date ?? $harvest->date; }); return view('winemaker.cellar.create-blend', compact('availableHarvests')); } /** * Store a new blended wine from multiple harvests */ public function storeBlend(Request $request) { $validated = $request->validate([ 'harvests' => 'required|array|min:1', 'harvests.*.selected' => 'sometimes', 'harvests.*.weight' => 'required_with:harvests.*.selected|numeric|min:0.01', 'wine_name' => 'required|string|max:255', 'vintage' => 'required|integer|min:1900|max:' . (date('Y') + 1), 'wine_type' => 'required|in:red,white,rose', 'sweetness' => 'required|in:dry,semi_dry,semi_sweet,sweet', 'alcohol_percentage' => 'required|numeric|min:0|max:20', 'bottles_produced' => 'required|integer|min:1', 'bottle_volume' => 'nullable|numeric|min:0.1|max:10', 'production_date' => 'required|date', 'bottling_date' => 'nullable|date', 'status' => 'required|in:in_production,aging,ready,sold_out', 'price_per_bottle' => 'nullable|numeric|min:0', 'description' => 'nullable|string', 'image' => 'nullable|image|mimes:jpeg,jpg,png,webp|max:2048', ]); try { DB::beginTransaction(); // Filter selected harvests and calculate total weight $selectedHarvests = []; $totalWeight = 0; foreach ($validated['harvests'] as $harvestId => $data) { if (isset($data['selected']) && isset($data['weight']) && $data['weight'] > 0) { $harvest = Harvest::with('varietyVariation.grapeVariety')->findOrFail($harvestId); // Validate weight if ($data['weight'] > $harvest->remaining_weight) { return back() ->withInput() ->with('error', 'Harvest #' . $harvestId . ': Cannot use ' . $data['weight'] . ' kg. Only ' . number_format($harvest->remaining_weight, 2) . ' kg available.'); } $selectedHarvests[] = [ 'harvest' => $harvest, 'weight' => $data['weight'] ]; $totalWeight += $data['weight']; } } if (count($selectedHarvests) === 0) { return back() ->withInput() ->with('error', 'Please select at least one harvest with a weight.'); } // Determine primary grape variety (from harvest with highest weight) $primaryHarvest = collect($selectedHarvests)->sortByDesc('weight')->first(); $grapeVariety = $primaryHarvest['harvest']->varietyVariation->grapeVariety; // Handle image upload $imagePath = null; if ($request->hasFile('image')) { $imagePath = $request->file('image')->store('wines', 'public'); } // Create the wine $wine = Wine::create([ 'wine_name' => $validated['wine_name'], 'vintage' => $validated['vintage'], 'grape_variety_id' => $grapeVariety->id, 'wine_type' => $validated['wine_type'], 'sweetness' => $validated['sweetness'], 'alcohol_percentage' => $validated['alcohol_percentage'], 'bottles_produced' => $validated['bottles_produced'], 'bottles_in_stock' => $validated['bottles_produced'], 'bottle_volume' => $validated['bottle_volume'] ?? 0.75, 'production_date' => $validated['production_date'], 'bottling_date' => $validated['bottling_date'], 'status' => $validated['status'], 'price_per_bottle' => $validated['price_per_bottle'], 'description' => $validated['description'], 'image_url' => $imagePath, ]); // Create wine production records for each harvest $harvestIds = []; foreach ($selectedHarvests as $item) { $blendPercentage = ($item['weight'] / $totalWeight) * 100; WineProduction::create([ 'wine_id' => $wine->id, 'harvest_id' => $item['harvest']->id, 'consumed_weight' => $item['weight'], 'blend_percentage' => $blendPercentage, ]); $harvestIds[] = '#' . $item['harvest']->id; } DB::commit(); $message = 'Blended wine created successfully from ' . count($selectedHarvests) . ' harvest(s): ' . implode(', ', $harvestIds); $message .= '. Total blend: ' . number_format($totalWeight, 2) . ' kg'; return redirect()->route('winemaker.cellar.index') ->with('success', $message); } catch (\Exception $e) { DB::rollBack(); return back() ->withInput() ->with('error', 'Failed to create blended wine: ' . $e->getMessage()); } } }