diff --git a/src/api/projects/forms.py b/src/api/projects/forms.py new file mode 100644 index 0000000..6b7f7cb --- /dev/null +++ b/src/api/projects/forms.py @@ -0,0 +1,27 @@ +from django import forms +from src.models.projects import Project, Task + +class ProjectForm(forms.ModelForm): + class Meta: + model = Project + fields = ["name", "client", "description", "start_date", "deadline", "budget", "status"] + widgets = { + "name": forms.TextInput(attrs={"class": "input input-bordered w-full", "placeholder": "Project Name"}), + "client": forms.Select(attrs={"class": "select select-bordered w-full"}), + "description": forms.Textarea(attrs={"class": "textarea textarea-bordered w-full", "rows": 4, "placeholder": "Project Description"}), + "start_date": forms.DateInput(attrs={"class": "input input-bordered w-full", "type": "date"}), + "deadline": forms.DateInput(attrs={"class": "input input-bordered w-full", "type": "date"}), + "budget": forms.NumberInput(attrs={"class": "input input-bordered w-full", "placeholder": "0.00"}), + "status": forms.Select(attrs={"class": "select select-bordered w-full"}), + } + +class TaskForm(forms.ModelForm): + class Meta: + model = Task + fields = ["title", "description", "status", "due_date"] + widgets = { + "title": forms.TextInput(attrs={"class": "input input-bordered w-full input-sm", "placeholder": "Task Title"}), + "description": forms.Textarea(attrs={"class": "textarea textarea-bordered w-full textarea-sm", "rows": 2, "placeholder": "Task Description"}), + "status": forms.Select(attrs={"class": "select select-bordered w-full select-sm"}), + "due_date": forms.DateInput(attrs={"class": "input input-bordered w-full input-sm", "type": "date"}), + } diff --git a/src/api/projects/urls.py b/src/api/projects/urls.py index e69de29..39287e9 100644 --- a/src/api/projects/urls.py +++ b/src/api/projects/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from src.api.projects import views + +urlpatterns = [ + path('', views.project_list, name='project_list'), + path('create/', views.project_create, name='project_create'), + path('/', views.project_detail, name='project_detail'), + path('/edit/', views.project_edit, name='project_edit'), + path('/delete/', views.project_delete, name='project_delete'), + path('/tasks/create/', views.task_create, name='task_create'), + path('tasks//update/', views.task_update_status, name='task_update_status'), + path('tasks//edit/', views.task_edit, name='task_edit'), + path('tasks//detail/', views.task_detail, name='task_detail'), + path('tasks//delete/', views.task_delete, name='task_delete'), +] diff --git a/src/api/projects/views.py b/src/api/projects/views.py index e69de29..6e417ab 100644 --- a/src/api/projects/views.py +++ b/src/api/projects/views.py @@ -0,0 +1,213 @@ +from django.shortcuts import render, get_object_or_404, redirect, HttpResponse +from django.views.decorators.http import require_http_methods +from src.models.projects import Project, Task +from src.api.projects.forms import ProjectForm, TaskForm + +def project_list(request): + projects = Project.objects.all().select_related('client') + return render(request, 'projects/project_list.html', {'projects': projects}) + +def project_create(request): + if request.method == 'POST': + form = ProjectForm(request.POST) + if form.is_valid(): + form.save() + return redirect('project_list') + else: + form = ProjectForm() + return render(request, 'projects/project_form.html', {'form': form}) + +def get_kanban_context(project): + tasks = project.tasks.all() + return { + 'project': project, + 'tasks_todo': tasks.filter(status='TODO'), + 'tasks_in_progress': tasks.filter(status='IN_PROGRESS'), + 'tasks_review': tasks.filter(status='REVIEW'), + 'tasks_done': tasks.filter(status='DONE'), + } + +def project_detail(request, pk): + project = get_object_or_404(Project, pk=pk) + context = get_kanban_context(project) + # Add a blank task form for the "Add Task" modal/section + context['task_form'] = TaskForm() + return render(request, 'projects/project_detail.html', context) + +@require_http_methods(["POST"]) +def task_create(request, project_id): + import logging + from datetime import datetime + logger = logging.getLogger(__name__) + + logger.info(f"Task create called for project {project_id}") + logger.info(f"POST data: {request.POST}") + + project = get_object_or_404(Project, pk=project_id) + title = request.POST.get('title', '').strip() + status = request.POST.get('status', 'TODO') + due_date_str = request.POST.get('due_date', '').strip() + + # Parse due_date string to date object + due_date = None + if due_date_str: + try: + due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date() + except ValueError: + logger.warning(f"Invalid due_date format: {due_date_str}") + + logger.info(f"Title: '{title}', Status: '{status}', Due Date: '{due_date}'") + + if not title: + logger.error("Task title is empty") + return HttpResponse("Task title is required", status=400) + + try: + task = Task.objects.create( + project=project, + title=title, + status=status, + due_date=due_date + ) + logger.info(f"Task created successfully: {task.id}") + except Exception as e: + logger.error(f"Error creating task: {e}") + return HttpResponse(f"Error creating task: {str(e)}", status=500) + + # Return the task HTML directly + due_date_html = '' + if task.due_date: + due_date_html = f''' +
+ + {task.due_date.strftime("%b %d")} +
''' + + html = f'''
+
+
+

{task.title}

{due_date_html} +
+ +
+
''' + + logger.info(f"Returning HTML response (length: {len(html)})") + return HttpResponse(html) + +@require_http_methods(["POST"]) +def task_update_status(request, task_id): + task = get_object_or_404(Task, pk=task_id) + status = request.POST.get('status') + if status in dict(Task.STATUS_CHOICES): + task.status = status + task.save() + + project = task.project + context = get_kanban_context(project) + return render(request, 'projects/partials/kanban_board.html', context) + +@require_http_methods(["POST"]) +def task_edit(request, task_id): + import logging + from datetime import datetime + logger = logging.getLogger(__name__) + + task = get_object_or_404(Task, pk=task_id) + title = request.POST.get('title', '').strip() + due_date_str = request.POST.get('due_date', '').strip() + + # Parse due_date string to date object + due_date = None + if due_date_str: + try: + due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date() + except ValueError: + logger.warning(f"Invalid due_date format: {due_date_str}") + + if not title: + return HttpResponse("Task title is required", status=400) + + try: + task.title = title + task.due_date = due_date + task.save() + logger.info(f"Task {task.id} updated successfully") + except Exception as e: + logger.error(f"Error updating task: {e}") + return HttpResponse(f"Error updating task: {str(e)}", status=500) + + # Return the updated task HTML + due_date_html = '' + if task.due_date: + due_date_html = f''' +
+ + {task.due_date.strftime("%b %d")} +
''' + + html = f'''
+
+
+

{task.title}

{due_date_html} +
+ +
+
''' + + return HttpResponse(html) + +def task_detail(request, task_id): + task = get_object_or_404(Task, pk=task_id) + return render(request, 'projects/partials/task_item.html', {'task': task}) + +@require_http_methods(["DELETE", "POST"]) +def task_delete(request, task_id): + task = get_object_or_404(Task, pk=task_id) + task.delete() + return HttpResponse("") + +def project_edit(request, pk): + project = get_object_or_404(Project, pk=pk) + if request.method == 'POST': + form = ProjectForm(request.POST, instance=project) + if form.is_valid(): + form.save() + return redirect('project_detail', pk=project.pk) + else: + form = ProjectForm(instance=project) + return render(request, 'projects/project_form.html', {'form': form, 'project': project}) + +@require_http_methods(["DELETE", "POST"]) +def project_delete(request, pk): + project = get_object_or_404(Project, pk=pk) + project.delete() + if request.htmx: + return HttpResponse("") + return redirect('project_list')