Back to Use Cases

CRM Lead-to-Close Workflow

From website form to closed deal — automated lead management with zero leads falling through the cracks.

Blazor C# .NET SQL Server Email API

The Problem

A growing services company was losing 40% of leads due to slow follow-ups and no tracking system. Sales reps relied on spreadsheets and memory.

  • Leads from website, email, and referrals scattered across channels
  • No visibility into pipeline status
  • Follow-ups forgotten — leads go cold in 48 hours
  • No data to forecast revenue or measure conversion rates

The Solution

A custom CRM built with Blazor Server and SQL Server. Automated lead capture, assignment, follow-up reminders, and pipeline analytics.

Architecture

Web Form

Contact page / Landing page

API Endpoint

ASP.NET Core API

Auto-Assign

Round-robin / by service

Pipeline View

Blazor Dashboard

Step-by-Step Execution Flow

Step 1: Lead Capture (Contact Form → API)

The website contact form posts to an ASP.NET Core API endpoint that creates a new lead record.

// LeadController.cs
[ApiController]
[Route("api/[controller]")]
public class LeadController : ControllerBase
{
    private readonly AppDbContext _db;
    private readonly ILeadAssigner _assigner;
    private readonly IEmailService _email;

    [HttpPost]
    public async Task<IActionResult> CreateLead([FromBody] LeadRequest request)
    {
        // Validate input
        if (string.IsNullOrEmpty(request.Email) || string.IsNullOrEmpty(request.Name))
            return BadRequest("Name and email are required");

        // Create lead
        var lead = new Lead
        {
            Name = request.Name,
            Email = request.Email,
            Phone = request.Phone,
            Service = request.Service,
            Message = request.Message,
            Stage = LeadStage.New,
            CreatedAt = DateTime.UtcNow,
            Source = request.Source ?? "website"
        };

        // Auto-assign to sales rep (round-robin)
        lead.AssignedTo = await _assigner.GetNextRepAsync(request.Service);

        _db.Leads.Add(lead);
        await _db.SaveChangesAsync();

        // Send notifications
        await _email.SendLeadNotification(lead);

        return Ok(new { id = lead.Id, assignedTo = lead.AssignedTo });
    }
}

Step 2: Lead Data Model

// Models/Lead.cs
public class Lead
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Email { get; set; } = "";
    public string Phone { get; set; } = "";
    public string Service { get; set; } = "";
    public string Message { get; set; } = "";
    public LeadStage Stage { get; set; } = LeadStage.New;
    public string AssignedTo { get; set; } = "";
    public string Source { get; set; } = "website";
    public DateTime CreatedAt { get; set; }
    public DateTime? LastContactedAt { get; set; }
    public DateTime? NextFollowUp { get; set; }
    public decimal? DealValue { get; set; }
    public string Notes { get; set; } = "";
}

public enum LeadStage
{
    New,
    Contacted,
    Qualified,
    Proposal,
    Negotiation,
    Won,
    Lost
}

Step 3: Auto-Assignment (Round-Robin)

// Services/LeadAssigner.cs
public class LeadAssigner : ILeadAssigner
{
    private readonly AppDbContext _db;

    public async Task<string> GetNextRepAsync(string service)
    {
        // Get reps for this service category
        var reps = await _db.SalesReps
            .Where(r => r.Services.Contains(service) && r.IsActive)
            .OrderBy(r => r.LastAssignedAt)
            .ToListAsync();

        if (!reps.Any())
            return "unassigned";

        // Round-robin: pick the rep who was assigned least recently
        var nextRep = reps.First();
        nextRep.LastAssignedAt = DateTime.UtcNow;
        await _db.SaveChangesAsync();

        return nextRep.Name;
    }
}

Step 4: Automated Follow-Up Reminders

// Services/FollowUpService.cs (runs as a background service)
public class FollowUpService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var email = scope.ServiceProvider.GetRequiredService<IEmailService>();

            // Find leads not contacted in 48 hours
            var staleLeads = await db.Leads
                .Where(l => l.Stage == LeadStage.New
                    && l.CreatedAt < DateTime.UtcNow.AddHours(-48)
                    && l.LastContactedAt == null)
                .ToListAsync();

            foreach (var lead in staleLeads)
            {
                await email.SendFollowUpReminder(lead.AssignedTo, lead);
            }

            // Check every hour
            await Task.Delay(TimeSpan.FromHours(1), token);
        }
    }
}

Step 5: Pipeline Dashboard (Blazor Component)

<!-- Pages/Pipeline.razor -->
@page "/pipeline"
@inject AppDbContext Db

<h3>Sales Pipeline</h3>

<div class="pipeline-board">
    @foreach (var stage in Enum.GetValues<LeadStage>())
    {
        <div class="pipeline-column">
            <h4>@stage (@leads.Count(l => l.Stage == stage))</h4>
            @foreach (var lead in leads.Where(l => l.Stage == stage))
            {
                <div class="lead-card">
                    <strong>@lead.Name</strong>
                    <span>@lead.Service</span>
                    <span>@lead.DealValue?.ToString("C")</span>
                </div>
            }
        </div>
    }
</div>

@code {
    private List<Lead> leads = new();

    protected override async Task OnInitializedAsync()
    {
        leads = await Db.Leads
            .OrderByDescending(l => l.CreatedAt)
            .ToListAsync();
    }
}

Business Impact

5 min response time

Down from 24+ hours

35% higher conversion

Zero leads lost

Full pipeline visibility

Real-time dashboard

Revenue forecasting

Data-driven decisions

Tech Stack

  • Frontend: Blazor Server (.NET 8)
  • Backend: ASP.NET Core Web API
  • Database: SQL Server (Entity Framework Core)
  • Email: MailKit / SendGrid API
  • Background Jobs: .NET BackgroundService
  • Hosting: Azure App Service

Need a Custom CRM?

We build CRM systems tailored to your sales process. Or use our ready-made Hospital/EduNest products.

Book Free Consultation