customer relationship management using laravel 12

Customer Relationship Management (Mini CRM) system built on Laravel 12 — including Contacts, Deals, Notes, Stages, Filtering, and Authentication. Everything is structured so you can copy & paste directly into a fresh Laravel 12 project.

I reference the official Laravel docs for the installation steps.

Mini CRM App — Laravel 12

Your CRM will include:

Entities

✔ Customers
✔ Deals associated with each customer
✔ Notes (timeline activities)
✔ Deal stages (New → Contacted → Proposal → Won/Lost)
✔ User authentication (Laravel Breeze)
✔ Search + filter
✔ Dashboard (Total customers, active deals, pipeline value)

1) Install Laravel 12

 

 bash

composer create-project laravel/laravel mini-crm
cd mini-crm


2) Install Auth (Breeze)
 bash

composer require laravel/breeze --dev
php artisan breeze:install blade
npm install
npm run dev
php artisan migrate


(Breeze usage docs.)

3) Models You Need

We will generate CRM models:

Customer

Deal

Note

Run:

 bash

php artisan make:model Customer -m -c -r -f
php artisan make:model Deal -m -c -r -f
php artisan make:model Note -m -c -r -f


4) Migrations (Full CRM Schema)

customers table

 bash

// database/migrations/xxxx_create_customers_table.php
public function up()
{
    Schema::create('customers', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->nullable();
        $table->string('phone')->nullable();
        $table->string('company')->nullable();
        $table->text('address')->nullable();
        $table->timestamps();
    });
}


deals table (belongs to customer)
 bash

// database/migrations/xxxx_create_deals_table.php
public function up()
{
    Schema::create('deals', function (Blueprint $table) {
        $table->id();
        $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
        $table->string('title');
        $table->decimal('amount', 10, 2)->default(0);
        $table->enum('stage', ['new', 'contacted', 'proposal', 'won', 'lost'])->default('new');
        $table->timestamps();
    });
}


notes table (customer activity/time log)
 bash

// database/migrations/xxxx_create_notes_table.php
public function up()
{
    Schema::create('notes', function (Blueprint $table) {
        $table->id();
        $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
        $table->text('note');
        $table->timestamps();
    });
}


Run:
 bash

php artisan migrate


5) Model Relationships

Customer

 bash

// app/Models/Customer.php
class Customer extends Model
{
    use HasFactory;

    protected $fillable = ['name','email','phone','company','address'];

    public function deals()
    {
        return $this->hasMany(Deal::class);
    }

    public function notes()
    {
        return $this->hasMany(Note::class);
    }
}


Deal
 bash

// app/Models/Deal.php
class Deal extends Model
{
    use HasFactory;

    protected $fillable = ['customer_id','title','amount','stage'];

    public function customer()
    {
        return $this->belongsTo(Customer::class);
    }
}


Note
 bash

// app/Models/Note.php
class Note extends Model
{
    use HasFactory;

    protected $fillable = ['customer_id','note'];

    public function customer()
    {
        return $this->belongsTo(Customer::class);
    }
}


6) CustomerController (CRUD + Search)
 bash

// app/Http/Controllers/CustomerController.php
class CustomerController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index(Request $request)
    {
        $q = $request->query('q');

        $customers = Customer::when($q, function ($query) use($q) {
                $query->where('name', 'like', "%$q%")
                      ->orWhere('email', 'like', "%$q%")
                      ->orWhere('company', 'like', "%$q%");
            })
            ->orderBy('id','desc')
            ->paginate(10);

        return view('customers.index', compact('customers','q'));
    }

    public function create()
    {
        return view('customers.create');
    }

    public function store(Request $request)
    {
        $data = $request->validate([
            'name'=>'required',
            'email'=>'nullable|email',
            'phone'=>'nullable|string',
            'company'=>'nullable|string',
            'address'=>'nullable|string'
        ]);

        Customer::create($data);

        return redirect()->route('customers.index')->with('success','Customer added.');
    }

    public function show(Customer $customer)
    {
        return view('customers.show', compact('customer'));
    }

    public function edit(Customer $customer)
    {
        return view('customers.edit', compact('customer'));
    }

    public function update(Request $request, Customer $customer)
    {
        $data = $request->validate([
            'name'=>'required',
            'email'=>'nullable|email',
            'phone'=>'nullable|string',
            'company'=>'nullable|string',
            'address'=>'nullable|string'
        ]);

        $customer->update($data);

        return redirect()->route('customers.index')->with('success','Updated.');
    }

    public function destroy(Customer $customer)
    {
        $customer->delete();

        return redirect()->route('customers.index')->with('success','Deleted.');
    }
}


7) DealController (Deals per Customer)
 bash

class DealController extends Controller
{
    public function store(Request $request, Customer $customer)
    {
        $data = $request->validate([
            'title'=>'required|string',
            'amount'=>'required|numeric',
            'stage'=>'required|in:new,contacted,proposal,won,lost',
        ]);

        $customer->deals()->create($data);

        return back()->with('success','Deal added.');
    }

    public function update(Request $request, Deal $deal)
    {
        $data = $request->validate([
            'title'=>'required|string',
            'amount'=>'required|numeric',
            'stage'=>'required|in:new,contacted,proposal,won,lost',
        ]);

        $deal->update($data);

        return back()->with('success','Deal updated.');
    }

    public function destroy(Deal $deal)
    {
        $deal->delete();

        return back()->with('success','Deal deleted.');
    }
}


8) NoteController (Activity Log)
 bash

class NoteController extends Controller
{
    public function store(Request $request, Customer $customer)
    {
        $data = $request->validate(['note'=>'required']);

        $customer->notes()->create($data);

        return back()->with('success','Note added.');
    }

    public function destroy(Note $note)
    {
        $note->delete();

        return back()->with('success','Note removed.');
    }
}


9) Routes
 bash

use App\Http\Controllers\CustomerController;
use App\Http\Controllers\DealController;
use App\Http\Controllers\NoteController;

Route::middleware(['auth'])->group(function () {

    Route::resource('customers', CustomerController::class);

    Route::post('/customers/{customer}/deals', [DealController::class,'store'])->name('deals.store');
    Route::patch('/deals/{deal}', [DealController::class,'update'])->name('deals.update');
    Route::delete('/deals/{deal}', [DealController::class,'destroy'])->name('deals.delete');

    Route::post('/customers/{customer}/notes', [NoteController::class,'store'])->name('notes.store');
    Route::delete('/notes/{note}', [NoteController::class,'destroy'])->name('notes.delete');
});

require __DIR__.'/auth.php';


10) Blade Views (Summary)
 bash

resources/views/customers/
    index.blade.php   (list + search)
    create.blade.php
    edit.blade.php
    show.blade.php    (customer details + deals + notes)


11) Dashboard (Optional) Create a simple dashboard route:
 bash

Route::get('/dashboard', function () {
    return [
        'customers' => \App\Models\Customer::count(),
        'active_deals' => \App\Models\Deal::where('stage','!=','won')->count(),
        'pipeline_value' => \App\Models\Deal::where('stage','!=','lost')->sum('amount')
    ];
})->middleware('auth');


12) Run the CRM
 bash

php artisan serve


resources/views/customers/index.blade.php

 bash

<?php

@extends('layouts.app')

@section('content')
<div class="max-w-6xl mx-auto p-4">
    <div class="flex items-center justify-between mb-6">
        <h2 class="text-2xl font-semibold">Customers</h2>

        <div class="flex items-center space-x-3">
            <form action="{{ route('customers.index') }}" method="GET" class="flex items-center">
                <input
                    name="q"
                    value="{{ request('q') }}"
                    placeholder="Search name, email, company..."
                    class="border rounded px-3 py-2 w-64"
                />
                <button class="ml-2 px-3 py-2 bg-blue-600 text-white rounded">Search</button>
            </form>

            <a href="{{ route('customers.create') }}"
               class="px-4 py-2 bg-green-600 text-white rounded shadow-sm hover:bg-green-700">
               Add Customer
            </a>
        </div>
    </div>

    @if(session('success'))
        <div class="mb-4 p-3 bg-green-50 border border-green-200 text-green-800 rounded">
            {{ session('success') }}
        </div>
    @endif

    <div class="bg-white shadow-sm rounded overflow-hidden">
        <table class="w-full table-auto">
            <thead class="bg-gray-100">
                <tr>
                    <th class="text-left px-4 py-3">#</th>
                    <th class="text-left px-4 py-3">Name</th>
                    <th class="text-left px-4 py-3">Email</th>
                    <th class="text-left px-4 py-3">Phone</th>
                    <th class="text-left px-4 py-3">Company</th>
                    <th class="text-left px-4 py-3">Actions</th>
                </tr>
            </thead>
            <tbody>
                @forelse($customers as $customer)
                    <tr class="border-t hover:bg-gray-50">
                        <td class="px-4 py-3">{{ $customer->id }}</td>
                        <td class="px-4 py-3">{{ $customer->name }}</td>
                        <td class="px-4 py-3">{{ $customer->email ?? '—' }}</td>
                        <td class="px-4 py-3">{{ $customer->phone ?? '—' }}</td>
                        <td class="px-4 py-3">{{ $customer->company ?? '—' }}</td>
                        <td class="px-4 py-3">
                            <a href="{{ route('customers.show', $customer) }}" class="text-blue-600 mr-3">View</a>
                            <a href="{{ route('customers.edit', $customer) }}" class="text-yellow-600 mr-3">Edit</a>
                            <form action="{{ route('customers.destroy', $customer) }}" method="POST" class="inline"
                                  onsubmit="return confirm('Delete this customer?')">
                                @csrf
                                @method('DELETE')
                                <button class="text-red-600">Delete</button>
                            </form>
                        </td>
                    </tr>
                @empty
                    <tr>
                        <td colspan="6" class="px-4 py-6 text-center text-gray-500">No customers found.</td>
                    </tr>
                @endforelse
            </tbody>
        </table>
    </div>

    <div class="mt-4">
        {{ $customers->links() }}
    </div>
</div>
@endsection
resources/views/customers/create.blade.php
 bash

@extends('layouts.app')

@section('content')
<div class="max-w-2xl mx-auto p-4">
    <div class="bg-white shadow rounded p-6">
        <h2 class="text-xl font-semibold mb-4">Add Customer</h2>

        <form action="{{ route('customers.store') }}" method="POST">
            @csrf

            <div class="grid grid-cols-1 gap-4">
                <div>
                    <label class="block text-sm font-medium mb-1">Name <span class="text-red-500">*</span></label>
                    <input type="text" name="name" value="{{ old('name') }}"
                           class="w-full border rounded px-3 py-2" required>
                    @error('name') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Email</label>
                    <input type="email" name="email" value="{{ old('email') }}"
                           class="w-full border rounded px-3 py-2">
                    @error('email') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Phone</label>
                    <input type="text" name="phone" value="{{ old('phone') }}"
                           class="w-full border rounded px-3 py-2">
                    @error('phone') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Company</label>
                    <input type="text" name="company" value="{{ old('company') }}"
                           class="w-full border rounded px-3 py-2">
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Address</label>
                    <textarea name="address" rows="3" class="w-full border rounded px-3 py-2">{{ old('address') }}</textarea>
                </div>

                <div class="flex items-center space-x-3 mt-2">
                    <button class="px-4 py-2 bg-green-600 text-white rounded">Save Customer</button>
                    <a href="{{ route('customers.index') }}" class="px-4 py-2 border rounded">Cancel</a>
                </div>
            </div>
        </form>
    </div>
</div>
@endsection
resources/views/customers/edit.blade.php
 bash

@extends('layouts.app')

@section('content')
<div class="max-w-2xl mx-auto p-4">
    <div class="bg-white shadow rounded p-6">
        <h2 class="text-xl font-semibold mb-4">Edit Customer</h2>

        <form action="{{ route('customers.update', $customer) }}" method="POST">
            @csrf
            @method('PUT')

            <div class="grid grid-cols-1 gap-4">
                <div>
                    <label class="block text-sm font-medium mb-1">Name <span class="text-red-500">*</span></label>
                    <input type="text" name="name" value="{{ old('name', $customer->name) }}"
                           class="w-full border rounded px-3 py-2" required>
                    @error('name') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Email</label>
                    <input type="email" name="email" value="{{ old('email', $customer->email) }}"
                           class="w-full border rounded px-3 py-2">
                    @error('email') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Phone</label>
                    <input type="text" name="phone" value="{{ old('phone', $customer->phone) }}"
                           class="w-full border rounded px-3 py-2">
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Company</label>
                    <input type="text" name="company" value="{{ old('company', $customer->company) }}"
                           class="w-full border rounded px-3 py-2">
                </div>

                <div>
                    <label class="block text-sm font-medium mb-1">Address</label>
                    <textarea name="address" rows="3" class="w-full border rounded px-3 py-2">{{ old('address', $customer->address) }}</textarea>
                </div>

                <div class="flex items-center space-x-3 mt-2">
                    <button class="px-4 py-2 bg-blue-600 text-white rounded">Update Customer</button>
                    <a href="{{ route('customers.index') }}" class="px-4 py-2 border rounded">Cancel</a>
                </div>
            </div>
        </form>
    </div>
</div>
@endsection
resources/views/customers/show.blade.php
 bash

@extends('layouts.app')

@section('content')
<div class="max-w-5xl mx-auto p-4 space-y-6">

    {{-- Header --}}
    <div class="flex items-start justify-between">
        <div>
            <h2 class="text-2xl font-semibold">{{ $customer->name }}</h2>
            <p class="text-sm text-gray-600">{{ $customer->company ?? '' }}</p>
            <p class="text-sm text-gray-500 mt-1">
                {{ $customer->email ?? 'No email' }} • {{ $customer->phone ?? 'No phone' }}
            </p>
        </div>

        <div class="flex items-center space-x-3">
            <a href="{{ route('customers.edit', $customer) }}" class="px-4 py-2 border rounded">Edit</a>

            <form action="{{ route('customers.destroy', $customer) }}" method="POST" onsubmit="return confirm('Delete this customer?')">
                @csrf @method('DELETE')
                <button class="px-4 py-2 bg-red-600 text-white rounded">Delete</button>
            </form>
        </div>
    </div>

    {{-- Main grid: Details | Deals --}}
    <div class="grid grid-cols-1 md:grid-cols-3 gap-6">

        {{-- Left: Customer details --}}
        <div class="md:col-span-1 bg-white p-4 rounded shadow">
            <h3 class="font-medium mb-2">Details</h3>
            <div class="text-sm text-gray-700 space-y-2">
                <p><strong>Company:</strong> {{ $customer->company ?? '—' }}</p>
                <p><strong>Email:</strong> {{ $customer->email ?? '—' }}</p>
                <p><strong>Phone:</strong> {{ $customer->phone ?? '—' }}</p>
                <p><strong>Address:</strong> <br><span class="whitespace-pre-line">{{ $customer->address ?? '—' }}</span></p>
                <p class="text-xs text-gray-400 mt-3">Created: {{ $customer->created_at->format('M d, Y') }}</p>
            </div>
        </div>

        {{-- Right: Deals & Notes --}}
        <div class="md:col-span-2 space-y-6">

            {{-- Deals card --}}
            <div class="bg-white rounded shadow p-4">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="font-medium">Deals</h3>

                    <button onclick="document.getElementById('deal-form').classList.toggle('hidden')" class="text-sm px-3 py-1 border rounded">New Deal</button>
                </div>

                <div id="deal-form" class="hidden mb-3">
                    <form action="{{ route('deals.store', $customer) }}" method="POST" class="grid grid-cols-1 md:grid-cols-3 gap-2 items-end">
                        @csrf
                        <div>
                            <label class="text-sm">Title</label>
                            <input name="title" required class="w-full border rounded px-2 py-1" />
                        </div>
                        <div>
                            <label class="text-sm">Amount</label>
                            <input name="amount" type="number" step="0.01" required class="w-full border rounded px-2 py-1" />
                        </div>
                        <div>
                            <label class="text-sm">Stage</label>
                            <select name="stage" class="w-full border rounded px-2 py-1">
                                <option value="new">New</option>
                                <option value="contacted">Contacted</option>
                                <option value="proposal">Proposal</option>
                                <option value="won">Won</option>
                                <option value="lost">Lost</option>
                            </select>
                        </div>
                        <div class="md:col-span-3 mt-2">
                            <button class="px-4 py-2 bg-green-600 text-white rounded">Save Deal</button>
                        </div>
                    </form>
                </div>

                {{-- Deals list --}}
                <div class="space-y-2">
                    @forelse($customer->deals()->latest()->get() as $deal)
                        <div class="flex items-center justify-between border rounded p-3">
                            <div>
                                <div class="font-medium">{{ $deal->title }}</div>
                                <div class="text-sm text-gray-500">Amount: ${{ number_format($deal->amount,2) }} • Stage: <span class="capitalize">{{ $deal->stage }}</span></div>
                            </div>

                            <div class="flex items-center space-x-2">
                                <form action="{{ route('deals.update', $deal) }}" method="POST" class="flex items-center space-x-2">
                                    @csrf
                                    @method('PATCH')
                                    <input type="hidden" name="title" value="{{ $deal->title }}">
                                    <input type="hidden" name="amount" value="{{ $deal->amount }}">
                                    <select name="stage" class="border rounded px-2 py-1 text-sm">
                                        <option value="new" {{ $deal->stage == 'new' ? 'selected' : '' }}>New</option>
                                        <option value="contacted" {{ $deal->stage == 'contacted' ? 'selected' : '' }}>Contacted</option>
                                        <option value="proposal" {{ $deal->stage == 'proposal' ? 'selected' : '' }}>Proposal</option>
                                        <option value="won" {{ $deal->stage == 'won' ? 'selected' : '' }}>Won</option>
                                        <option value="lost" {{ $deal->stage == 'lost' ? 'selected' : '' }}>Lost</option>
                                    </select>
                                    <button class="px-3 py-1 bg-blue-600 text-white rounded text-sm">Update</button>
                                </form>

                                <form action="{{ route('deals.delete', $deal) }}" method="POST" onsubmit="return confirm('Delete deal?')">
                                    @csrf @method('DELETE')
                                    <button class="px-3 py-1 bg-red-600 text-white rounded text-sm">Delete</button>
                                </form>
                            </div>
                        </div>
                    @empty
                        <div class="text-gray-500">No deals yet.</div>
                    @endforelse
                </div>
            </div>

            {{-- Notes / Activity log --}}
            <div class="bg-white rounded shadow p-4">
                <div class="flex items-center justify-between mb-3">
                    <h3 class="font-medium">Notes / Activity</h3>
                    <button onclick="document.getElementById('note-form').classList.toggle('hidden')" class="text-sm px-3 py-1 border rounded">Add Note</button>
                </div>

                <div id="note-form" class="hidden mb-3">
                    <form action="{{ route('notes.store', $customer) }}" method="POST">
                        @csrf
                        <textarea name="note" rows="3" class="w-full border rounded px-2 py-1" placeholder="Write a note..." required></textarea>
                        <div class="mt-2">
                            <button class="px-4 py-2 bg-green-600 text-white rounded">Save Note</button>
                        </div>
                    </form>
                </div>

                <div class="space-y-3">
                    @forelse($customer->notes()->latest()->get() as $note)
                        <div class="border rounded p-3">
                            <div class="text-sm text-gray-700 whitespace-pre-line">{{ $note->note }}</div>
                            <div class="flex items-center justify-between mt-2">
                                <div class="text-xs text-gray-400">— {{ $note->created_at->diffForHumans() }}</div>
                                <form action="{{ route('notes.delete', $note) }}" method="POST" onsubmit="return confirm('Remove note?')">
                                    @csrf @method('DELETE')
                                    <button class="text-red-600 text-sm">Delete</button>
                                </form>
                            </div>
                        </div>
                    @empty
                        <div class="text-gray-500">No activity yet.</div>
                    @endforelse
                </div>
            </div>

        </div>
    </div>
</div>
@endsection

Leave a Reply

Your email address will not be published. Required fields are marked *