diff --git a/src/admin/clients.py b/src/admin/clients.py
index 375242a..85aa51b 100644
--- a/src/admin/clients.py
+++ b/src/admin/clients.py
@@ -1,11 +1,13 @@
from django.contrib import admin
from django.forms import ModelForm
-from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import SplitPhoneNumberField
-# Assuming these imports are correct based on your setup
-from src.admin.base import admin_site
+from src.admin.shared import (
+ admin_site,
+ format_placeholder,
+ format_strong_with_subtext,
+)
from src.models.clients import Client
@@ -64,11 +66,7 @@ class ClientAdmin(admin.ModelAdmin):
def name_display(self, obj):
"""Display name with subtle emphasis."""
- return format_html(
- "{}
{}",
- obj.name,
- obj.short_code or "—",
- )
+ return format_strong_with_subtext(obj.name, obj.short_code)
name_display.short_description = "Client"
@@ -77,7 +75,7 @@ class ClientAdmin(admin.ModelAdmin):
if obj.phone:
# Uses the .as_international property from the PhoneNumber wrapper
return obj.phone.as_international
- return "—"
+ return format_placeholder()
formatted_phone.short_description = "Phone"
diff --git a/src/admin/finance.py b/src/admin/finance.py
index bea461a..e5e8b1e 100644
--- a/src/admin/finance.py
+++ b/src/admin/finance.py
@@ -1,8 +1,12 @@
from django.contrib import admin
-from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
-from src.admin.base import admin_site
+from src.admin.shared import (
+ admin_site,
+ format_badge,
+ format_currency,
+ format_strong_with_subtext,
+)
from src.models.finance import Expense, Invoice, Payment
@@ -76,11 +80,7 @@ class InvoiceAdmin(admin.ModelAdmin):
)
def client_display(self, obj):
- return format_html(
- "{}
{}",
- obj.client.name,
- obj.client.short_code,
- )
+ return format_strong_with_subtext(obj.client.name, obj.client.short_code)
client_display.short_description = "Client"
@@ -90,7 +90,7 @@ class InvoiceAdmin(admin.ModelAdmin):
project_display.short_description = "Project"
def amount_display(self, obj):
- return format_html("${:,.2f}", obj.amount)
+ return format_currency(obj.amount)
amount_display.short_description = "Amount"
@@ -103,11 +103,7 @@ class InvoiceAdmin(admin.ModelAdmin):
"CANCELLED": "#6c757d",
}
color = color_map.get(obj.status, "#333")
- return format_html(
- "{}",
- color,
- obj.get_status_display(),
- )
+ return format_badge(obj.get_status_display(), background=color)
status_badge.short_description = "Status"
@@ -154,7 +150,7 @@ class ExpenseAdmin(admin.ModelAdmin):
readonly_fields = ("created_at",)
def amount_display(self, obj):
- return format_html("${:,.2f}", obj.amount)
+ return format_currency(obj.amount)
amount_display.short_description = "Amount"
@@ -182,15 +178,13 @@ class PaymentAdmin(admin.ModelAdmin):
)
def invoice_display(self, obj):
- return format_html(
- "{}
{}",
- obj.invoice.invoice_number,
- obj.invoice.client.name,
+ return format_strong_with_subtext(
+ obj.invoice.invoice_number, obj.invoice.client.name
)
invoice_display.short_description = "Invoice"
def amount_display(self, obj):
- return format_html("${:,.2f}", obj.amount)
+ return format_currency(obj.amount)
amount_display.short_description = "Amount"
diff --git a/src/admin/productivity.py b/src/admin/productivity.py
index caef48c..d33c65e 100644
--- a/src/admin/productivity.py
+++ b/src/admin/productivity.py
@@ -1,9 +1,13 @@
from django.contrib import admin
-from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
-from src.admin.base import admin_site
+from src.admin.shared import (
+ admin_site,
+ format_badge,
+ format_placeholder,
+ format_strong,
+)
from src.models.productivity import Note, TimeEntry
@@ -34,7 +38,7 @@ class NoteAdmin(admin.ModelAdmin):
def title_display(self, obj):
"""Show truncated title for clarity."""
- return format_html("{}", obj.title[:60])
+ return format_strong(obj.title[:60])
title_display.short_description = "Title"
@@ -77,17 +81,17 @@ class TimeEntryAdmin(admin.ModelAdmin):
readonly_fields = ("duration",)
def description_display(self, obj):
- return format_html("{}", obj.description[:50])
+ return format_strong(obj.description[:50])
description_display.short_description = "Description"
def project_display(self, obj):
- return obj.project.name if obj.project else "—"
+ return obj.project.name if obj.project else format_placeholder()
project_display.short_description = "Project"
def task_display(self, obj):
- return obj.task.name if obj.task else "—"
+ return obj.task.name if obj.task else format_placeholder()
task_display.short_description = "Task"
@@ -95,7 +99,7 @@ class TimeEntryAdmin(admin.ModelAdmin):
return (
localtime(obj.start_time).strftime("%Y-%m-%d %H:%M")
if obj.start_time
- else "—"
+ else format_placeholder()
)
start_time_display.short_description = "Start"
@@ -103,7 +107,7 @@ class TimeEntryAdmin(admin.ModelAdmin):
def end_time_display(self, obj):
if obj.end_time:
return localtime(obj.end_time).strftime("%Y-%m-%d %H:%M")
- return format_html("In Progress")
+ return format_badge("In Progress", background="#999")
end_time_display.short_description = "End"
@@ -111,12 +115,8 @@ class TimeEntryAdmin(admin.ModelAdmin):
if obj.duration:
hours = obj.duration.total_seconds() / 3600
color = "#28a745" if hours < 8 else "#f39c12"
- return format_html(
- "{:.2f} hrs",
- color,
- hours,
- )
- return format_html("—")
+ return format_badge(f"{hours:.2f} hrs", background=color)
+ return format_placeholder()
duration_display.short_description = "Duration"
diff --git a/src/admin/projects.py b/src/admin/projects.py
index 5383ade..30989e6 100644
--- a/src/admin/projects.py
+++ b/src/admin/projects.py
@@ -1,9 +1,15 @@
from django.contrib import admin
from django.utils import timezone
-from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
-from src.admin.base import admin_site
+from src.admin.shared import (
+ admin_site,
+ format_badge,
+ format_currency,
+ format_placeholder,
+ format_strong,
+ format_strong_with_subtext,
+)
from src.models.projects import Milestone, Project, Task
@@ -73,7 +79,7 @@ class ProjectAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at")
def name_display(self, obj):
- return format_html("{}", obj.name)
+ return format_strong(obj.name)
name_display.short_description = "Project"
@@ -91,16 +97,12 @@ class ProjectAdmin(admin.ModelAdmin):
"CANCELLED": "#dc3545",
}
color = color_map.get(obj.status, "#999")
- return format_html(
- "{}",
- color,
- obj.get_status_display(),
- )
+ return format_badge(obj.get_status_display(), background=color)
status_badge.short_description = "Status"
def budget_display(self, obj):
- return format_html("${:,.2f}", obj.budget) if obj.budget else "—"
+ return format_currency(obj.budget) if obj.budget else format_placeholder()
budget_display.short_description = "Budget"
@@ -131,7 +133,7 @@ class MilestoneAdmin(admin.ModelAdmin):
)
def title_display(self, obj):
- return format_html("{}", obj.title)
+ return format_strong(obj.title)
title_display.short_description = "Title"
@@ -143,11 +145,7 @@ class MilestoneAdmin(admin.ModelAdmin):
def status_badge(self, obj):
color = "#28a745" if obj.is_completed else "#dc3545"
text = "Completed" if obj.is_completed else "Pending"
- return format_html(
- "{}",
- color,
- text,
- )
+ return format_badge(text, background=color)
status_badge.short_description = "Status"
@@ -189,7 +187,7 @@ class TaskAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at")
def title_display(self, obj):
- return format_html("{}", obj.title)
+ return format_strong(obj.title)
title_display.short_description = "Title"
@@ -200,11 +198,11 @@ class TaskAdmin(admin.ModelAdmin):
def due_date_display(self, obj):
if not obj.due_date:
- return format_html("No due date")
- elif obj.due_date < timezone.localdate():
- return format_html("{}", obj.due_date)
- elif obj.due_date == timezone.localdate():
- return format_html("Today")
+ return format_placeholder(None, "No due date", "#999")
+ if obj.due_date < timezone.localdate():
+ return format_badge(obj.due_date, background="#dc3545")
+ if obj.due_date == timezone.localdate():
+ return format_badge("Today", background="#f39c12")
return obj.due_date
due_date_display.short_description = "Due Date"
@@ -217,11 +215,7 @@ class TaskAdmin(admin.ModelAdmin):
"DONE": "#28a745",
}
color = color_map.get(obj.status, "#333")
- return format_html(
- "{}",
- color,
- obj.get_status_display(),
- )
+ return format_badge(obj.get_status_display(), background=color)
status_badge.short_description = "Status"
diff --git a/src/admin/services.py b/src/admin/services.py
index 5a53934..87c04e9 100644
--- a/src/admin/services.py
+++ b/src/admin/services.py
@@ -1,9 +1,17 @@
from django.contrib import admin
from django.utils import timezone
-from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
-from src.admin.base import admin_site
+from src.admin.shared import (
+ admin_site,
+ format_badge,
+ format_boolean_icon,
+ format_currency,
+ format_link,
+ format_placeholder,
+ format_strong,
+ format_strong_with_subtext,
+)
from src.models.services import Credential, Service
@@ -75,7 +83,7 @@ class ServiceAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at")
def name_display(self, obj):
- return format_html("{}", obj.name)
+ return format_strong(obj.name)
name_display.short_description = "Service"
@@ -93,28 +101,24 @@ class ServiceAdmin(admin.ModelAdmin):
"OTHER": "#6c757d",
}
color = color_map.get(obj.service_type, "#333")
- return format_html(
- "{}",
- color,
- obj.get_service_type_display(),
- )
+ return format_badge(obj.get_service_type_display(), background=color)
type_badge.short_description = "Type"
def cost_display(self, obj):
- return format_html("${:,.2f}", obj.cost)
+ return format_currency(obj.cost)
cost_display.short_description = "Cost"
def renewal_price_display(self, obj):
- return format_html("${:,.2f}", obj.renewal_price)
+ return format_currency(obj.renewal_price)
renewal_price_display.short_description = "Renewal"
def expiry_display(self, obj):
"""Color-coded expiry indicator."""
if not obj.expiry_date:
- return format_html("—")
+ return format_placeholder()
today = timezone.localdate()
delta = (obj.expiry_date - today).days
@@ -138,11 +142,7 @@ class ServiceAdmin(admin.ModelAdmin):
expiry_display.short_description = "Expiry"
def auto_renew_display(self, obj):
- icon = "✅" if obj.auto_renew else "❌"
- color = "#28a745" if obj.auto_renew else "#999"
- return format_html(
- f"{icon}"
- )
+ return format_boolean_icon(obj.auto_renew)
auto_renew_display.short_description = "Auto Renew"
@@ -183,23 +183,17 @@ class CredentialAdmin(admin.ModelAdmin):
# --- DISPLAY HELPERS ---
def service_display(self, obj):
- return format_html(
- "{}
{}",
- obj.service.name,
- obj.service.client.name,
- )
+ return format_strong_with_subtext(obj.service.name, obj.service.client.name)
service_display.short_description = "Service"
def username_display(self, obj):
- return obj.username or format_html("—")
+ return obj.username or format_placeholder()
username_display.short_description = "Username"
def url_display(self, obj):
- if obj.url:
- return format_html("{}", obj.url, obj.url)
- return format_html("—")
+ return format_link(obj.url)
url_display.short_description = "URL"
diff --git a/src/admin/shared.py b/src/admin/shared.py
new file mode 100644
index 0000000..ac69b24
--- /dev/null
+++ b/src/admin/shared.py
@@ -0,0 +1,55 @@
+from django.utils.html import format_html
+from src.admin.base import * #noqa: F401
+
+def format_strong(text):
+ return format_html("{}", text)
+
+
+def format_strong_with_subtext(primary, secondary, secondary_color="#888"):
+ return format_html(
+ "{}
{}",
+ primary,
+ secondary_color,
+ secondary if secondary else "—",
+ )
+
+
+def format_placeholder(value=None, placeholder="—", color="#888"):
+ return (
+ value
+ if value
+ else format_html("{}", color, placeholder)
+ )
+
+
+def format_badge(label, background="#333", text_color="white"):
+ return format_html(
+ "{}",
+ background,
+ text_color,
+ label,
+ )
+
+
+def format_currency(amount):
+ return format_html("${:,.2f}", amount) if amount is not None else "—"
+
+
+def format_boolean_icon(
+ flag, true_icon="✅", false_icon="❌", true_color="#28a745", false_color="#999"
+):
+ color = true_color if flag else false_color
+ icon = true_icon if flag else false_icon
+ return format_html(
+ "{}",
+ color,
+ icon,
+ )
+
+
+def format_link(url, label=None):
+ return (
+ format_html("{}", url, label or url)
+ if url
+ else format_placeholder()
+ )
\ No newline at end of file