๐ 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
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.
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');
}
}
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.');
}
}
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>
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');
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>
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
- Create a Test Plant: Use the plant creation form to add a new plant
- Generate QR Code: Generate and download QR code for the test plant
- Test QR Scanning: Scan the QR code with your mobile device
- Add Measurements: Record growth measurements for the plant
- Test Search/Filter: Use the search and filter functionality
- 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 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"