The DRY Approach to CRM Status Displays: Building a Universal Badge Component


Refactored badge statuses

Are you wrestling with a CRM that's bursting at the seams with multiple models, each flaunting its own set of statuses? If you've ever found yourself drowning in a sea of repetitive code updates just to change a simple status display, you're in the right place. This post is your lifeline to a more efficient, maintainable solution.

Imagine a world where you can update status displays across your entire application by tweaking just one piece of code. Sounds too good to be true? Well, buckle up, because we're about to make it a reality.

In this guide, we'll dive into:
  1. The power of Enums and dynamic types
  2. Creating a sleek, color-coded badge system for different statuses
  3. Implementing a centralized approach to status management

But before we leap into the solution, let's take a step back. I'll walk you through my original approach - warts and all. We'll dissect why it falls short, setting the stage for our journey towards code nirvana.

By the end of this post, you'll be armed with the knowledge to transform your CRM's status management from a sprawling mess into a lean, mean, efficiency machine. Ready to say goodbye to tedious, error-prone updates and hello to streamlined, maintainable code? Let's dive in!
//TaskStatusBadge.vue

<script setup>
import { startCase } from 'lodash';
import { computed } from 'vue';

const props = defineProps({
    status: {
        type: String,
        required: true
    }
});

const color = {
    'draft': 'bg-yellow-50 text-yellow-800 ring-yellow-600/20',
    'pending': 'bg-blue-50 text-blue-800 ring-blue-600/20',
    'in progress': 'bg-indigo-50 text-indigo-800 ring-indigo-600/20',
    'completed': 'bg-green-50 text-green-800 ring-green-600/20',
    'default': 'bg-gray-50 text-gray-800 ring-gray-600/20',
};

const badgeColor = computed(() => {
    return color[props.status] ?? color['default'];
});
</script>

<template>
    <span
        class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset"
        :class="badgeColor"
    >
        {{ startCase(status) }}
    </span>
</template>
The Hidden Pitfall of Hardcoded Components
At first glance, our current component seems to work flawlessly. It displays the correct badge with the right color for each status. So, what's the catch?
The issue lies not in its functionality, but in its lack of flexibility and reusability. Let's break down the problems:
  1. Limited Scope: This component is tailored for a specific set of statuses. What happens when we need to add a new status or change an existing one? We'd have to modify the component itself.
  2. Repetitive Code: With 5 different badge components for various models, we're violating the DRY (Don't Repeat Yourself) principle. This leads to maintenance headaches and increased chances of inconsistencies.
  3. Scalability Concerns: As our application grows, creating a new component for each model's statuses or types becomes unsustainable.

The Vision: A Universal Badge Component
What if we could have a single, flexible <Badge> component that could handle any status or type across all our models? Imagine a component that:
  • Accepts a :type prop to specify the current status
  • Takes a :types prop to define all possible statuses and their corresponding styles
  • Automatically renders the correct badge based on these props
With this approach, we could:
  • Use the same component for all models, reducing code duplication
  • Easily add or modify statuses without touching the component's internal logic
  • Ensure consistency across our entire application
In the next section, we'll dive into refactoring our current implementation into this versatile <Badge> component. Get ready to transform your codebase from a collection of rigid, single-use components into a flexible, maintainable system that can handle any status or type you throw at it!

The Power of Dynamic Badges: A Deep Dive
Let's explore our newly refactored <Badge> component and the supporting code that makes it truly dynamic and reusable.

1. The Universal Badge Component
// Badge.vue

<script setup>
import { startCase } from 'lodash';
import { computed } from 'vue';

const props = defineProps({
    type: {
        type: String,
        required: true
    },
    types: {
        type: Object,
        required: true,
    }
});

const badgeColor = computed(() => {
    return props.types[props.type] || 'gray-badge';
});
</script>

<template>
    <span
        class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset"
        :class="badgeColor"
    >
        {{ startCase(type) }}
    </span>
</template>
This component is now flexible enough to handle any type of badge. It accepts two props:
  • type: The current status or type
  • types: An object mapping types to their corresponding color classes
2. Enum-Powered Type Management
// App\Enums\ClientTypes.php

<?php

namespace App\Enums;

enum ClientTypes: string
{
    case LEAD = 'lead';
    case PROSPECT = 'prospect';
    case CUSTOMER = 'customer';
    case INACTIVE = 'inactive';
    case REFERRED = 'referred';
    case VIP = 'vip';

    public static function toSelectArray(): array
    {
        return collect(self::cases())
            ->mapWithKeys(function ($case) {
                return [$case->value => ucwords(str_replace('_', ' ', $case->name))];
            })
            ->toArray();
    }

    public static function badgeColors()
    {
        return [
            self::LEAD->value => 'yellow-badge',
            self::PROSPECT->value => 'blue-badge',
            self::CUSTOMER->value => 'green-badge',
            self::INACTIVE->value => 'red-badge',
            self::REFERRED->value => 'purple-badge',
            self::VIP->value => 'indigo-badge'
        ];
    }
}
The ClientTypes enum serves two crucial purposes:
  1. It defines all possible client types, ensuring consistency across your application.
  2. The badgeColors() method maps each type to its corresponding color class.
3. Custom CSS for Badge Styling
/* Badge Colors */ 
.yellow-badge { 
    @apply bg-yellow-50 text-yellow-800 ring-yellow-600/20;
}
.blue-badge { 
    @apply bg-blue-50 text-blue-800 ring-blue-600/20;
} 
/* ... other badge colors ... */
These custom classes leverage Tailwind's @apply directive to create reusable badge styles. This approach allows us to use dynamic classes that won't be purged by Tailwind's optimization process.

Putting It All Together
To use this system:
  1. Pass ClientTypes::badgeColors() from your backend to your frontend.
  2. In your Vue component, pass this data to the <Badge> component:
<Badge :type="client.type" :types="clientTypeBadgeColors" />
Conclusion: Embracing Flexibility and Maintainability
By refactoring our badge system, we've achieved several key improvements:
  1. Centralized Management: All badge colors are defined in one place (the enum), making updates a breeze.
  2. Reusability: Our <Badge> component can now be used for any model with statuses or types.
  3. Scalability: Adding new types or changing existing ones no longer requires touching the component code.
  4. Consistency: With colors defined in the enum, we ensure consistent styling across the application.
This approach not only simplifies our current codebase but also sets us up for future growth. As your application evolves and new models or types are introduced, you can easily extend this system without accumulating technical debt.

Remember, the key to maintainable code often lies in creating flexible, reusable components and centralizing configuration. This badge system is just one example of how thinking ahead can save you countless hours of refactoring down the line.

So, the next time you find yourself creating multiple similar components, take a step back and consider: could this be solved with a more dynamic, centralized approach? Your future self (and your team) will thank you!