227 lines
6.3 KiB
Python
227 lines
6.3 KiB
Python
from django.contrib import admin
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from src.admin.shared import (
|
|
admin_site,
|
|
format_badge,
|
|
format_currency,
|
|
format_placeholder,
|
|
format_strong,
|
|
)
|
|
from src.models.projects import Milestone, Project, Task
|
|
|
|
|
|
class MilestoneInline(admin.TabularInline):
|
|
model = Milestone
|
|
extra = 1
|
|
fields = ("title", "due_date", "is_completed")
|
|
show_change_link = True
|
|
ordering = ("due_date",)
|
|
verbose_name_plural = "Milestones"
|
|
|
|
|
|
class TaskInline(admin.TabularInline):
|
|
model = Task
|
|
extra = 1
|
|
fields = ("title", "status", "due_date")
|
|
show_change_link = True
|
|
ordering = ("status", "due_date")
|
|
verbose_name_plural = "Tasks"
|
|
|
|
|
|
@admin.register(Project, site=admin_site)
|
|
class ProjectAdmin(admin.ModelAdmin):
|
|
list_display = (
|
|
"name_display",
|
|
"client_display",
|
|
"status_badge",
|
|
"start_date",
|
|
"deadline",
|
|
"budget_display",
|
|
)
|
|
list_filter = ("status", "client", "start_date", "deadline")
|
|
search_fields = ("name", "client__name", "description")
|
|
ordering = ("-created_at",)
|
|
list_per_page = 25
|
|
inlines = [MilestoneInline, TaskInline]
|
|
|
|
fieldsets = (
|
|
(
|
|
_("Project Overview"),
|
|
{
|
|
"fields": ("client", "name", "description", "status"),
|
|
},
|
|
),
|
|
(
|
|
_("Timeline"),
|
|
{
|
|
"fields": ("start_date", "deadline"),
|
|
"description": "Start and deadline dates help in tracking project progress.",
|
|
},
|
|
),
|
|
(
|
|
_("Financials"),
|
|
{
|
|
"fields": ("budget",),
|
|
},
|
|
),
|
|
(
|
|
_("Timestamps"),
|
|
{
|
|
"fields": ("created_at", "updated_at"),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
)
|
|
|
|
readonly_fields = ("created_at", "updated_at")
|
|
|
|
def name_display(self, obj):
|
|
return format_strong(obj.name)
|
|
|
|
name_display.short_description = "Project"
|
|
|
|
def client_display(self, obj):
|
|
return obj.client.name
|
|
|
|
client_display.short_description = "Client"
|
|
|
|
def status_badge(self, obj):
|
|
color_map = {
|
|
"PLANNING": "#6c757d",
|
|
"IN_PROGRESS": "#007bff",
|
|
"ON_HOLD": "#ffc107",
|
|
"COMPLETED": "#28a745",
|
|
"CANCELLED": "#dc3545",
|
|
}
|
|
color = color_map.get(obj.status, "#999")
|
|
return format_badge(obj.get_status_display(), background=color)
|
|
|
|
status_badge.short_description = "Status"
|
|
|
|
def budget_display(self, obj):
|
|
return format_currency(obj.budget) if obj.budget else format_placeholder()
|
|
|
|
budget_display.short_description = "Budget"
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
if obj.deadline and obj.start_date and obj.deadline < obj.start_date:
|
|
self.message_user(
|
|
request,
|
|
"⚠️ Deadline cannot be earlier than start date.",
|
|
level="warning",
|
|
)
|
|
super().save_model(request, obj, form, change)
|
|
|
|
|
|
@admin.register(Milestone, site=admin_site)
|
|
class MilestoneAdmin(admin.ModelAdmin):
|
|
list_display = ("title_display", "project_display", "due_date", "status_badge")
|
|
list_filter = ("is_completed", "due_date", "project")
|
|
search_fields = ("title", "project__name")
|
|
ordering = ("due_date",)
|
|
|
|
fieldsets = (
|
|
(
|
|
_("Milestone Info"),
|
|
{
|
|
"fields": ("project", "title", "due_date", "is_completed"),
|
|
},
|
|
),
|
|
)
|
|
|
|
def title_display(self, obj):
|
|
return format_strong(obj.title)
|
|
|
|
title_display.short_description = "Title"
|
|
|
|
def project_display(self, obj):
|
|
return obj.project.name
|
|
|
|
project_display.short_description = "Project"
|
|
|
|
def status_badge(self, obj):
|
|
color = "#28a745" if obj.is_completed else "#dc3545"
|
|
text = "Completed" if obj.is_completed else "Pending"
|
|
return format_badge(text, background=color)
|
|
|
|
status_badge.short_description = "Status"
|
|
|
|
|
|
@admin.register(Task, site=admin_site)
|
|
class TaskAdmin(admin.ModelAdmin):
|
|
list_display = (
|
|
"title_display",
|
|
"project_display",
|
|
"status_badge",
|
|
"due_date_display",
|
|
)
|
|
list_filter = ("status", "due_date", "project")
|
|
search_fields = ("title", "project__name", "description")
|
|
ordering = ("status", "due_date")
|
|
|
|
fieldsets = (
|
|
(
|
|
_("Task Info"),
|
|
{
|
|
"fields": ("project", "title", "description", "status"),
|
|
},
|
|
),
|
|
(
|
|
_("Schedule"),
|
|
{
|
|
"fields": ("due_date",),
|
|
"description": "Due date cannot be in the past.",
|
|
},
|
|
),
|
|
(
|
|
_("Timestamps"),
|
|
{
|
|
"fields": ("created_at", "updated_at"),
|
|
"classes": ("collapse",),
|
|
},
|
|
),
|
|
)
|
|
readonly_fields = ("created_at", "updated_at")
|
|
|
|
def title_display(self, obj):
|
|
return format_strong(obj.title)
|
|
|
|
title_display.short_description = "Title"
|
|
|
|
def project_display(self, obj):
|
|
return obj.project.name
|
|
|
|
project_display.short_description = "Project"
|
|
|
|
def due_date_display(self, obj):
|
|
if not obj.due_date:
|
|
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"
|
|
|
|
def status_badge(self, obj):
|
|
color_map = {
|
|
"TODO": "#6c757d",
|
|
"IN_PROGRESS": "#007bff",
|
|
"REVIEW": "#ffc107",
|
|
"DONE": "#28a745",
|
|
}
|
|
color = color_map.get(obj.status, "#333")
|
|
return format_badge(obj.get_status_display(), background=color)
|
|
|
|
status_badge.short_description = "Status"
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
if obj.due_date and obj.due_date < timezone.localdate():
|
|
self.message_user(
|
|
request, "⚠️ Due date cannot be in the past.", level="warning"
|
|
)
|
|
super().save_model(request, obj, form, change)
|