๐ŸŒฑ Phase 2: Core Plant Tracking

QR Code System & Plant Management Interface

Duration: 2-3 weeks | Complexity: Intermediate

๐Ÿ“‹ Phase Overview

This phase builds the core functionality for tracking individual blackberry plants. You'll create Eloquent models for your existing database tables, build comprehensive plant management interfaces, implement QR code generation and scanning, and create mobile-responsive forms for field data collection.

End Goal: A fully functional plant tracking system where each plant can be managed via QR code, with comprehensive growth monitoring and mobile-friendly interfaces.

โš ๏ธ Prerequisites

  • Phase 1 Completed: Laravel installation and authentication working
  • Database Access: Connection to wwwhom8_blackberries established
  • Composer Access: To install QR code package
  • Mobile Device: For testing QR code scanning
1

Install QR Code Generation Package

Install SimpleSoftwareIO Simple QR Code package for generating QR codes.

Installation:

# Install QR code package composer require simplesoftwareio/simple-qrcode # Publish package configuration (optional) php artisan vendor:publish --provider="SimpleSoftwareIO\QrCode\QrCodeServiceProvider"

Test Installation:

# Test in tinker php artisan tinker > QrCode::size(200)->generate('Hello World'); > exit
Expected Result: QR code package installed and ready to generate codes.
2

Create Eloquent Models for Existing Tables

Create Laravel models that map to your existing database tables.

Create Plant Model:

# Create Plant model php artisan make:model Plant # Create PlantVariety model php artisan make:model PlantVariety # Create PlantMeasurement model php artisan make:model PlantMeasurement # Create Harvest model php artisan make:model Harvest # Create QrCode model php artisan make:model QrCode

Plant Model (app/Models/Plant.php):

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Plant extends Model { protected $table = 'plants'; protected $primaryKey = 'plant_id'; public $incrementing = false; protected $keyType = 'string'; protected $fillable = [ 'plant_id', 'farm_id', 'variety_id', 'parent_plant_id', 'propagation_method', 'planted_date', 'location_row', 'location_position', 'coordinates_lat', 'coordinates_lng', 'status', 'source', 'initial_height_cm', 'initial_cost', 'qr_code_generated', 'notes' ]; protected $casts = [ 'planted_date' => 'date', 'coordinates_lat' => 'decimal:8', 'coordinates_lng' => 'decimal:8', 'initial_height_cm' => 'decimal:2', 'initial_cost' => 'decimal:2', 'qr_code_generated' => 'boolean', 'created_at' => 'datetime', 'updated_at' => 'datetime' ]; // Relationships public function variety(): BelongsTo { return $this->belongsTo(PlantVariety::class, 'variety_id'); } public function measurements(): HasMany { return $this->hasMany(PlantMeasurement::class, 'plant_id', 'plant_id'); } public function harvests(): HasMany { return $this->hasMany(Harvest::class, 'plant_id', 'plant_id'); } public function qrCodes(): HasMany { return $this->hasMany(QrCode::class, 'plant_id', 'plant_id'); } public function parentPlant(): BelongsTo { return $this->belongsTo(Plant::class, 'parent_plant_id', 'plant_id'); } public function children(): HasMany { return $this->hasMany(Plant::class, 'parent_plant_id', 'plant_id'); } // Helper methods public function getAgeInDays(): int { return $this->planted_date->diffInDays(now()); } public function getCurrentHeight(): ?float { return $this->measurements()->latest()->first()?->height_cm; } public function getLatestHealthScore(): ?float { return $this->measurements()->latest()->first()?->health_score; } public function getTotalYield(): float { return $this->harvests()->sum('yield_lbs'); } }

PlantVariety Model (app/Models/PlantVariety.php):

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; class PlantVariety extends Model { protected $table = 'plant_varieties'; protected $primaryKey = 'variety_id'; protected $fillable = [ 'variety_name', 'variety_type', 'expected_yield_year1', 'expected_yield_mature', 'maturity_days', 'harvest_seasons', 'cold_hardiness_zone', 'thornless', 'disease_resistance', 'notes' ]; protected $casts = [ 'expected_yield_year1' => 'decimal:2', 'expected_yield_mature' => 'decimal:2', 'thornless' => 'boolean', 'harvest_seasons' => 'array', 'created_at' => 'datetime' ]; public function plants(): HasMany { return $this->hasMany(Plant::class, 'variety_id'); } }
3

Create Controllers for Plant Management

Create controllers to handle plant CRUD operations and QR code functionality.

Create Controllers:

# Create Plant controller with resource methods php artisan make:controller PlantController --resource # Create QR code controller php artisan make:controller QrCodeController # Create Plant Measurement controller php artisan make:controller PlantMeasurementController

PlantController (app/Http/Controllers/PlantController.php):

<?php namespace App\Http\Controllers; use App\Models\Plant; use App\Models\PlantVariety; use Illuminate\Http\Request; use Illuminate\View\View; use Illuminate\Http\RedirectResponse; class PlantController extends Controller { public function index(Request $request): View { $plants = Plant::with('variety') ->when($request->search, function ($query, $search) { $query->where('plant_id', 'like', "%{$search}%") ->orWhere('location_row', 'like', "%{$search}%"); }) ->when($request->variety, function ($query, $variety) { $query->where('variety_id', $variety); }) ->when($request->status, function ($query, $status) { $query->where('status', $status); }) ->orderBy('plant_id') ->paginate(20); $varieties = PlantVariety::all(); return view('plants.index', compact('plants', 'varieties')); } public function show(Plant $plant): View { $plant->load(['variety', 'measurements' => function($query) { $query->latest()->limit(10); }, 'harvests' => function($query) { $query->latest()->limit(5); }, 'qrCodes']); return view('plants.show', compact('plant')); } public function create(): View { $varieties = PlantVariety::all(); $parentPlants = Plant::where('status', 'active')->get(); return view('plants.create', compact('varieties', 'parentPlants')); } public function store(Request $request): RedirectResponse { $validated = $request->validate([ 'plant_id' => 'required|string|max:50|unique:plants', 'variety_id' => 'required|exists:plant_varieties,variety_id', 'planted_date' => 'required|date', 'location_row' => 'nullable|string|max:20', 'location_position' => 'nullable|string|max:20', 'parent_plant_id' => 'nullable|exists:plants,plant_id', 'propagation_method' => 'nullable|string|max:50', 'source' => 'nullable|string|max:100', 'initial_height_cm' => 'nullable|numeric|min:0', 'initial_cost' => 'nullable|numeric|min:0', 'notes' => 'nullable|string' ]); $validated['farm_id'] = 1; // Default farm ID $validated['status'] = 'active'; $plant = Plant::create($validated); return redirect()->route('plants.show', $plant) ->with('success', 'Plant created successfully!'); } public function edit(Plant $plant): View { $varieties = PlantVariety::all(); $parentPlants = Plant::where('status', 'active') ->where('plant_id', '!=', $plant->plant_id)->get(); return view('plants.edit', compact('plant', 'varieties', 'parentPlants')); } public function update(Request $request, Plant $plant): RedirectResponse { $validated = $request->validate([ 'variety_id' => 'required|exists:plant_varieties,variety_id', 'planted_date' => 'required|date', 'location_row' => 'nullable|string|max:20', 'location_position' => 'nullable|string|max:20', 'parent_plant_id' => 'nullable|exists:plants,plant_id', 'propagation_method' => 'nullable|string|max:50', 'status' => 'required|in:active,dormant,removed,dead', 'source' => 'nullable|string|max:100', 'notes' => 'nullable|string' ]); $plant->update($validated); return redirect()->route('plants.show', $plant) ->with('success', 'Plant updated successfully!'); } public function destroy(Plant $plant): RedirectResponse { $plant->update(['status' => 'removed']); return redirect()->route('plants.index') ->with('success', 'Plant marked as removed.'); } }
4

Create Plant Management Views

Create Blade templates for plant management interfaces.

Create Views Directory:

# Create plants views directory mkdir -p resources/views/plants

Plants Index View (resources/views/plants/index.blade.php):

<x-app-layout> <x-slot name="header"> <div class="flex justify-between items-center"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Plants Management </h2> <a href="{{ route('plants.create') }}" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"> Add New Plant </a> </div> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <!-- Search and Filter Form --> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg mb-6"> <div class="p-6"> <form method="GET" class="grid grid-cols-1 md:grid-cols-4 gap-4"> <input type="text" name="search" placeholder="Search Plant ID or Location" value="{{ request('search') }}" class="border rounded px-3 py-2"> <select name="variety" class="border rounded px-3 py-2"> <option value="">All Varieties</option> @foreach($varieties as $variety) <option value="{{ $variety->variety_id }}" {{ request('variety') == $variety->variety_id ? 'selected' : '' }}> {{ $variety->variety_name }} </option> @endforeach </select> <select name="status" class="border rounded px-3 py-2"> <option value="">All Status</option> <option value="active" {{ request('status') == 'active' ? 'selected' : '' }}>Active</option> <option value="dormant" {{ request('status') == 'dormant' ? 'selected' : '' }}>Dormant</option> <option value="removed" {{ request('status') == 'removed' ? 'selected' : '' }}>Removed</option> </select> <button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"> Filter </button> </form> </div> </div> <!-- Plants Grid --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> @foreach($plants as $plant) <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg hover:shadow-lg transition-shadow"> <div class="p-6"> <div class="flex justify-between items-start mb-4"> <h3 class="text-lg font-semibold text-gray-900"> {{ $plant->plant_id }} </h3> <span class="px-2 py-1 text-xs rounded-full @if($plant->status == 'active') bg-green-100 text-green-800 @elseif($plant->status == 'dormant') bg-yellow-100 text-yellow-800 @else bg-red-100 text-red-800 @endif"> {{ ucfirst($plant->status) }} </span> </div> <div class="space-y-2 text-sm text-gray-600"> <p><strong>Variety:</strong> {{ $plant->variety->variety_name ?? 'Unknown' }}</p> <p><strong>Location:</strong> Row {{ $plant->location_row }}, Pos {{ $plant->location_position }}</p> <p><strong>Age:</strong> {{ $plant->getAgeInDays() }} days</p> <p><strong>Height:</strong> {{ $plant->getCurrentHeight() ?? 'N/A' }} cm</p> <p><strong>Total Yield:</strong> {{ number_format($plant->getTotalYield(), 2) }} lbs</p> </div> <div class="mt-4 flex space-x-2"> <a href="{{ route('plants.show', $plant) }}" class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"> View </a> <a href="{{ route('plants.edit', $plant) }}" class="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600"> Edit </a> </div> </div> </div> @endforeach </div> <!-- Pagination --> <div class="mt-6"> {{ $plants->appends(request()->query())->links() }} </div> </div> </div> </x-app-layout>
5

QR Code Generation & Scanning

Implement QR code generation for plants and create scanning interface.

QrCodeController (app/Http/Controllers/QrCodeController.php):

<?php namespace App\Http\Controllers; use App\Models\Plant; use App\Models\QrCode; use Illuminate\Http\Request; use SimpleSoftwareIO\QrCode\Facades\QrCode as QrCodeGenerator; use Illuminate\Http\Response; class QrCodeController extends Controller { public function generate(Plant $plant): Response { // Generate QR code URL $qrUrl = route('plants.scan', $plant->plant_id); // Generate QR code image $qrCode = QrCodeGenerator::size(300) ->margin(2) ->format('png') ->generate($qrUrl); // Save QR code record QrCode::updateOrCreate( ['plant_id' => $plant->plant_id], [ 'qr_data' => $qrUrl, 'qr_format' => 'png', 'qr_size' => '300x300', 'generated_date' => now()->toDateString(), 'active' => true ] ); // Update plant record $plant->update(['qr_code_generated' => true]); return response($qrCode) ->header('Content-Type', 'image/png') ->header('Content-Disposition', 'attachment; filename="plant-' . $plant->plant_id . '-qr.png"'); } public function scan(Request $request, $plantId) { $plant = Plant::with(['variety', 'measurements' => function($query) { $query->latest()->limit(3); }])->findOrFail($plantId); // Log scan activity $qrCode = QrCode::where('plant_id', $plantId)->first(); if ($qrCode) { $qrCode->increment('scan_count'); $qrCode->update(['last_scanned_date' => now()]); } // Check if request is from mobile $isMobile = $request->header('User-Agent') && (strpos($request->header('User-Agent'), 'Mobile') !== false); return view('plants.scan', compact('plant', 'isMobile')); } public function bulkGenerate(Request $request) { $plantIds = $request->input('plant_ids', []); $plants = Plant::whereIn('plant_id', $plantIds)->get(); $results = []; foreach ($plants as $plant) { $qrUrl = route('plants.scan', $plant->plant_id); QrCode::updateOrCreate( ['plant_id' => $plant->plant_id], [ 'qr_data' => $qrUrl, 'qr_format' => 'png', 'qr_size' => '300x300', 'generated_date' => now()->toDateString(), 'active' => true ] ); $plant->update(['qr_code_generated' => true]); $results[] = $plant->plant_id; } return response()->json([ 'success' => true, 'generated' => count($results), 'plant_ids' => $results ]); } }

Add QR Routes (routes/web.php):

// Add these routes to your web.php file Route::middleware('auth')->group(function () { Route::resource('plants', PlantController::class); Route::get('/plants/{plant}/qr-generate', [QrCodeController::class, 'generate'])->name('plants.qr.generate'); Route::post('/qr/bulk-generate', [QrCodeController::class, 'bulkGenerate'])->name('qr.bulk.generate'); }); // Public scan route (no auth required for field scanning) Route::get('/scan/{plantId}', [QrCodeController::class, 'scan'])->name('plants.scan');
6

Mobile-Responsive Scanning Interface

Create a mobile-optimized interface for QR code scanning and quick plant updates.

Plant Scan View (resources/views/plants/scan.blade.php):

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <title>Plant: {{ $plant->plant_id }} - Quick View</title> <script src="https://cdn.tailwindcss.com"></script> <style> /* Mobile-optimized styles */ .touch-button { min-height: 44px; min-width: 44px; } .large-text { font-size: 18px; line-height: 1.4; } </style> </head> <body class="bg-gray-100"> <div class="min-h-screen"> <!-- Header --> <div class="bg-green-600 text-white p-4 shadow-lg"> <div class="flex items-center justify-between"> <h1 class="text-xl font-bold">๐ŸŒฑ {{ $plant->plant_id }}</h1> @auth <a href="{{ route('plants.show', $plant) }}" class="bg-white text-green-600 px-3 py-1 rounded touch-button"> Full View </a> @else <a href="{{ route('login') }}" class="bg-white text-green-600 px-3 py-1 rounded touch-button"> Login </a> @endauth </div> </div> <!-- Plant Info Cards --> <div class="p-4 space-y-4"> <!-- Basic Info Card --> <div class="bg-white rounded-lg shadow p-4"> <h2 class="text-lg font-semibold mb-3 text-gray-800">Plant Information</h2> <div class="grid grid-cols-2 gap-3 large-text"> <div> <span class="font-medium text-gray-600">Variety:</span> <br>{{ $plant->variety->variety_name ?? 'Unknown' }} </div> <div> <span class="font-medium text-gray-600">Status:</span> <br><span class="px-2 py-1 text-sm rounded-full @if($plant->status == 'active') bg-green-100 text-green-800 @else bg-yellow-100 text-yellow-800 @endif"> {{ ucfirst($plant->status) }} </span> </div> <div> <span class="font-medium text-gray-600">Location:</span> <br>Row {{ $plant->location_row }}, Pos {{ $plant->location_position }} </div> <div> <span class="font-medium text-gray-600">Age:</span> <br>{{ $plant->getAgeInDays() }} days </div> </div> </div> <!-- Latest Measurements --> <div class="bg-white rounded-lg shadow p-4"> <h2 class="text-lg font-semibold mb-3 text-gray-800">Latest Measurements</h2> @if($plant->measurements->count() > 0) @foreach($plant->measurements->take(2) as $measurement) <div class="border-b pb-2 mb-2 last:border-b-0 large-text"> <div class="flex justify-between items-center"> <span class="font-medium">{{ $measurement->measurement_date->format('M j, Y') }}</span> <span class="text-sm text-gray-500"> {{ $measurement->measurement_date->diffForHumans() }} </span> </div> <div class="grid grid-cols-2 gap-2 mt-2 text-sm"> @if($measurement->height_cm) <div><strong>Height:</strong> {{ $measurement->height_cm }}cm</div> @endif @if($measurement->health_score) <div><strong>Health:</strong> {{ $measurement->health_score }}/10</div> @endif </div> </div> @endforeach @else <p class="text-gray-500 large-text">No measurements recorded yet.</p> @endif </div> <!-- Quick Actions --> @auth <div class="bg-white rounded-lg shadow p-4"> <h2 class="text-lg font-semibold mb-3 text-gray-800">Quick Actions</h2> <div class="grid grid-cols-2 gap-3"> <a href="{{ route('plants.edit', $plant) }}" class="bg-blue-500 text-white p-3 rounded text-center touch-button hover:bg-blue-600"> ๐Ÿ“ Edit Plant </a> <button onclick="addMeasurement()" class="bg-green-500 text-white p-3 rounded touch-button hover:bg-green-600"> ๐Ÿ“ Add Measurement </button> <button onclick="recordHarvest()" class="bg-yellow-500 text-white p-3 rounded touch-button hover:bg-yellow-600"> ๐Ÿซ Record Harvest </button> <button onclick="shareLocation()" class="bg-purple-500 text-white p-3 rounded touch-button hover:bg-purple-600"> ๐Ÿ“ Share Location </button> </div> </div> @endauth <!-- Scan Information --> <div class="bg-blue-50 rounded-lg p-4"> <h3 class="font-medium text-blue-800 mb-2">๐Ÿ“ฑ Mobile Scanning</h3> <p class="text-blue-700 text-sm"> This page was accessed via QR code scan. Bookmark for quick access or use the plant management system for full features. </p> </div> </div> </div> <script> // Mobile-friendly JavaScript functions function addMeasurement() { if (confirm('Redirect to add measurement form?')) { window.location.href = '{{ route("plants.show", $plant) }}#measurements'; } } function recordHarvest() { if (confirm('Redirect to harvest recording form?')) { window.location.href = '{{ route("plants.show", $plant) }}#harvests'; } } function shareLocation() { if (navigator.share) { navigator.share({ title: 'Plant {{ $plant->plant_id }}', text: 'Blackberry Plant Location', url: window.location.href }); } else if (navigator.clipboard) { navigator.clipboard.writeText(window.location.href); alert('Location URL copied to clipboard!'); } } </script> </body> </html>
7

Plant Measurement Logging System

Create a comprehensive measurement logging system for tracking plant growth and health.

Create PlantMeasurementController:

<?php namespace App\Http\Controllers; use App\Models\Plant; use App\Models\PlantMeasurement; use Illuminate\Http\Request; class PlantMeasurementController extends Controller { public function create(Plant $plant) { return view('measurements.create', compact('plant')); } public function store(Request $request, Plant $plant) { $validated = $request->validate([ 'measurement_date' => 'required|date', 'height_cm' => 'nullable|numeric|min:0', 'cane_count_primary' => 'nullable|integer|min:0', 'cane_count_secondary' => 'nullable|integer|min:0', 'cane_length_avg_cm' => 'nullable|numeric|min:0', 'leaf_health' => 'nullable|in:excellent,good,fair,poor', 'max_berry_size_mm' => 'nullable|numeric|min:0', 'berry_sweetness_brix' => 'nullable|numeric|min:0|max:30', 'health_score' => 'required|numeric|min:1|max:10', 'flowering_stage' => 'nullable|string', 'pest_issues' => 'nullable|string', 'disease_issues' => 'nullable|string', 'growth_vigor' => 'nullable|in:excellent,good,average,poor', 'growth_notes' => 'nullable|string', 'weather_conditions' => 'nullable|string' ]); $validated['plant_id'] = $plant->plant_id; $validated['measured_by'] = auth()->user()->name; PlantMeasurement::create($validated); return redirect()->route('plants.show', $plant) ->with('success', 'Measurement recorded successfully!'); } }

Add Measurement Routes:

// Add to routes/web.php Route::middleware('auth')->group(function () { Route::get('/plants/{plant}/measurements/create', [PlantMeasurementController::class, 'create']) ->name('measurements.create'); Route::post('/plants/{plant}/measurements', [PlantMeasurementController::class, 'store']) ->name('measurements.store'); });

โœ… Phase 2 Completion Checklist

  • QR code package installed and configured
  • Eloquent models created for all plant-related tables
  • Plant controller with full CRUD operations
  • Plant management views (index, show, create, edit)
  • QR code generation and scanning system
  • Mobile-responsive scanning interface
  • Plant measurement logging system
  • Search and filter functionality for plants
  • Plant status management (active, dormant, removed)
  • Bulk QR code generation capability

๐Ÿงช Testing Phase 2 Features

  1. Create a Test Plant: Use the plant creation form to add a new plant
  2. Generate QR Code: Generate and download QR code for the test plant
  3. Test QR Scanning: Scan the QR code with your mobile device
  4. Add Measurements: Record growth measurements for the plant
  5. Test Search/Filter: Use the search and filter functionality
  6. Mobile Testing: Verify mobile responsiveness of all interfaces

๐ŸŽฏ Next Steps: Phase 3 Preparation

With Phase 2 complete, you're ready for Phase 3: Operations Modules. The next phase will involve:

  • Watering logs with weather integration
  • Fertilizer application tracking
  • Soil test management
  • Air layering tracking system
  • Labor and equipment logging
๐Ÿ’พ

Create Phase 2 Backup

# Create database backup mysqldump -u wwwhom8_main -p wwwhom8_blackberries > phase2_backup.sql # Create git commit git add . git commit -m "Phase 2: Core Plant Tracking completed - QR codes, plant management, mobile interface" # Create git tag git tag -a v2.0-phase2 -m "Phase 2: Core Plant Tracking completed"