diff --git a/horilla/horilla_apps.py b/horilla/horilla_apps.py index c0dbc780d..21c1944dc 100644 --- a/horilla/horilla_apps.py +++ b/horilla/horilla_apps.py @@ -19,6 +19,8 @@ INSTALLED_APPS.append("auditlog") INSTALLED_APPS.append("biometric") INSTALLED_APPS.append("helpdesk") INSTALLED_APPS.append("offboarding") +INSTALLED_APPS.append("project") + if settings.env("AWS_ACCESS_KEY_ID", default=None) and "storages" not in INSTALLED_APPS: INSTALLED_APPS.append("storages") @@ -54,6 +56,7 @@ SIDEBARS = [ "offboarding", "asset", "helpdesk", + "project", ] WHITE_LABELLING = False diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/project/admin.py b/project/admin.py new file mode 100644 index 000000000..9191a4891 --- /dev/null +++ b/project/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import * +# Register your models here. +admin.site.register(Project) +admin.site.register(ProjectStage) +admin.site.register(Task) +admin.site.register(TimeSheet) diff --git a/project/apps.py b/project/apps.py new file mode 100644 index 000000000..7d1aa97f0 --- /dev/null +++ b/project/apps.py @@ -0,0 +1,12 @@ +from django.apps import AppConfig + + +class ProjectConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "project" + + def ready(self): + from horilla.horilla_settings import APPS + + APPS.append("project") + super().ready() diff --git a/project/decorator.py b/project/decorator.py new file mode 100644 index 000000000..da809b736 --- /dev/null +++ b/project/decorator.py @@ -0,0 +1,138 @@ +from project.methods import any_project_manager, any_project_member, any_task_manager, any_task_member +from .models import Project,Task,ProjectStage +from django.contrib import messages +from django.http import HttpResponseRedirect + + +decorator_with_arguments = lambda decorator: lambda *args, **kwargs: lambda func: decorator(func, *args, **kwargs) + +@decorator_with_arguments +def is_projectmanager_or_member_or_perms(function,perm): + def _function(request, *args, **kwargs): + """ + This method is used to check the employee is project manager or not + """ + user = request.user + if ( + user.has_perm(perm) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ): + return function(request, *args, **kwargs) + messages.info(request, "You don't have permission.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + return _function + +@decorator_with_arguments +def project_update_permission(function=None, *args, **kwargs): + def check_project_member(request,project_id=None,*args, **kwargs,): + """ + This method is used to check the employee is project member or not + """ + project = Project.objects.get(id = project_id) + if (request.user.has_perm('project.change_project') or + request.user.employee_get == project.manager or + request.user.employee_get in project.members.all() + ): + return function(request, *args,project_id=project_id, **kwargs) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + # return function(request, *args, **kwargs) + return check_project_member + + +@decorator_with_arguments +def project_delete_permission(function=None,*args,**kwargs): + def is_project_manager(request,project_id=None,*args, **kwargs,): + """ + This method is used to check the employee is project manager or not + """ + project = Project.objects.get(id = project_id) + if ( + request.user.employee_get == project.manager or + request.user.has_perm('project.delete_project') + ): + return function(request, *args,project_id=project_id, **kwargs) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + return is_project_manager + + +@decorator_with_arguments +def project_stage_update_permission(function=None, *args, **kwargs): + def check_project_member(request,stage_id=None,*args, **kwargs,): + """ + This method is used to check the employee is project stage member or not + """ + project = ProjectStage.objects.get(id = stage_id).project + if (request.user.has_perm('project.change_project') or + request.user.has_perm('project.change_task') or + request.user.employee_get == project.manager or + request.user.employee_get in project.members.all() + ): + return function(request, *args,stage_id=stage_id, **kwargs) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + # return function(request, *args, **kwargs) + return check_project_member + +@decorator_with_arguments +def project_stage_delete_permission(function=None,*args,**kwargs): + def is_project_manager(request,stage_id=None,*args, **kwargs,): + """ + This method is used to check the employee is project stage manager or not + """ + project = ProjectStage.objects.get(id = stage_id).project + if ( + request.user.employee_get == project.manager or + request.user.has_perm('project.delete_project') + ): + return function(request, *args,stage_id=stage_id, **kwargs) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + return is_project_manager + +@decorator_with_arguments +def task_update_permission(function=None,*args,**kwargs): + def is_task_member(request,task_id): + """ + This method is used to check the employee is task member or not + """ + task = Task.objects.get(id = task_id) + project = task.project + + if (request.user.has_perm('project.change_task') or + request.user.has_perm('project.change_project') or + request.user.employee_get == task.task_manager or + request.user.employee_get in task.task_members.all() or + request.user.employee_get == project.manager or + request.user.employee_get in project.members.all() + ): + return function(request, *args,task_id=task_id, **kwargs) + + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + return is_task_member + + +@decorator_with_arguments +def task_delete_permission(function=None,*args,**kwargs): + def is_task_manager(request,task_id): + """ + This method is used to check the employee is task manager or not + """ + task = Task.objects.get(id = task_id) + project = task.project + + if (request.user.has_perm('project.delete_task') or + request.user.has_perm('project.delete_project') or + request.user.employee_get == task.task_manager or + request.user.employee_get == project.manager + ): + return function(request,task_id=task_id) + + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + return is_task_manager \ No newline at end of file diff --git a/project/filters.py b/project/filters.py new file mode 100644 index 000000000..de8049214 --- /dev/null +++ b/project/filters.py @@ -0,0 +1,162 @@ +import django_filters +from django import forms +from django.db.models import Q +import django_filters +from attendance.filters import FilterSet +from horilla.filters import filter_by_name +from .models import TimeSheet, Project, Task,Employee + + +class ProjectFilter(FilterSet): + search = django_filters.CharFilter(method="filter_by_project") + + class Meta: + model = Project + fields = ["title", + "manager", + "status", + "end_date", + "start_date", + ] + + manager = django_filters.ModelChoiceFilter( + field_name = 'manager',queryset=Employee.objects.all() + ) + start_from = django_filters.DateFilter( + field_name="start_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + end_till = django_filters.DateFilter( + field_name="end_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + def filter_by_project(self, queryset, _, value): + queryset = queryset.filter(title__icontains=value) + return queryset + + +class TaskFilter(FilterSet): + search = django_filters.CharFilter(method="filter_by_task") + task_manager = django_filters.ModelChoiceFilter( + field_name = 'task_manager',queryset=Employee.objects.all() + ) + end_till = django_filters.DateFilter( + field_name="end_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + class Meta: + model = Task + fields = ["title", + "stage", + "task_manager", + "end_date", + "status", + "project", + ] + + def filter_by_task(self, queryset, _, value): + queryset = queryset.filter(title__icontains=value) + return queryset + +class TaskAllFilter(FilterSet): + search = django_filters.CharFilter(method="filter_by_task") + manager = django_filters.ModelChoiceFilter( + field_name = 'task_manager',queryset=Employee.objects.all() + ) + end_till = django_filters.DateFilter( + field_name="end_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + class Meta: + model = Task + fields = ["title", + 'project', + "stage", + "task_manager", + "end_date", + "status", + ] + + def filter_by_task(self, queryset, _, value): + queryset = queryset.filter(title__icontains=value) + return queryset + + + +class TimeSheetFilter(FilterSet): + """ + Filter set class for Timesheet model + """ + + date = django_filters.DateFilter( + field_name="date", widget=forms.DateInput(attrs={"type": "date"}) + ) + start_from = django_filters.DateFilter( + field_name="date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + end_till = django_filters.DateFilter( + field_name="date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + project = django_filters.ModelChoiceFilter( + field_name="project_id", queryset=Project.objects.all() + ), + + task = django_filters.ModelChoiceFilter( + field_name="task_id", queryset=Task.objects.all() + ) + search = django_filters.CharFilter(method="filter_by_employee") + + class Meta: + """ + Meta class to add additional options + """ + + model = TimeSheet + fields = [ + "employee_id", + "project_id", + "task_id", + "date", + "status", + ] + + def filter_by_employee(self, queryset, _, value): + """ + Filter queryset by first name or last name. + """ + + # Split the search value into first name and last name + + parts = value.split() + first_name = parts[0] + last_name = " ".join(parts[1:]) if len(parts) > 1 else "" + + # Filter the queryset by first name and last name + if first_name and last_name != "": + queryset = queryset.filter( + Q(employee_id__employee_first_name__icontains=first_name) + | Q(employee_id__employee_last_name__icontains=last_name) + ) + elif first_name: + queryset = queryset.filter( + Q(employee_id__employee_first_name__icontains=first_name) + | Q(employee_id__employee_last_name__icontains=first_name) + ) + elif last_name: + queryset = queryset.filter( + employee_id__employee_last_name__icontains=last_name + ) + return queryset + diff --git a/project/forms.py b/project/forms.py new file mode 100644 index 000000000..158caab5d --- /dev/null +++ b/project/forms.py @@ -0,0 +1,259 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ +from .models import * +from payroll.forms.forms import ModelForm +from django.template.loader import render_to_string + + +class ProjectForm(ModelForm): + """ + Form for Project model + """ + class Meta: + """ + Meta class to add the additional info + """ + + model = Project + fields = "__all__" + widgets = { + "start_date": forms.DateInput(attrs={"type": "date"}), + "end_date": forms.DateInput(attrs={"type": "date"}), + } + + +class ProjectTimeSheetForm(ModelForm): + """ + Form for Project model in Time sheet form + """ + def __init__(self, *args, **kwargs): + super(ProjectTimeSheetForm, self).__init__(*args, **kwargs) + self.fields["status"].widget.attrs.update( + { + "style": "width: 100%; height: 47px;", + "class": "oh-select", + } + ) + def __init__(self, *args, request=None, **kwargs): + super(ProjectTimeSheetForm, self).__init__(*args, **kwargs) + self.fields["manager"].widget.attrs.update({"id":"manager_id"}) + self.fields["status"].widget.attrs.update({"id":"status_id"}) + self.fields["members"].widget.attrs.update({"id":"members_id"}) + self.fields['title'].widget.attrs.update({'id':'id_project'}) + + class Meta: + """ + Meta class to add the additional info + """ + + model = Project + fields = "__all__" + widgets = { + "start_date": forms.DateInput(attrs={"type": "date"}), + "end_date": forms.DateInput(attrs={"type": "date"}), + } + + + +class TaskForm(ModelForm): + """ + Form for Task model + """ + + class Meta: + """ + Meta class to add the additional info + """ + + model = Task + fields = "__all__" + # exclude = ("project_id",) + + widgets = { + "end_date": forms.DateInput(attrs={"type": "date"}), + "project":forms.HiddenInput(), + "stage":forms.HiddenInput(), + "sequence":forms.HiddenInput() + } + +class TaskFormCreate(ModelForm): + + """ + Form for Task model in create button inside task view + """ + class Meta: + """ + Meta class to add the additional info + """ + + model = Task + fields = "__all__" + # exclude = ("project_id",) + + widgets = { + "end_date": forms.DateInput(attrs={"type": "date"}), + "project":forms.HiddenInput(), + "sequence":forms.HiddenInput(), + "stage": forms.SelectMultiple( + attrs={ + "class": "oh-select oh-select-2 select2-hidden-accessible", + "onchange": "keyResultChange($(this))", + } + ), + } + + def __init__(self, *args, request=None, **kwargs): + super(TaskFormCreate, self).__init__(*args, **kwargs) + self.fields["stage"].widget.attrs.update({"id":"project_stage"}) + + def as_p(self): + """ + Render the form fields as HTML table rows with Bootstrap styling. + """ + context = {"form": self} + table_html = render_to_string("common_form.html", context) + return table_html + +class TaskAllForm(ModelForm): + """ + Form for Task model in task all view + """ + class Meta: + """ + Meta class to add the additional info + """ + model = Task + fields = "__all__" + + widgets= { + "end_date": forms.DateInput(attrs={"type": "date"}), + "sequence":forms.HiddenInput() + } + def __init__(self, *args, request=None, **kwargs): + super(TaskAllForm, self).__init__(*args, **kwargs) + self.fields["project"].choices = list(self.fields["project"].choices) + self.fields["project"].choices.append( + ("create_new_project", "Create a new project") + ) + # Remove the select2 class from the "project" field + # self.fields["project"].widget.attrs.pop("class", None) + # self.fields["stage"].widget.attrs.pop("class", None) + self.fields["stage"].widget.attrs.update({"id":"project_stage"}) + + + +class TimeSheetForm(ModelForm): + """ + Form for Time Sheet model + """ + + class Meta: + """ + Meta class to add the additional info + """ + + model = TimeSheet + fields = "__all__" + widgets = { + "date": forms.DateInput(attrs={"type": "date"}), + } + + def __init__(self, *args, request=None, **kwargs): + super(TimeSheetForm, self).__init__(*args, **kwargs) + if request: + if not request.user.has_perm("project.add_timesheet"): + employee = Employee.objects.filter(employee_user_id=request.user) + employee_list = Employee.objects.filter(employee_work_info__reporting_manager_id=employee.first()) + self.fields["employee_id"].queryset = employee_list | employee + if len(employee_list) == 0: + self.fields["employee_id"].widget = forms.HiddenInput() + self.fields['project_id'].widget.attrs.update({'id':'id_project'}) + self.fields["project_id"].choices = list(self.fields["project_id"].choices) + self.fields["project_id"].choices.append( + ("create_new_project", "Create a new project") + ) + +class TimesheetInTaskForm(ModelForm): + class Meta: + """ + Meta class to add the additional info + """ + model = TimeSheet + fields = "__all__" + widgets = { + "date": forms.DateInput(attrs={"type": "date"}), + "project_id":forms.HiddenInput(), + "task_id":forms.HiddenInput(), + } + +class ProjectStageForm(ModelForm): + """ + Form for Project stage model + """ + + class Meta: + """ + Meta class to add the additional info + """ + + model = ProjectStage + fields = "__all__" + # exclude = ("project",) + + widgets = { + "project":forms.HiddenInput(), + 'sequence':forms.HiddenInput() + } + +class TaskTimeSheetForm(ModelForm): + """ + Form for Task model in timesheet form + """ + class Meta: + """ + Meta class to add the additional info + """ + + model = Task + fields = "__all__" + widgets = { + "end_date": forms.DateInput(attrs={"type": "date"}), + "project": forms.HiddenInput(), + } + + def __init__(self, *args, **kwargs): + super(TaskTimeSheetForm, self).__init__(*args, **kwargs) + # Add style to the start_date and end_date fields + # self.fields["stage"].choices.append( + # ("create_new_project", "Create a new project") + # ) + self.fields["status"].widget.attrs.update( + { + "style": "width: 100%; height: 47px;", + "class": "oh-select", + + } + ) + self.fields["description"].widget.attrs.update( + { + "style": "width: 100%; height: 130px;", + "class": "oh-select", + } + ) + self.fields["description"].widget.attrs.update( + { + "style": "width: 100%; height: 130px;", + "class": "oh-select", + } + ) + + self.fields["stage"].widget.attrs.update( + { + "id":'project_stage' + } + ) + + + + + diff --git a/project/methods.py b/project/methods.py new file mode 100644 index 000000000..26372310d --- /dev/null +++ b/project/methods.py @@ -0,0 +1,196 @@ +import random +from django.core.paginator import Paginator +from django.contrib import messages +from django.http import HttpResponseRedirect +from base.methods import get_pagination +from recruitment.decorators import decorator_with_arguments +from employee.models import Employee +from project.models import TimeSheet,Project,Task + + +def strtime_seconds(time): + """ + this method is used to reconvert time in H:M formate string back to seconds and return it + args: + time : time in H:M format + """ + ftr = [3600, 60, 1] + return sum(a * b for a, b in zip(ftr, map(int, time.split(":")))) + + +def paginator_qry(qryset, page_number): + """ + This method is used to generate common paginator limit. + """ + paginator = Paginator(qryset, get_pagination()) + qryset = paginator.get_page(page_number) + return qryset + + +def random_color_generator(): + r = random.randint(0, 255) + g = random.randint(0, 255) + b = random.randint(0, 255) + if r==g or g==b or b==r: + random_color_generator() + return f"rgba({r}, {g}, {b} , 0.7)" + + +# color_palette=[] +# Function to generate distinct colors for each project +def generate_colors(num_colors): + # Define a color palette with distinct colors + color_palette = [ + "rgba(255, 99, 132, 1)", # Red + "rgba(54, 162, 235, 1)", # Blue + "rgba(255, 206, 86, 1)", # Yellow + "rgba(75, 192, 192, 1)", # Green + "rgba(153, 102, 255, 1)", # Purple + "rgba(255, 159, 64, 1)", # Orange + ] + + if num_colors > len(color_palette): + for i in range(num_colors-len(color_palette)): + color_palette.append(random_color_generator()) + + colors = [] + for i in range(num_colors): + # color=random_color_generator() + colors.append(color_palette[i % len(color_palette)]) + + return colors + +def any_project_manager(user): + employee = user.employee_get + if employee.project_manager.all().exists(): + return True + else: + return False + +def any_project_member(user): + employee = user.employee_get + if employee.project_members.all().exists(): + return True + else: + return False + +def any_task_manager(user): + employee = user.employee_get + if employee.task_set.all().exists(): + return True + else: + return False + +def any_task_member(user): + employee = user.employee_get + if employee.tasks.all().exists(): + return True + else: + return False + +@decorator_with_arguments +def is_projectmanager_or_member_or_perms(function,perm): + def _function(request, *args, **kwargs): + + """ + This method is used to check the employee is project manager or not + """ + user = request.user + if ( + user.has_perm(perm) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ): + return function(request, *args, **kwargs) + messages.info(request, "You don't have permission.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + return _function + + +# def is_project_member(request,project_id): +# """ +# This method is used to check the employee is project member or not +# """ +# print(Project.objects.get(id = project_id)) +# print(Project.objects.get(id = project_id).members.all() ) +# if (request.user.has_perm('project.change_project') or +# request.user.employee_get == Project.objects.get(id = project_id).manager or +# request.user.employee_get in Project.objects.get(id = project_id).members.all() +# ): +# return True +# return False + +# def is_project_manager(request,project_id): +# """ +# This method is used to check the employee is project manager or not +# """ +# print(Project.objects.get(id = project_id)) +# print(Project.objects.get(id = project_id).manager ) +# if ( +# request.user.employee_get == Project.objects.get(id = project_id).manager or +# request.user.has_perm('project.delete_project') + +# ): +# return True +# return False + + +# def is_project_manager(request, project_id): +# """ +# This function checks if the user is a project manager or has permission to delete a project. +# """ +# user = request.user +# try: +# project = Project.objects.get(id=project_id) +# except Project.DoesNotExist: +# return False # Project with the specified ID does not exist + +# if user.employee_get == project.manager or user.has_perm('project.delete_project'): +# return True # User is a project manager or has delete_project permission + +# return False # User does not have the required permission + + +def is_task_member(request,task_id): + """ + This method is used to check the employee is task member or not + """ + if (request.user.has_perm('project.change_task') or + request.user.employee_get == Task.objects.get(id = task_id).task_manager or + request.user.employee_get in Task.objects.get(id = task_id).task_members.all() + ): + return True + return False + +def is_task_manager(request,task_id): + """ + This method is used to check the employee is task member or not + """ + if (request.user.has_perm('project.delete_task') or + request.user.employee_get == Task.objects.get(id = task_id).task_manager + ): + return True + return False + + +def time_sheet_update_permissions(request,time_sheet_id): + if ( + request.user.has_perm("project.change_timesheet") + or request.user.employee_get == TimeSheet.objects.get(id=time_sheet_id).employee_id + or TimeSheet.objects.get(id=time_sheet_id).employee_id in Employee.objects.filter(employee_work_info__reporting_manager_id=request.user.employee_get) + ): + return True + else: + return False + +def time_sheet_delete_permissions(request,time_sheet_id): + if ( + request.user.has_perm("project.delete_timesheet") + or request.user.employee_get == TimeSheet.objects.get(id=time_sheet_id).employee_id + or TimeSheet.objects.get(id=time_sheet_id).employee_id in Employee.objects.filter(employee_work_info__reporting_manager_id=request.user.employee_get) + ): + return True + else: + return False \ No newline at end of file diff --git a/project/migrations/__init__.py b/project/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/project/models.py b/project/models.py new file mode 100644 index 000000000..44bd138b2 --- /dev/null +++ b/project/models.py @@ -0,0 +1,249 @@ +""" +models.py + +This module is used to register models for project app + +""" +from django.db import models +from django.apps import apps +from employee.models import Employee +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from employee.models import Employee +from datetime import date + + + +# Create your models here. + +def validate_time_format(value): + """ + this method is used to validate the format of duration like fields. + """ + if len(value) > 5: + raise ValidationError(_("Invalid format, it should be HH:MM format")) + try: + hour, minute = value.split(":") + + if len(hour) < 2 or len(minute) < 2: + raise ValidationError(_( "Invalid format, it should be HH:MM format")) + + minute = int(minute) + if len(hour) > 2 or minute not in range(60): + raise ValidationError(_("Invalid time")) + except ValueError as error: + raise ValidationError(_("Invalid format")) from error + + + +class Project(models.Model): + PROJECT_STATUS = [ + ("new", "New"), + ("in_progress", "In Progress"), + ("completed", "Completed"), + ("on_hold", "On Hold"), + ("cancelled", "Cancelled"), + ("expired", "Expired"), + ] + title = models.CharField(max_length=200, unique=True, verbose_name="Name") + manager = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="project_manager", + verbose_name="Project Manager", + ) + members = models.ManyToManyField( + Employee, + blank=True, + related_name="project_members", + verbose_name="Project Members", + ) + status = models.CharField(choices=PROJECT_STATUS, max_length=250, default="new") + start_date = models.DateField() + end_date = models.DateField(null=True, blank=True) + document = models.FileField( + upload_to="project/files", blank=True, null=True, verbose_name="Project File" + ) + is_active = models.BooleanField(default=True) + description = models.TextField() + objects = models.Manager() + + + + def clean(self) -> None: + # validating end date + if self.end_date is not None: + if self.end_date < self.start_date: + raise ValidationError({"document": "End date is less than start date"}) + if self.end_date < date.today(): + self.status = "expired" + + def __str__(self): + return self.title + + + +class ProjectStage(models.Model): + """ + ProjectStage model + """ + title = models.CharField(max_length=200) + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="project_stages", + ) + sequence = models.IntegerField(null=True,blank=True) + is_end_stage = models.BooleanField(default=False) + + + def __str__(self) -> str: + return f"{self.title}" + + def clean(self) -> None: + if self.is_end_stage: + project = self.project + existing_end_stage = project.project_stages.filter(is_end_stage = True).exclude(id = self.id) + + if existing_end_stage: + end_stage = project.project_stages.filter(is_end_stage = True).first() + raise ValidationError( + _(f"Already exist an end stage - {end_stage.title}.") + ) + + + class Meta: + """ + Meta class to add the additional info + """ + + unique_together = ["project", "title"] + + +class Task(models.Model): + """ + Task model + """ + TASK_STATUS = [ + ("to_do", "To Do"), + ("in_progress", "In Progress"), + ("completed", "Completed"), + ("expired", "Expired"), + ] + title = models.CharField(max_length=200) + project = models.ForeignKey(Project, on_delete=models.CASCADE, null = True, blank= True) + stage = models.ForeignKey( + ProjectStage, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="tasks", + verbose_name="Project Stage", + ) + task_manager = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + null=True, + blank=True, + verbose_name="Task Manager", + ) + task_members = models.ManyToManyField( + Employee, blank=True, related_name="tasks", verbose_name="Task Members" + ) + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) + status = models.CharField(choices=TASK_STATUS, max_length=250, default="to_do") + document = models.FileField( + upload_to="task/files", blank=True, null=True, verbose_name="Task File" + ) + is_active = models.BooleanField(default=True) + description = models.TextField() + sequence = models.IntegerField(default=0) + objects = models.Manager() + + + def clean(self) -> None: + if self.end_date is not None and self.project.end_date is not None: + if ( + self.project.end_date < self.end_date + or self.project.start_date > self.end_date + ): + raise ValidationError( + {"status": "End date must be between project start and end dates."} + ) + if self.end_date < date.today(): + self.status = "expired" + class Meta: + """ + Meta class to add the additional info + """ + unique_together = ["project", "title"] + + def __str__(self): + return f"{self.title}" + + + + + +class TimeSheet(models.Model): + """ + TimeSheet model + """ + TIME_SHEET_STATUS = [ + ("in_Progress", "In Progress"), + ("completed", "Completed"), + ] + project_id = models.ForeignKey( + Project, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="project_timesheet", + verbose_name="Project", + ) + task_id = models.ForeignKey( + Task, + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="task_timesheet", + verbose_name="Task", + ) + employee_id = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + verbose_name="Employee", + ) + date = models.DateField(default=timezone.now) + time_spent = models.CharField( + null=True, + default="00:00", + max_length=10, + validators=[validate_time_format], + verbose_name="Hours Spent", + ) + status = models.CharField( + choices=TIME_SHEET_STATUS, max_length=250, default="in_Progress" + ) + description = models.TextField(blank=True, null=True) + objects = models.Manager() + + class Meta: + ordering = ("-id",) + + def clean(self): + if self.project_id is None: + raise ValidationError({"project_id": "Project name is Required."}) + if self.description is None or self.description == "": + raise ValidationError( + {"description": "Please provide a description to your Time sheet"} + ) + + def __str__(self): + return f"{self.project_id} {self.date} {self.time_spent}" diff --git a/project/sidebar.py b/project/sidebar.py new file mode 100644 index 000000000..4baa544cf --- /dev/null +++ b/project/sidebar.py @@ -0,0 +1,105 @@ +""" +project/sidebar.py +""" + +from django.urls import reverse +from base.templatetags.basefilters import is_reportingmanager +from django.utils.translation import gettext_lazy as trans +from django.contrib.auth.context_processors import PermWrapper + + +from project.methods import any_project_manager, any_project_member, any_task_manager, any_task_member + +MENU = trans("Project") +IMG_SRC = "images/ui/project.png" +ACCESSIBILITY = "project.sidebar.menu_accessibilty" + +SUBMENUS = [ + { + "menu": trans("Dashboard"), + "redirect": reverse("project-dashboard-view"), + "accessibility": "project.sidebar.dashboard_accessibility" + }, + { + "menu": trans("Projects"), + "redirect": reverse("project-view"), + "accessibility": "project.sidebar.project_accessibility", + }, + { + "menu": trans("Tasks"), + "redirect": reverse("task-all"), + "accessibility": "project.sidebar.task_accessibility", + }, + { + "menu": trans("Timesheet"), + "redirect": reverse("view-time-sheet"), + "accessibility": "project.sidebar.timesheet_accessibility", + }, +] + +def menu_accessibilty( + request, _menu: str = "", user_perms: PermWrapper = [], *args, **kwargs +) -> bool: + user = request.user + return ( + "project" in user_perms or + is_reportingmanager(user) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ) + +def dashboard_accessibility(request, submenu, user_perms, *args, **kwargs): + user = request.user + if ( + user.has_perm("project.view_project") or + is_reportingmanager(user) or + any_project_manager(user) or + any_task_manager(user) + ): + return True + else: + return False + +def project_accessibility(request, submenu, user_perms, *args, **kwargs): + user = request.user + if ( + user.has_perm("project.view_project") or + is_reportingmanager(user) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ): + return True + else: + return False + +def task_accessibility(request, submenu, user_perms, *args, **kwargs): + user = request.user + if ( + user.has_perm("project.view_task") or + is_reportingmanager(user) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ): + return True + else: + return False + +def timesheet_accessibility(request, submenu, user_perms, *args, **kwargs): + user = request.user + if ( + user.has_perm("project.view_timesheet") or + is_reportingmanager(user) or + any_project_manager(user) or + any_project_member(user) or + any_task_manager(user) or + any_task_member(user) + ): + return True + else: + return False \ No newline at end of file diff --git a/project/static/dashboard/projectChart.js b/project/static/dashboard/projectChart.js new file mode 100644 index 000000000..fc9e5752a --- /dev/null +++ b/project/static/dashboard/projectChart.js @@ -0,0 +1,89 @@ +$(document).ready(function(){ + function projectStatusChart(dataSet, labels) { + const data = { + labels: labels, + datasets: dataSet, + }; + // Create chart using the Chart.js library + window['projectChart'] = {} + const ctx = document.getElementById("projectStatusCanvas").getContext("2d"); + + projectChart = new Chart(ctx, { + type: 'bar', + data: data, + options: { + }, + }); + } + $.ajax({ + url: "/project/project-status-chart", + type: "GET", + success: function (response) { + // Code to handle the response + // response = {'dataSet': [{'label': 'Odoo developer 2023-03-30', 'data': [3, 0, 5, 3]}, {'label': 'React developer 2023-03-31', 'data': [0, 1, 1, 0]}, {'label': 'Content Writer 2023-04-01', 'data': [1, 0, 0, 0]}], 'labels': ['Initial', 'Test', 'Interview', 'Hired']} + dataSet = response.dataSet; + labels = response.labels; + projectStatusChart(dataSet, labels); + + }, + }); + $('#projectStatusForward').click(function (e) { + var chartType = projectChart.config.type + if (chartType === 'line') { + chartType = 'bar'; + } else if(chartType==='bar') { + chartType = 'doughnut'; + } else if(chartType==='doughnut'){ + chartType = 'pie' + }else if(chartType==='pie'){ + chartType = 'line' + } + projectChart.config.type = chartType; + projectChart.update(); + }); + + // for creating task status chart + function taskStatusChart(dataSet, labels) { + const data = { + labels: labels, + datasets: dataSet, + }; + // Create chart using the Chart.js library + window['taskChart'] = {} + const ctx = document.getElementById("taskStatusCanvas").getContext("2d"); + + taskChart = new Chart(ctx, { + type: 'bar', + data: data, + options: { + }, + }); + } + $.ajax({ + url: "/project/task-status-chart", + type: "GET", + success: function (response) { + // Code to handle the response + // response = {'dataSet': [{'label': 'Odoo developer 2023-03-30', 'data': [3, 0, 5, 3]}, {'label': 'React developer 2023-03-31', 'data': [0, 1, 1, 0]}, {'label': 'Content Writer 2023-04-01', 'data': [1, 0, 0, 0]}], 'labels': ['Initial', 'Test', 'Interview', 'Hired']} + dataSet = response.dataSet; + labels = response.labels; + taskStatusChart(dataSet, labels); + + }, + }); + $('#taskStatusForward').click(function (e) { + var chartType = taskChart.config.type + if (chartType === 'line') { + chartType = 'bar'; + } else if(chartType==='bar') { + chartType = 'doughnut'; + } else if(chartType==='doughnut'){ + chartType = 'pie' + }else if(chartType==='pie'){ + chartType = 'line' + } + taskChart.config.type = chartType; + taskChart.update(); + }); + +}); \ No newline at end of file diff --git a/project/static/project/import.js b/project/static/project/import.js new file mode 100644 index 000000000..c55a305ea --- /dev/null +++ b/project/static/project/import.js @@ -0,0 +1,252 @@ +var downloadMessages = { + ar: "هل ترغب في تنزيل القالب؟", + de: "Möchten Sie die Vorlage herunterladen?", + es: "¿Quieres descargar la plantilla?", + en: "Do you want to download the template?", + fr: "Voulez-vous télécharger le modèle ?", + }; + + var importsuccess = { + ar: "نجح الاستيراد", // Arabic + de: "Import erfolgreich", // German + es: "Importado con éxito", // Spanish + en: "Imported Successfully!", // English + fr: "Importation réussie" // French + }; + + var uploadsuccess = { + ar: "تحميل كامل", // Arabic + de: "Upload abgeschlossen", // German + es: "Carga completa", // Spanish + en: "Upload Complete!", // English + fr: "Téléchargement terminé" // French + }; + + var uploadingmessage = { + ar: "جارٍ الرفع", + de: "Hochladen...", + es: "Subiendo...", + en: "Uploading...", + fr: "Téléchargement en cours...", + }; + + var validationmessage = { + ar: "يرجى تحميل ملف بامتداد .xlsx فقط.", + de: "Bitte laden Sie nur eine Datei mit der Erweiterung .xlsx hoch.", + es: "Por favor, suba un archivo con la extensión .xlsx solamente.", + en: "Please upload a file with the .xlsx extension only.", + fr: "Veuillez télécharger uniquement un fichier avec l'extension .xlsx.", + }; + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var languageCode = response.language_code; + callback(languageCode); // Pass the language code to the callback + }, + }); + } + + + // Get the form element + var form = document.getElementById("projectImportForm"); + + // Add an event listener to the form submission + form.addEventListener("submit", function (event) { + // Prevent the default form submission + event.preventDefault(); + + // Create a new form data object + var formData = new FormData(); + + // Append the file to the form data object + var fileInput = document.querySelector("#projectImportFile"); + formData.append("file", fileInput.files[0]); + $.ajax({ + type: "POST", + url: "/project/project-import", + dataType: "binary", + data: formData, + processData: false, + contentType: false, + headers: { + "X-CSRFToken": getCookie('csrftoken'), // Replace with your csrf token value + }, + xhrFields: { + responseType: "blob", + }, + success: function (response) { + const file = new Blob([response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "ImportError.xlsx"; + document.body.appendChild(link); + link.click(); + }, + error: function (xhr, textStatus, errorThrown) { + console.error("Error downloading file:", errorThrown); + }, + }); + }); + + + $("#importProject").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = downloadMessages[languageCode]; + // Use SweetAlert for the confirmation dialog + Swal.fire({ + + text: confirmMessage, + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#008000', + cancelButtonColor: '#d33', + confirmButtonText: 'Confirm' + }).then(function(result) { + if (result.isConfirmed) { + $("#loading").show(); + var xhr = new XMLHttpRequest(); + xhr.open('GET', "/project/project-import", true); + xhr.responseType = 'arraybuffer'; + + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + var percent = (e.loaded / e.total) * 100; + $(".progress-bar").width(percent + "%").attr("aria-valuenow", percent); + $("#progress-text").text("Uploading... " + percent.toFixed(2) + "%"); + } + }; + + xhr.onload = function (e) { + if (this.status == 200) { + const file = new Blob([this.response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "project_template.xlsx"; + document.body.appendChild(link); + link.click(); + } + }; + + xhr.onerror = function (e) { + console.error("Error downloading file:", e); + }; + + xhr.send(); + } + }); + }); + }); + + $(document).ajaxStart(function () { + $("#loading").show(); + }); + + $(document).ajaxStop(function () { + $("#loading").hide(); + }); + + function simulateProgress() { + var languageCode = null; + getCurrentLanguageCode(function(code){ + languageCode = code; + var importMessage = importsuccess[languageCode]; + var uploadMessage = uploadsuccess[languageCode]; + var uploadingMessage = uploadingmessage[languageCode]; + let progressBar = document.querySelector('.progress-bar'); + let progressText = document.getElementById('progress-text'); + + let width = 0; + let interval = setInterval(function() { + if (width >= 100) { + clearInterval(interval); + progressText.innerText = uploadMessage; + setTimeout(function() { + document.getElementById('loading').style.display = 'none'; + }, 3000); + Swal.fire({ + text: importMessage, + icon: "success", + showConfirmButton: false, + timer: 2000, + timerProgressBar: true, + }); + setTimeout(function() { + $('#projectImport').removeClass('oh-modal--show'); + location.reload(true); + }, 2000); + } else { + width++; + progressBar.style.width = width + '%'; + progressBar.setAttribute('aria-valuenow', width); + progressText.innerText = uploadingMessage + width + '%'; + } + }, 20); + } + )} + + document.getElementById('projectImportForm').addEventListener('submit', function(event) { + event.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function(code){ + languageCode = code; + var erroMessage = validationmessage[languageCode]; + + var fileInput = $('#projectImportFile').val(); + var allowedExtensions = /(\.xlsx)$/i; + + if (!allowedExtensions.exec(fileInput)) { + + var errorMessage = document.createElement('div'); + errorMessage.classList.add('error-message'); + + errorMessage.textContent = erroMessage; + + document.getElementById('error-container').appendChild(errorMessage); + + fileInput.value = ''; + + setTimeout(function() { + errorMessage.remove(); + }, 2000); + + return false; + } + else{ + + document.getElementById('loading').style.display = 'block'; + + + simulateProgress(); + } + + }); + }) + \ No newline at end of file diff --git a/project/static/project/project_action.js b/project/static/project/project_action.js new file mode 100644 index 000000000..2f27f7a0e --- /dev/null +++ b/project/static/project/project_action.js @@ -0,0 +1,408 @@ +var archiveMessages = { + // ar: "هل ترغب حقًا في أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter archivieren?", + // es: "¿Realmente quieres archivar a todos los empleados seleccionados?", + en: "Do you really want to archive all the selected projects?", + // fr: "Voulez-vous vraiment archiver tous les employés sélectionnés ?", + }; + + var unarchiveMessages = { + // ar: "هل ترغب حقًا في إلغاء أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter aus der Archivierung zurückholen?", + // es: "¿Realmente quieres desarchivar a todos los empleados seleccionados?", + en: "Do you really want to unarchive all the selected projects?", + // fr: "Voulez-vous vraiment désarchiver tous les employés sélectionnés?", + }; + + var deleteMessages = { + // ar: "هل ترغب حقًا في حذف جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter löschen?", + // es: "¿Realmente quieres eliminar a todos los empleados seleccionados?", + en: "Do you really want to delete all the selected projects?", + // fr: "Voulez-vous vraiment supprimer tous les employés sélectionnés?", + }; + + var exportMessages = { + // ar: "هل ترغب حقًا في حذف جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter löschen?", + // es: "¿Realmente quieres eliminar a todos los empleados seleccionados?", + en: "Do you really want to export all the selected projects?", + // fr: "Voulez-vous vraiment supprimer tous les employés sélectionnés?", + }; + + var downloadMessages = { + ar: "هل ترغب في تنزيل القالب؟", + de: "Möchten Sie die Vorlage herunterladen?", + es: "¿Quieres descargar la plantilla?", + en: "Do you want to download the template?", + fr: "Voulez-vous télécharger le modèle ?", + }; + + var norowMessages = { + // ar: "لم يتم تحديد أي صفوف.", + // de: "Es wurden keine Zeilen ausgewählt.", + // es: "No se han seleccionado filas.", + en: "No rows have been selected.", + // fr: "Aucune ligne n'a été sélectionnée.", + }; + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var languageCode = response.language_code; + callback(languageCode); // Pass the language code to the callback + }, + }); + } + + + // // Get the form element + // var form = document.getElementById("projectImportForm"); + + // // Add an event listener to the form submission + // form.addEventListener("submit", function (event) { + // // Prevent the default form submission + // event.preventDefault(); + + // // Create a new form data object + // var formData = new FormData(); + + // // Append the file to the form data object + // var fileInput = document.querySelector("#projectImportFile"); + // formData.append("file", fileInput.files[0]); + // $.ajax({ + // type: "POST", + // url: "/project/project-import", + // dataType: "binary", + // data: formData, + // processData: false, + // contentType: false, + // headers: { + // "X-CSRFToken": getCookie('csrftoken'), // Replace with your csrf token value + // }, + // xhrFields: { + // responseType: "blob", + // }, + // success: function (response) { + // const file = new Blob([response], { + // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // }); + // const url = URL.createObjectURL(file); + // const link = document.createElement("a"); + // link.href = url; + // link.download = "ImportError.xlsx"; + // document.body.appendChild(link); + // link.click(); + // }, + // error: function (xhr, textStatus, errorThrown) { + // console.error("Error downloading file:", errorThrown); + // }, + // }); + // }); + + // $("#importProject").click(function (e) { + // e.preventDefault(); + // var languageCode = null; + // getCurrentLanguageCode(function (code) { + // languageCode = code; + // var confirmMessage = downloadMessages[languageCode]; + // // Use SweetAlert for the confirmation dialog + // Swal.fire({ + // text: confirmMessage, + // icon: 'question', + // showCancelButton: true, + // confirmButtonColor: '#008000', + // cancelButtonColor: '#d33', + // confirmButtonText: 'Confirm' + // }).then(function(result) { + // if (result.isConfirmed) { + // $.ajax({ + // type: "GET", + // url: "/project/project-import", + // dataType: "binary", + // xhrFields: { + // responseType: "blob", + // }, + // success: function (response) { + // const file = new Blob([response], { + // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + // }); + // const url = URL.createObjectURL(file); + // const link = document.createElement("a"); + // link.href = url; + // link.download = "project_template.xlsx"; + // document.body.appendChild(link); + // link.click(); + // }, + // error: function (xhr, textStatus, errorThrown) { + // console.error("Error downloading file:", errorThrown); + // }, + // }); + // } + // }); + // }); + // }); + + // $('#importProject').click(function (e) { + // $.ajax({ + // type: 'POST', + // url: '/project/project-import', + // dataType: 'binary', + // xhrFields: { + // responseType: 'blob' + // }, + // success: function(response) { + // const file = new Blob([response], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}); + // const url = URL.createObjectURL(file); + // const link = document.createElement('a'); + // link.href = url; + // link.download = 'project.xlsx'; + // document.body.appendChild(link); + // link.click(); + // }, + // error: function(xhr, textStatus, errorThrown) { + // console.error('Error downloading file:', errorThrown); + // } + // }); + // }); + + + $(".all-projects").change(function (e) { + var is_checked = $(this).is(":checked"); + if (is_checked) { + $(".all-project-row").prop("checked", true); + } else { + $(".all-project-row").prop("checked", false); + } + }); + + $("#exportProject").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = exportMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-project-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-project-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/project-bulk-export", + dataType: "binary", + xhrFields: { + responseType: "blob", + }, + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + const file = new Blob([response], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = "project details.xlsx"; + document.body.appendChild(link); + link.click(); + }, + }); + } + }); + } + }); + }); + + $("#archiveProject").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = archiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-project-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + e.preventDefault(); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/project-bulk-archive?is_active=False", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); + + +$("#unArchiveProject").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = unarchiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-project-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-project-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/project-bulk-archive?is_active=True", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); + +$("#deleteProject").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = deleteMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-project-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-project-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/project-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); \ No newline at end of file diff --git a/project/static/project/project_view.js b/project/static/project/project_view.js new file mode 100644 index 000000000..5195a526a --- /dev/null +++ b/project/static/project/project_view.js @@ -0,0 +1,25 @@ +$(document).ready(function(){ + $("#filter-project").keyup(function (e) { + $(".project-view-type").attr("hx-vals", `{"search":"${$(this).val()}"}`); + }); + $(".project-view-type").click(function (e) { + let view = $(this).data("view"); + var currentURL = window.location.href; + if (view != undefined){ + // Check if the query string already exists in the URL + if (/\?view=[^&]+/.test(currentURL)) { + // If the query parameter ?view exists, replace it with the new value + newURL = currentURL.replace(/\?view=[^&]+/, "?view="+view); + } + else { + // If the query parameter ?view does not exist, add it to the URL + var separator = currentURL.includes('?') ? '&' : '?'; + newURL = currentURL + separator + "view="+view; + } + + history.pushState({}, "", newURL); + $("#filter-project").attr("hx-vals", `{"view":"${view}"}`); + $('#timesheetForm').attr("hx-vals", `{"view":"${view}"}`); + } + }); +}); \ No newline at end of file diff --git a/project/static/project/task_pipeline.js b/project/static/project/task_pipeline.js new file mode 100644 index 000000000..12093c1fb --- /dev/null +++ b/project/static/project/task_pipeline.js @@ -0,0 +1,138 @@ +$(document).ready(function(){ + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + // for the search function + $("#filter-task").keyup(function (e) { + var search = $(this).val().toLowerCase(); + var view = $(this).data('view') + if (view == 'list') { + $('.task_row').each(function () { + var task = $(this).data('task') + if (task.includes(search)) { + $(this).show(); + } else { + $(this).hide(); + } + }) + + } else { + $('.task').each(function () { + var task = $(this).data('task') + if (task.includes(search)) { + $(this).show(); + } else { + $(this).hide(); + } + }) + } + }); + + + $('.task').mousedown(function(){ + window ['previous_task_id'] = $(this).attr('data-task-id') + window ['previous_stage_id'] = $(this).parent().attr('data-stage-id') + }); + + $(".tasks-container").on("DOMNodeInserted", function (e) { + var updated_task_id = $(e.target).attr('data-task-id'); + var updated_stage_id = $(this).attr("data-stage-id"); + if (updated_task_id != null) { + var new_seq = {} + var task_container = $(this).children(".task") + task_container.each(function(i, obj) { + new_seq[$(obj).data('task-id')] = i + }); + $.ajax({ + type: "post", + url: '/project/drag-and-drop-task', + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + updated_task_id: updated_task_id, + updated_stage_id : updated_stage_id, + previous_task_id : previous_task_id, + previous_stage_id : previous_stage_id, + sequence:JSON.stringify(new_seq), + }, + success: function(response){ + if (response.change) { // Check if the 'change' attribute in the response is True + $("#ohMessages").append(` +
+
+ ${response.message} +
+
`); + } + }, + }); + }; + }); + + + + $('.stage').mouseup(function(){ + window['previous_stage_id'] = $(this).attr('data-stage-id') + window['previous_sequence'] = $(this).attr('data-sequence') + setTimeout(function() { + var new_seq = {} + $('.stage').each(function(i, obj) { + new_seq[$(obj).attr('data-stage-id')] = i + }); + $.ajax({ + type: 'post', + url: '/project/drag-and-drop-stage', + data:{ + csrfmiddlewaretoken: getCookie("csrftoken"), + sequence:JSON.stringify(new_seq), + }, + success: function(response) { + if (response.change) { // Check if the 'change' attribute in the response is True + $("#ohMessages").append(` +
+
+ ${response.message} +
+
`); + } + }, + }) + }, 100); + }) + + $("#filter-task").keyup(function (e) { + $(".task-view-type").attr("hx-vals", `{"search":"${$(this).val()}"}`); + }); + $(".task-view-type").click(function (e) { + let view = $(this).data("view"); + var currentURL = window.location.href; + if (view != undefined){ + // Check if the query string already exists in the URL + if (/\?view=[^&]+/.test(currentURL)) { + // If the query parameter ?view exists, replace it with the new value + newURL = currentURL.replace(/\?view=[^&]+/, "?view="+view); + } + else { + // If the query parameter ?view does not exist, add it to the URL + var separator = currentURL.includes('?') ? '&' : '?'; + newURL = currentURL + separator + "view=card"; + } + + history.pushState({}, "", newURL); + $("#filter-task").attr("hx-vals", `{"view":"${view}"}`); + $('#timesheetForm').attr("hx-vals", `{"view":"${view}"}`); + } + }); +}); \ No newline at end of file diff --git a/project/static/task_all/task_all_action.js b/project/static/task_all/task_all_action.js new file mode 100644 index 000000000..9f8d18335 --- /dev/null +++ b/project/static/task_all/task_all_action.js @@ -0,0 +1,220 @@ +var archiveMessages = { + // ar: "هل ترغب حقًا في أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter archivieren?", + // es: "¿Realmente quieres archivar a todos los empleados seleccionados?", + en: "Do you really want to archive all the selected tasks?", + // fr: "Voulez-vous vraiment archiver tous les employés sélectionnés ?", + }; + + var unarchiveMessages = { + // ar: "هل ترغب حقًا في إلغاء أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter aus der Archivierung zurückholen?", + // es: "¿Realmente quieres desarchivar a todos los empleados seleccionados?", + en: "Do you really want to unarchive all the selected tasks?", + // fr: "Voulez-vous vraiment désarchiver tous les employés sélectionnés?", + }; + + var deleteMessages = { + // ar: "هل ترغب حقًا في حذف جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter löschen?", + // es: "¿Realmente quieres eliminar a todos los empleados seleccionados?", + en: "Do you really want to delete all the selected tasks?", + // fr: "Voulez-vous vraiment supprimer tous les employés sélectionnés?", + }; + + var norowMessages = { + // ar: "لم يتم تحديد أي صفوف.", + // de: "Es wurden keine Zeilen ausgewählt.", + // es: "No se han seleccionado filas.", + en: "No rows have been selected.", + // fr: "Aucune ligne n'a été sélectionnée.", + }; + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var languageCode = response.language_code; + callback(languageCode); // Pass the language code to the callback + }, + }); + } + $(".all-task-all").change(function (e) { + var is_checked = $(this).is(":checked"); + if (is_checked) { + $(".all-task-all-row").prop("checked", true); + } else { + $(".all-task-all-row").prop("checked", false); + } + }); + + $("#archiveTaskAll").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = archiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-task-all-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + e.preventDefault(); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/task-all-bulk-archive?is_active=False", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); + + +$("#unArchiveTaskAll").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = unarchiveMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-task-all-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "info", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-task-all-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/task-all-bulk-archive?is_active=True", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); + +$("#deleteTaskAll").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = deleteMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-task-all-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-task-all-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/task-all-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); \ No newline at end of file diff --git a/project/static/time_sheet/time_sheet_action.js b/project/static/time_sheet/time_sheet_action.js new file mode 100644 index 000000000..a8801f77a --- /dev/null +++ b/project/static/time_sheet/time_sheet_action.js @@ -0,0 +1,118 @@ +var archiveMessages = { + // ar: "هل ترغب حقًا في أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter archivieren?", + // es: "¿Realmente quieres archivar a todos los empleados seleccionados?", + en: "Do you really want to archive all the selected timesheet?", + // fr: "Voulez-vous vraiment archiver tous les employés sélectionnés ?", + }; + + var unarchiveMessages = { + // ar: "هل ترغب حقًا في إلغاء أرشفة جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter aus der Archivierung zurückholen?", + // es: "¿Realmente quieres desarchivar a todos los empleados seleccionados?", + en: "Do you really want to unarchive all the selected timesheet?", + // fr: "Voulez-vous vraiment désarchiver tous les employés sélectionnés?", + }; + + var deleteMessages = { + // ar: "هل ترغب حقًا في حذف جميع الموظفين المحددين؟", + // de: "Möchten Sie wirklich alle ausgewählten Mitarbeiter löschen?", + // es: "¿Realmente quieres eliminar a todos los empleados seleccionados?", + en: "Do you really want to delete all the selected timesheet?", + // fr: "Voulez-vous vraiment supprimer tous les employés sélectionnés?", + }; + + var norowMessages = { + // ar: "لم يتم تحديد أي صفوف.", + // de: "Es wurden keine Zeilen ausgewählt.", + // es: "No se han seleccionado filas.", + en: "No rows have been selected.", + // fr: "Aucune ligne n'a été sélectionnée.", + }; + + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== "") { + const cookies = document.cookie.split(";"); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === name + "=") { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + function getCurrentLanguageCode(callback) { + $.ajax({ + type: "GET", + url: "/employee/get-language-code/", + success: function (response) { + var languageCode = response.language_code; + callback(languageCode); // Pass the language code to the callback + }, + }); + } + $(".all-time-sheet").change(function (e) { + var is_checked = $(this).is(":checked"); + if (is_checked) { + $(".all-time-sheet-row").prop("checked", true); + } else { + $(".all-time-sheet-row").prop("checked", false); + } + }); + + +$("#deleteTimeSheet").click(function (e) { + e.preventDefault(); + var languageCode = null; + getCurrentLanguageCode(function (code) { + languageCode = code; + var confirmMessage = deleteMessages[languageCode]; + var textMessage = norowMessages[languageCode]; + var checkedRows = $(".all-time-sheet-row").filter(":checked"); + if (checkedRows.length === 0) { + Swal.fire({ + text: textMessage, + icon: "warning", + confirmButtonText: "Close", + }); + } else { + Swal.fire({ + text: confirmMessage, + icon: "error", + showCancelButton: true, + confirmButtonColor: "#008000", + cancelButtonColor: "#d33", + confirmButtonText: "Confirm", + }).then(function (result) { + if (result.isConfirmed) { + var checkedRows = $(".all-time-sheet-row").filter(":checked"); + ids = []; + checkedRows.each(function () { + ids.push($(this).attr("id")); + }); + + $.ajax({ + type: "POST", + url: "/project/time-sheet-bulk-delete", + data: { + csrfmiddlewaretoken: getCookie("csrftoken"), + ids: JSON.stringify(ids), + }, + success: function (response, textStatus, jqXHR) { + if (jqXHR.status === 200) { + location.reload(); // Reload the current page + } else { + // console.log("Unexpected HTTP status:", jqXHR.status); + } + }, + }); + } + }); + } + }); + }); \ No newline at end of file diff --git a/project/templates/dashboard/project_dashboard.html b/project/templates/dashboard/project_dashboard.html new file mode 100644 index 000000000..9a9329f04 --- /dev/null +++ b/project/templates/dashboard/project_dashboard.html @@ -0,0 +1,152 @@ +{% extends 'index.html' %} +{% block content %} +{% load static i18n %} +{% load i18n %} + +
+
+
+
+
+ +
+
+
+ {% trans "Total Projects" %} +
+ +
+
+ +
+
+
+ {% trans "New Projects" %} +
+ +
+
+ +
+
+
+ {% trans "Projects in progress" %} +
+ +
+
+
+ +
+ +
+
+
+ {% trans "Project Status" %} + +
+
+ +
+
+
+ +
+
+
+ {% trans "Task Status" %} + + +
+
+ +
+
+
+ +
+
+ + +
+ +
+
    +
  • +
  • +
  • +
  • +
+
+
+
+ {% trans "Projects due in this month" %} + {% trans "View all" %} +
+
+
    + {% if unexpired_project %} + {% for project in unexpired_project %} +
  • + +
    +
    + Beth Gibbons +
    + {{project.title}} +
    +
    +
  • + {% endfor %} + {% else %} +
    +
    + Page not found. 404. +

    {% trans "No projects due in this month." %}

    +
    +
    + {% endif %} + +
+
+
+
+
+
+ +
+ + + + +{% endblock content %} \ No newline at end of file diff --git a/project/templates/dashboard/project_details.html b/project/templates/dashboard/project_details.html new file mode 100644 index 000000000..8ed3bbc7f --- /dev/null +++ b/project/templates/dashboard/project_details.html @@ -0,0 +1,71 @@ +{% load i18n %} {% load yes_no %} + +
+ +
{{project.title}}
+
+ +
+
+ {% trans "Manager" %} + {{project.manager}} +
+
+ {% trans "Members" %} + {% for member in project.members.all %}{{member}}, {% endfor %} +
+
+
+
+ {% trans "Status" %} + {{project.get_status_display}} +
+
+ {% trans "No of Tasks" %} + {{task_count}} +
+
+
+
+ {% trans "Start Date" %} + {{project.start_date}} +
+
+ {% trans "End date" %} + {{project.end_date}} +
+
+
+
+ {% trans "Document" %} + {{project.document}} +
+
+ {% trans "Description" %} + {{project.description}} +
+
+
+ {% comment %}
{% endcomment %} + + + {% trans "View" %} + + {% comment %}
{% endcomment %} +
+ +
\ No newline at end of file diff --git a/project/templates/project/new/filter_project.html b/project/templates/project/new/filter_project.html new file mode 100644 index 000000000..38c8f3b38 --- /dev/null +++ b/project/templates/project/new/filter_project.html @@ -0,0 +1,44 @@ +{% load i18n %} {% load basefilters %} +{% comment %}
{% endcomment %} +
+
+
{% trans "Project" %}
+
+
+
+
+ + {{f.form.manager}} +
+
+ + {{f.form.status}} +
+
+
+
+ + {{f.form.start_from}} +
+
+ + {{f.form.end_till}} +
+
+ +
+
+
+
+ +{% comment %}
{% endcomment %} \ No newline at end of file diff --git a/project/templates/project/new/forms/project_creation.html b/project/templates/project/new/forms/project_creation.html new file mode 100644 index 000000000..93195a241 --- /dev/null +++ b/project/templates/project/new/forms/project_creation.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ +
{% trans "Project" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/project/templates/project/new/forms/project_update.html b/project/templates/project/new/forms/project_update.html new file mode 100644 index 000000000..5aaf991a3 --- /dev/null +++ b/project/templates/project/new/forms/project_update.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ +
{% trans "Project" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/project/templates/project/new/navbar.html b/project/templates/project/new/navbar.html new file mode 100644 index 000000000..a3e811335 --- /dev/null +++ b/project/templates/project/new/navbar.html @@ -0,0 +1,235 @@ +{% load i18n %} + + + + + +
+
+

{% trans "Projects" %}

+ + + +
+
+ {% if projects %} + {% comment %} for search{% endcomment %} +
+
+ + +
+
+
    + {% comment %} for list view {% endcomment %} +
  • + +
  • + {% comment %} for card view {% endcomment %} +
  • + +
  • +
+
+ {% comment %} for filtering {% endcomment %} +
+ + +
+
+ {% comment %} for actions {% endcomment %} +
+
+ + +
+
+ {% endif %} + {% comment %} for create project {% endcomment %} + +
+
+ diff --git a/project/templates/project/new/overall.html b/project/templates/project/new/overall.html new file mode 100644 index 000000000..a50abbc4f --- /dev/null +++ b/project/templates/project/new/overall.html @@ -0,0 +1,34 @@ +{% extends 'index.html' %} +{% load static %} +{% block content %} + +
+ {% include 'project/new/navbar.html' %} +
+ +
+ + {% if view_type == 'list' %} + {% include 'project/new/project_list_view.html' %} + + {% else %} + {% include 'project/new/project_kanban_view.html' %} + {% endif %} +
+ + + +{% endblock content %} + diff --git a/project/templates/project/new/project_kanban_view.html b/project/templates/project/new/project_kanban_view.html new file mode 100644 index 000000000..0a0118bbf --- /dev/null +++ b/project/templates/project/new/project_kanban_view.html @@ -0,0 +1,201 @@ + +{% load static %} + +{% load i18n %} +{% load basefilters %} + + +{% comment %} for showing messages {% endcomment %} +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} +{% comment %} easy filters {% endcomment %} +
+ + + {% trans "New" %} + + + + {% trans "In progress" %} + + + + {% trans "Completed" %} + + + {% trans "On Hold" %} + + + + {% trans "Cancelled" %} + + + + {% trans "Expired" %} + +
+ + + {% comment %} kanban view {% endcomment %} + {% if projects %} +
+ {% for project in projects %} +
+ + {% comment %} url link {% endcomment %} + + + {% comment %} placing image {% endcomment %} +
+
+ {% if project.image %} + Username + {% else %} + Username + {% endif %} +
+
+ +
+ {{project.title}} + {% trans "Project manager" %} : {{project.manager}}
+ {% trans "Status" %}: {{project.get_status_display}}
+ {% trans "End date" %} : {{project.end_date}} +
+
+
+ + {{ project.task_set.all|length}} + +
+
+ + {% comment %} dropdown {% endcomment %} +
+ + {% comment %} dropdown menu {% endcomment %} + + +
+
+
+ {% endfor %} +
+ {% comment %} pagination {% endcomment %} +
+ + {% trans "Page" %} {{ projects.number }} {% trans "of" %} {{ projects.paginator.num_pages }}. + + +
+{% comment %} {% endcomment %} + +{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available projects; please create a new one." %}

+
+
+{% endif %} + + +{% comment %} js scripts {% endcomment %} + + \ No newline at end of file diff --git a/project/templates/project/new/project_list_view.html b/project/templates/project/new/project_list_view.html new file mode 100644 index 000000000..620872873 --- /dev/null +++ b/project/templates/project/new/project_list_view.html @@ -0,0 +1,236 @@ + +{% load static %} +{% load i18n %} + + +{% comment %} for showing messages {% endcomment %} +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} + +
+ {% if projects %} +
+ {% comment %} easy filters {% endcomment %} +
+ + + {% trans "New" %} + + + + {% trans "In progress" %} + + + + {% trans "Completed" %} + + + {% trans "On Hold" %} + + + + {% trans "Cancelled" %} + + + + {% trans "Expired" %} + +
+ +
+
+
+
+
+
+
+ +
+
+ {% trans "Project" %} +
+
+
+
{% trans "Project Manager" %}
+
{% trans "Project Members" %}
+
{% trans "Status" %}
+
{% trans "Start Date" %}
+
{% trans "End Date" %}
+
{% trans "File" %}
+
{% trans "Description" %}
+
+
+
+ + {% for project in projects %} +
+
+
+ + {{project.title}} +
+
+ +
{{project.manager}}
+
+ {% for employee in project.members.all %} {{employee}}
+ {% endfor %} +
+
{{project.get_status_display}}
+
{{project.start_date}}
+
{% if project.end_date %}{{project.end_date}}{% endif %}
+
{% if project.document %}document{% endif %}
+
{{project.description}}
+ +
+ +
+
+ {% endfor %} + {% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available projects; please create a new one." %}

+
+
+ + {% endif %} + +
+
+
+ {% comment %}
+ + {% trans "Page" %} {{ allowances.number }} {% trans "of" %} {{ + allowances.paginator.num_pages }}. + + +
{% endcomment %} +
+ + +{% comment %} js scripts {% endcomment %} + + + + + \ No newline at end of file diff --git a/project/templates/project_stage/forms/create_project_stage.html b/project/templates/project_stage/forms/create_project_stage.html new file mode 100644 index 000000000..57e837957 --- /dev/null +++ b/project/templates/project_stage/forms/create_project_stage.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ +
{% trans "Project Stage" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/project/templates/project_stage/forms/update_project_stage.html b/project/templates/project_stage/forms/update_project_stage.html new file mode 100644 index 000000000..99c5d4310 --- /dev/null +++ b/project/templates/project_stage/forms/update_project_stage.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ +
{% trans "Project Stage" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }}{{form.errorList}} + +
+
\ No newline at end of file diff --git a/project/templates/task/new/filter_task.html b/project/templates/task/new/filter_task.html new file mode 100644 index 000000000..f4f6abfae --- /dev/null +++ b/project/templates/task/new/filter_task.html @@ -0,0 +1,43 @@ +{% load i18n %} {% load basefilters %} +
+
+
+
{% trans "Task" %}
+
+
+ +
+
+ + {{f.form.task_manager}} +
+
+ + {{f.form.stage}} +
+
+
+
+ + {{f.form.status}} +
+
+ + {{f.form.end_till}} +
+
+ +
+
+
+
+ +
\ No newline at end of file diff --git a/project/templates/task/new/forms/create_task.html b/project/templates/task/new/forms/create_task.html new file mode 100644 index 000000000..6aa48b83e --- /dev/null +++ b/project/templates/task/new/forms/create_task.html @@ -0,0 +1,30 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/project/templates/task/new/forms/create_task_project.html b/project/templates/task/new/forms/create_task_project.html new file mode 100644 index 000000000..19396a24f --- /dev/null +++ b/project/templates/task/new/forms/create_task_project.html @@ -0,0 +1,55 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} +
+
+ + \ No newline at end of file diff --git a/project/templates/task/new/forms/create_timesheet.html b/project/templates/task/new/forms/create_timesheet.html new file mode 100644 index 000000000..58475a47e --- /dev/null +++ b/project/templates/task/new/forms/create_timesheet.html @@ -0,0 +1,38 @@ +{% load i18n %} +
+ +
{% trans "Timesheet" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
diff --git a/project/templates/task/new/forms/update_task.html b/project/templates/task/new/forms/update_task.html new file mode 100644 index 000000000..0e519eeac --- /dev/null +++ b/project/templates/task/new/forms/update_task.html @@ -0,0 +1,55 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ + \ No newline at end of file diff --git a/project/templates/task/new/forms/update_timesheet.html b/project/templates/task/new/forms/update_timesheet.html new file mode 100644 index 000000000..9ad068ce2 --- /dev/null +++ b/project/templates/task/new/forms/update_timesheet.html @@ -0,0 +1,37 @@ +{% load i18n %} +
+ +
{% trans "Timesheet" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
\ No newline at end of file diff --git a/project/templates/task/new/overall.html b/project/templates/task/new/overall.html new file mode 100644 index 000000000..a0fa03e2a --- /dev/null +++ b/project/templates/task/new/overall.html @@ -0,0 +1,144 @@ +{% extends 'index.html' %} + +{% block content %} +{% load i18n %} {% load yes_no %}{% load static %} + +
+ {% include 'task/new/task_navbar.html' %} +
+
+
+ {% if view_type == 'list' %} + {% include 'task/new/task_list_view.html' %} + {% else %} + {% include 'task/new/task_kanban_view.html' %} + {% endif %} +
+ + + + + + + + + + + +{% endblock content %} diff --git a/project/templates/task/new/task_accordion_view.html b/project/templates/task/new/task_accordion_view.html new file mode 100644 index 000000000..a67726c6d --- /dev/null +++ b/project/templates/task/new/task_accordion_view.html @@ -0,0 +1,105 @@ +{% load i18n %} {% load yes_no %} +
+
+
+ {% comment %} +
+
+
+
+
+ +
+
{% trans "Tasks" %}
+
+
+
{% trans "Task Assigner" %}
+
{% trans "Task Members" %}
+
{% trans "End Date" %}
+
{% trans "Stage" %}
+
{% trans "Document" %}
+
{% trans "Description" %}
+
{% trans "Actions" %}
+
+
+ {% endcomment %} + +
+
{{task.title}}
+
{{task.task_assigner}}
+
+ {% for employee in task.task_members.all %} {{employee}}
+ {% endfor %} +
+
{{task.end_date}}
+ {% comment %} +
{{task.stage}}
+ {% endcomment %} +
+ +
+ +
{{task.document}}
+
{{task.description}}
+
+
+
+ + + diff --git a/project/templates/task/new/task_details.html b/project/templates/task/new/task_details.html new file mode 100644 index 000000000..c5c5b1d0d --- /dev/null +++ b/project/templates/task/new/task_details.html @@ -0,0 +1,134 @@ +{% load i18n %} {% load yes_no %} + +
+ +
{{task.title}}
+
+ +
+
+ {% trans "Title" %} + {{task.title}} +
+
+ {% trans "Project" %} + {{task.project}} +
+
+
+
+ {% trans "Stage" %} + {{task.stage}} +
+
+ {% trans "Task manager" %} + {{task.task_manager}} +
+
+
+
+ {% trans "Task members" %} + {% for member in task.task_members.all %}{{member}}, {% endfor %} +
+
+ {% trans "Status" %} + {{task.get_status_display}} +
+
+
+
+ {% trans "End Date" %} + {{task.end_date}} + +
+
+ {% trans "Description" %} + {{task.description}} +
+
+
+
+ {% trans "Document" %} + {{task.document}} + + +
+
+
+ +
+
+ {% comment %} {% endcomment %} + diff --git a/project/templates/task/new/task_kanban_view.html b/project/templates/task/new/task_kanban_view.html new file mode 100644 index 000000000..466665fe3 --- /dev/null +++ b/project/templates/task/new/task_kanban_view.html @@ -0,0 +1,210 @@ + +{% load static %} +{% load i18n %} + + +{% comment %} for showing messages {% endcomment %} +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +
+
+{% include "filter_tags.html" %} + +{% if stages %} + {% comment %} vertical tabs {% endcomment %} +
+
+
+ + {% comment %} stages view {% endcomment %} + {% for stage in stages %} +
+
+
+ {{stage.title}} +
+
+ + {{ stage.tasks.all|length}} + +
+ + {% comment %} drop down menu {% endcomment %} +
+
+
+ + + +
+ + +
+
+ +
+
+
+
+
+ + {% comment %} task inside stage {% endcomment %} +
+ + {% for task in stage.tasks.all|dictsort:"sequence" %} + {% if task in tasks %} +
+
+ +
+
+ task +
+ +
+ + {{task}} + + {{task.task_manager}}
+ {{task.end_date}}
+ {{task.get_status_display}} +
+
+
+ + + {% comment %} drop down inside card {% endcomment %} + +
+ +
+
+ +
+
+
+
+ +
+ {% endif %} + {% endfor %} +
+
+ {% endfor %} + + + {% trans "Stage" %} + + +
+ +
+
+{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available tasks; please create a new one." %}

+
+
+{% endif %} + + + {% comment %} js files {% endcomment %} + + + diff --git a/project/templates/task/new/task_list_view.html b/project/templates/task/new/task_list_view.html new file mode 100644 index 000000000..a26fb644d --- /dev/null +++ b/project/templates/task/new/task_list_view.html @@ -0,0 +1,427 @@ +{% load static %} +{% load i18n %} {% load yes_no %} + + +{% comment %} for showing messages {% endcomment %} +
+
+{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} +{% if stages %} +
+
+
+ {% for stage in stages %} + +
+
+ + + {{ stage.tasks.all|length}} + + {{stage.title}} + +
+
+ +
+
+ +
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ {% trans "Tasks" %} +
+
+
+
{% trans "Task Manager" %}
+
{% trans "End Date" %}
+
{% trans "Status" %}
+
{% trans "Stage" %}
+
{% trans "Document" %}
+
{% trans "Description" %}
+
{% trans "Actions" %}
+
+
+ {% for task in stage.tasks.all %} + + {% comment %} {% if task in tasks %} {% endcomment %} +
+
{{task.title}}
+
{{task.task_manager}}
+
{{task.end_date}}
+
{{task.get_status_display}}
+
+ +
+ +
{{task.document}}
+
{{task.description}}
+
+
+ + + {% comment %} + + {% endcomment %} + + + + +
+
+
+ {% comment %} {% endif %} {% endcomment %} + {% endfor %} +
+
+
+
+ {% endfor %} + + +
+
+
+{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available tasks; please create a new one." %}

+
+
+{% endif %} + + + + + + + + +{% comment %} +
+
+
+ {% for project_stage in project_stages %} {{project_stage.stage}}
+ {% endfor %} + {% for task in tasks %} +
+ {{ task.task_title }} +
+
+ +
+ +
+
+
+
+
+
+
+
+
+
Task Assigner
+
Task Members
+
End Date
+
Status
+
Documents
+
Description
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ Mary Magdalene +
+ {{task.task_assigner_id}} +
+
+
+ {% for employee in task.task_members_id.all %} {{employee}}
+ {% endfor %} +
+
{{task.end_date}}
+
{{task.status}}
+
{{task.document}}
+
{{task.description}}
+
+
+
+ +
+ +
+ {% endfor %} + +
+
+ + + + + + + + {% endcomment %} + +{% comment %} js files {% endcomment %} diff --git a/project/templates/task/new/task_navbar.html b/project/templates/task/new/task_navbar.html new file mode 100644 index 000000000..11e5f5774 --- /dev/null +++ b/project/templates/task/new/task_navbar.html @@ -0,0 +1,104 @@ +{% load i18n %} + +
+ +
+

{{project}} {% trans ":Tasks" %}

+ + + +
+ +
+ {% comment %} for search{% endcomment %} +
+ + +
+ + +
+
    + {% comment %} for list view {% endcomment %} +
  • + + + +
  • + {% comment %} for card view {% endcomment %} +
  • + + +
  • +
+
+ {% comment %} for filtering {% endcomment %} +
+ + +
+ {% comment %} for create task {% endcomment %} + +
+
+ diff --git a/project/templates/task/new/task_timesheet.html b/project/templates/task/new/task_timesheet.html new file mode 100644 index 000000000..06d889b28 --- /dev/null +++ b/project/templates/task/new/task_timesheet.html @@ -0,0 +1,140 @@ +{% load i18n %} {% load yes_no %} {% load static %} + +
+ +
Time Sheet
+
+ {% comment %} for add timesheet {% endcomment %} +
+ +
+ + {% if time_sheets %} +
+
+
+
+
+
+
{% trans "Employee" %}
+
+
+ {% comment %} +
{% trans "Employee" %}
+ {% endcomment %} +
{% trans "Project" %}
+
{% trans "Task" %}
+
{% trans "Date" %}
+
{% trans "Time Spent" %}
+
{% trans "Status" %}
+
{% trans "Description" %}
+
{% trans "Actions" %}
+
+
+ + {% for time_sheet in time_sheets %} +
+
+
+
+ Username +
+ {{time_sheet.employee_id.employee_first_name}} + {{time_sheet.employee_id.employee_last_name|default:""}} + +
+
+
{{time_sheet.project_id.title}}
+ +
{{time_sheet.task_id}}
+
{{time_sheet.date}}
+
{{time_sheet.time_spent}}
+
{{time_sheet.get_status_display}}
+
{{time_sheet.description|truncatechars:15}}
+
+
+ + + + +
+
+
+ {% endfor %} +
+
+ {% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available timesheets; please create a new one." %}

+
+
+ {% endif %} +
+ + + diff --git a/project/templates/task_all/forms/create_project_stage_taskall.html b/project/templates/task_all/forms/create_project_stage_taskall.html new file mode 100644 index 000000000..652f823fa --- /dev/null +++ b/project/templates/task_all/forms/create_project_stage_taskall.html @@ -0,0 +1,88 @@ +{% load i18n %} +
+ +
{% trans "Project Stage" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ \ No newline at end of file diff --git a/project/templates/task_all/forms/create_taskall.html b/project/templates/task_all/forms/create_taskall.html new file mode 100644 index 000000000..6ab8baba9 --- /dev/null +++ b/project/templates/task_all/forms/create_taskall.html @@ -0,0 +1,115 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ +{% comment %} modals for showing new project and new task creation {% endcomment %} + + + + diff --git a/project/templates/task_all/forms/update_taskall.html b/project/templates/task_all/forms/update_taskall.html new file mode 100644 index 000000000..cbde0e180 --- /dev/null +++ b/project/templates/task_all/forms/update_taskall.html @@ -0,0 +1,110 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+{% comment %} modals for showing new project and new task creation {% endcomment %} + + + \ No newline at end of file diff --git a/project/templates/task_all/task_all_card.html b/project/templates/task_all/task_all_card.html new file mode 100644 index 000000000..5b7b9b0c8 --- /dev/null +++ b/project/templates/task_all/task_all_card.html @@ -0,0 +1,157 @@ +{% load i18n %} +{% load static %} +{% if messages %} +
+ {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} + +{% comment %} easy filters {% endcomment %} +
+ + + {% trans "To Do" %} + + + + {% trans "In progress" %} + + + + {% trans "Completed" %} + + + + {% trans "Expired" %} + +
+ +{% if tasks %} +
+ {% for task in tasks %} +
+ + +
+
+ Username +
+
+
+ {{task.title}} + {% trans "Project Name" %}: {{task.project}}
+ {% trans "Stage Name" %} : {{task.stage}}
+ {% trans "End Date" %} : {{task.end_date}} +
+
+
+
+ + +
+
+
+ {% endfor %} +
+ +
+ + {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. + + +
+{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available tasks; please create a new one." %}

+
+
+{% endif %} diff --git a/project/templates/task_all/task_all_filter.html b/project/templates/task_all/task_all_filter.html new file mode 100644 index 000000000..6afb977a7 --- /dev/null +++ b/project/templates/task_all/task_all_filter.html @@ -0,0 +1,48 @@ +{% load i18n %} {% load basefilters %} +
+
+
{% trans "Task" %}
+
+
+
+
+ + {{f.form.task_manager}} +
+
+ + {{f.form.stage}} +
+
+
+
+ + {{f.form.project}} +
+
+ + {{f.form.status}} +
+
+
+
+ + {{f.form.end_till}} +
+
+ + +
+
+
+
+ diff --git a/project/templates/task_all/task_all_list.html b/project/templates/task_all/task_all_list.html new file mode 100644 index 000000000..cb98c6bd0 --- /dev/null +++ b/project/templates/task_all/task_all_list.html @@ -0,0 +1,180 @@ +{% load i18n %} +{% load static %} +{% if messages %} +
+ {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} +{% if tasks %} +
+ {% comment %} easy filters {% endcomment %} +
+ + + {% trans "To Do" %} + + + + {% trans "In progress" %} + + + + {% trans "Completed" %} + + + + {% trans "Expired" %} + +
+
+
+
+
+
+
+
+ +
+
+ {% trans "Task" %} +
+
+
+
{% trans "Project" %}
+
{% trans "Stage" %}
+
{% trans "Mangers" %}
+
{% trans "Members" %}
+
{% trans "End Date" %}
+
{% trans "Status" %}
+
{% trans "Description" %}
+ {% comment %}
{% endcomment %} +
+
+
+ {% for task in tasks %} +
+
+
+
+
+ +
+
+ {{task.title}} +
+
+
+ + {{task.project}} + {{task.stage}} + + {{task.task_manager}} + + {% for member in task.task_members.all %} + {{member}}, + {% endfor %} + + + {{task.end_date}} + {{task.get_status_display}} + {{task.description}} + + {% comment %} + {% if perms.recruitment.view_history %} + + {% endif %} + {% endcomment %} +
+
+ {% comment %} {% if perms.recruitment.change_candidate %} {% endcomment %} + + {% comment %} {% endif %} {% endcomment %} + {% comment %} {% if perms.recruitment.delete_candidate %} {% endcomment %} +
+ {% csrf_token %} + +
+ {% comment %} {% endif %} {% endcomment %} +
+
+ + +
+ +
+ {% endfor %} +
+
+
+ {% comment %} pagination {% endcomment %} +
+ + {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. + + +
+{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available tasks; please create a new one." %}

+
+
+{% endif %} + + \ No newline at end of file diff --git a/project/templates/task_all/task_all_navbar.html b/project/templates/task_all/task_all_navbar.html new file mode 100644 index 000000000..8480c0f6c --- /dev/null +++ b/project/templates/task_all/task_all_navbar.html @@ -0,0 +1,168 @@ +{% load i18n %} + +
+ +
+

{% trans "Tasks" %}

+ + + +
+ +
+ {% if tasks %} +
+ {% comment %} for search{% endcomment %} + +
+ + +
+ + +
+
    + {% comment %} for list view {% endcomment %} +
  • + + + +
  • + {% comment %} for card view {% endcomment %} +
  • + + +
  • +
+
+ {% comment %} for filtering {% endcomment %} +
+ + +
+
+ + {% comment %} for actions {% endcomment %} +
+
+ + +
+
+ + {% endif %} + {% comment %} for create {% endcomment %} + +
+
+ + \ No newline at end of file diff --git a/project/templates/task_all/task_all_overall.html b/project/templates/task_all/task_all_overall.html new file mode 100644 index 000000000..3dd46e3c6 --- /dev/null +++ b/project/templates/task_all/task_all_overall.html @@ -0,0 +1,123 @@ +{% extends 'index.html' %} +{% load static %} +{% block content %} + +
+ {% include 'task_all/task_all_navbar.html' %} + +
+ +
+ {% if view_type == 'list' %} + {% include 'task_all/task_all_list.html' %} + {% else %} + {% include 'task_all/task_all_card.html' %} + {% endif %} +
+ + + + + + + + + +{% endblock content %} + diff --git a/project/templates/task_all/update_task.html b/project/templates/task_all/update_task.html new file mode 100644 index 000000000..f5e6d72f9 --- /dev/null +++ b/project/templates/task_all/update_task.html @@ -0,0 +1,42 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ + \ No newline at end of file diff --git a/project/templates/time_sheet/chart.html b/project/templates/time_sheet/chart.html new file mode 100644 index 000000000..97e8011cb --- /dev/null +++ b/project/templates/time_sheet/chart.html @@ -0,0 +1,252 @@ +{% extends 'index.html' %}{% block content %}{% load static %} {% load i18n %} +{% load basefilters %} {% if request.user.employee_get.id == emp_id or perms.project.view_timesheet or request.user|is_reportingmanager %} +
+
+
+
+ Personal Timesheet of {{emp_name|capfirst}} +
+
+
+ + + +
+
+
+ +
+
+{% else %} {% include '404.html' %} {% endif %} + + + + +{% endblock %} diff --git a/project/templates/time_sheet/filters.html b/project/templates/time_sheet/filters.html new file mode 100644 index 000000000..94a0b12d2 --- /dev/null +++ b/project/templates/time_sheet/filters.html @@ -0,0 +1,66 @@ +{% load i18n %} {% load basefilters %} +
+
+
+
{% trans "Time Sheet" %}
+
+
+
+
+ + {{f.form.project_id}} +
+
+ + {{f.form.status}} +
+
+ +
+
+ + {{f.form.task}} +
+
+ + {{f.form.date}} +
+
+ {% if perms.project.view_timesheet or request.user|is_reportingmanager %} +
+
+ + {{f.form.employee_id}} +
+
+ {% endif %} +
+
+
+ +
+
{% trans "Advanced" %}
+
+
+
+
+ + {{f.form.start_from}} +
+
+
+
+ + {{f.form.end_till}} +
+
+
+
+
+
+ +
diff --git a/project/templates/time_sheet/form-create.html b/project/templates/time_sheet/form-create.html new file mode 100644 index 000000000..e5ab2117d --- /dev/null +++ b/project/templates/time_sheet/form-create.html @@ -0,0 +1,126 @@ +{% block content %} {% load static %} {% load i18n %} +
+ +
{% trans "Time Sheet" %}
+
+
+ +
+ {% csrf_token %} {{form.as_p}} + +
+
+ + + + +{% endblock content %} diff --git a/project/templates/time_sheet/form-update.html b/project/templates/time_sheet/form-update.html new file mode 100644 index 000000000..03bbf530e --- /dev/null +++ b/project/templates/time_sheet/form-update.html @@ -0,0 +1,98 @@ +{% block content %} {% load static %} {% load i18n %} +
+ +
{% trans "Time Sheet" %}
+
+
+ {% comment %} +
+ {% csrf_token %} {{form.as_p}} + +
+
+ +{% endblock content %} diff --git a/project/templates/time_sheet/form_project_time_sheet.html b/project/templates/time_sheet/form_project_time_sheet.html new file mode 100644 index 000000000..f5f2d53e1 --- /dev/null +++ b/project/templates/time_sheet/form_project_time_sheet.html @@ -0,0 +1,92 @@ +{% load i18n %} +
+ +
{% trans "Project" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ \ No newline at end of file diff --git a/project/templates/time_sheet/form_task_time_sheet.html b/project/templates/time_sheet/form_task_time_sheet.html new file mode 100644 index 000000000..04e0ea5a2 --- /dev/null +++ b/project/templates/time_sheet/form_task_time_sheet.html @@ -0,0 +1,110 @@ +{% load i18n %} +
+ +
{% trans "Task" %}
+
+
+ +
+ {% csrf_token %} {{ form.as_p }} + +
+
+ \ No newline at end of file diff --git a/project/templates/time_sheet/time_sheet_card_view.html b/project/templates/time_sheet/time_sheet_card_view.html new file mode 100644 index 000000000..c2c50d52b --- /dev/null +++ b/project/templates/time_sheet/time_sheet_card_view.html @@ -0,0 +1,149 @@ +{% load i18n %} +{% load static %} +{% load basefilters %} +{% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+{% endif %} +{% include "filter_tags.html" %} +{% comment %} easy filters {% endcomment %} +
+ + + {% trans "In progress" %} + + + + {% trans "Completed" %} + +
+{% if time_sheets %} +
+ {% for time_sheet in time_sheets %} +
+ +
+
+ Username +
+
+ +
+ {{time_sheet.employee_id}} + {{time_sheet.date}}
+ {{time_sheet.project_id}}
+ {{time_sheet.task_id}} | + {% trans "Time Spent" %} : {{time_sheet.time_spent}} +
+
+
+
+ + +
+
+
+ {% endfor %} +
+ + +
+ + {% trans "Page" %} {{ time_sheets.number }} {% trans "of" %} {{ time_sheets.paginator.num_pages }}. + + +
+ {% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available timesheets; please create a new one." %}

+
+
+{% endif %} \ No newline at end of file diff --git a/project/templates/time_sheet/time_sheet_list_view.html b/project/templates/time_sheet/time_sheet_list_view.html new file mode 100644 index 000000000..ef0cabe8b --- /dev/null +++ b/project/templates/time_sheet/time_sheet_list_view.html @@ -0,0 +1,188 @@ +{% load i18n %} {% load yes_no %} {% load static %} +{% include "filter_tags.html" %} + +{% if time_sheets %} +
+ {% comment %} easy filters {% endcomment %} +
+ + + {% trans "In progress" %} + + + + {% trans "Completed" %} + +
+ {% comment %} table of contents {% endcomment %} +
+
+
+
+
+
+
+ +
+
{% trans "Employee" %}
+
+
+ {% comment %} +
{% trans "Employee" %}
+ {% endcomment %} +
{% trans "Project" %}
+
{% trans "Task" %}
+
{% trans "Date" %}
+
{% trans "Time Spent" %}
+
{% trans "Status" %}
+
{% trans "Description" %}
+
{% trans "Actions" %}
+
+
+ + {% for time_sheet in time_sheets %} +
+
+
+
+ +
+
+
+ Username +
+ {{time_sheet.employee_id.employee_first_name}} + {{time_sheet.employee_id.employee_last_name|default:""}} + +
+
+
+
{{time_sheet.project_id.title}}
+
{{time_sheet.task_id}}
+
{{time_sheet.date}}
+
{{time_sheet.time_spent}}
+
{{time_sheet.get_status_display}}
+
{{time_sheet.description|truncatechars:15}}
+
+ +
+
+ {% endfor %} +
+
+
+ +
+ + {% trans "Page" %} {{ time_sheets.number }} {% trans "of" %} {{ time_sheets.paginator.num_pages }}. + + +
+{% else %} +
+
+ Page not found. 404. +

{% trans "There are currently no available timesheets; please create a new one." %}

+
+
+{% endif %} + + \ No newline at end of file diff --git a/project/templates/time_sheet/time_sheet_navbar.html b/project/templates/time_sheet/time_sheet_navbar.html new file mode 100644 index 000000000..bcc11ce5d --- /dev/null +++ b/project/templates/time_sheet/time_sheet_navbar.html @@ -0,0 +1,144 @@ +{% load i18n %} +{% load basefilters %} +
+
+

{% trans "Time Sheet" %}

+ + + +
+ {% if perms.project.view_timesheet or request.user|is_reportingmanager %} +
+
+ + +
+ {% endif %} +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+ +
+ + +
+ {% comment %} for actions {% endcomment %} +
+
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/project/templates/time_sheet/time_sheet_single_view.html b/project/templates/time_sheet/time_sheet_single_view.html new file mode 100644 index 000000000..c190dd4d0 --- /dev/null +++ b/project/templates/time_sheet/time_sheet_single_view.html @@ -0,0 +1,94 @@ +{% load i18n %} {% load yes_no %} {% load basefilters %} +
+ +
Timesheet Details
+
+ +
+
+ {% trans "Employee" %} + {{time_sheet.employee_id}} +
+
+ {% trans "Project" %} + {{time_sheet.project_id}} +
+
+
+
+ {% trans "Task" %} + {{time_sheet.task_id}} +
+
+ {% trans "Date" %} + {{time_sheet.date}} +
+
+
+
+ {% trans "Time Spent" %} + {{time_sheet.time_spent}} +
+
+ {% trans "Status" %} + {{time_sheet.status}} +
+
+
+
+ {% trans "Description" %} + {{time_sheet.description}} +
+
+
+
+ + {% trans "Edit" %} + + {% if perms.project.view_timesheet or request.user|is_reportingmanager %} + + {% trans "View Timesheet Chart" %} + + {% endif %} + + {% trans "Delete" %} + +
+
+
+ + diff --git a/project/templates/time_sheet/time_sheet_view.html b/project/templates/time_sheet/time_sheet_view.html new file mode 100644 index 000000000..7ffef8cec --- /dev/null +++ b/project/templates/time_sheet/time_sheet_view.html @@ -0,0 +1,65 @@ +{% extends 'index.html' %} +{% block content %} + {% load i18n %} + {% load basefilters %} + +
+ {% include 'time_sheet/time_sheet_navbar.html' %} + +
+ {% if view_type == "card" %} + {% include 'time_sheet/time_sheet_card_view.html' %} + {% else %} + {% include 'time_sheet/time_sheet_list_view.html' %} + {% endif %} +
+
+ + + +{% endblock content %} diff --git a/project/tests.py b/project/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/project/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/project/urls.py b/project/urls.py new file mode 100644 index 000000000..1ff5a8bd3 --- /dev/null +++ b/project/urls.py @@ -0,0 +1,117 @@ +from django.urls import path + +from project.models import Project +from . import views +urlpatterns = [ + # Dashboard + path('project-dashboard-view',views.dashboard_view,name='project-dashboard-view'), + path('project-status-chart',views.project_status_chart,name='project-status-chart'), + path('task-status-chart',views.task_status_chart,name='task-status-chart'), + path( + 'project-detailed-view//', + views.project_detailed_view, + name='project-detailed-view' + ), + + # Project + path('project-view/',views.project_view,name='project-view'), + path("create-project", views.create_project, name="create-project"), + path( + "update-project//", + views.project_update, + name="update-project" + ), + path("delete-project//", views.project_delete, name="delete-project"), + path("project-filter", views.project_filter, name="project-filter"), + path("project-import", views.project_import, name="project-import"), + path("project-bulk-export",views.project_bulk_export,name="project-bulk-export"), + path("project-bulk-archive",views.project_bulk_archive,name="project-bulk-archive"), + path("project-bulk-delete",views.project_bulk_delete,name="project-bulk-delete"), + path('project-archive//',views.project_archive,name='project-archive'), + + + # Task + path('task-view//',views.task_view,name='task-view',kwargs={"model":Project}), + path('create-task//',views.create_task,name='create-task'), + path('create-task-in-project//',views.create_task_in_project,name='create-task-in-project'), + path('update-task//',views.update_task,name='update-task'), + path('delete-task//',views.delete_task,name='delete-task'), + path('task-details//',views.task_details,name='task-details'), + path('task-filter//',views.task_filter,name='task-filter'), + path('task-stage-change',views.task_stage_change,name='task-stage-change'), + path('task-timesheet//',views.task_timesheet,name='task-timesheet'), + path("create-timesheet-task//",views.create_timesheet_task,name="create-timesheet-task"), + path("update-timesheet-task//",views.update_timesheet_task,name="update-timesheet-task"), + path('drag-and-drop-task',views.drag_and_drop_task,name='drag-and-drop-task'), + + # Task-all + path('task-all',views.task_all,name='task-all'), + path('create-task-all',views.task_all_create,name='create-task-all'), + path('update-task-all//',views.update_task_all,name='update-task-all'), + path('task-all-filter/',views.task_all_filter,name='task-all-filter'), + path("task-all-bulk-archive",views.task_all_bulk_archive,name="task-all-bulk-archive"), + path("task-all-bulk-delete",views.task_all_bulk_delete,name="task-all-bulk-delete"), + path('task-all-archive//',views.task_all_archive,name='task-all-archive'), + + + + # Project stage + path('create-project-stage//',views.create_project_stage,name='create-project-stage'), + path('update-project-stage//',views.update_project_stage,name='update-project-stage'), + path('delete-project-stage//',views.delete_project_stage,name='delete-project-stage'), + path('get-stages',views.get_stages,name="get-stages"), + path('create-stage-taskall',views.create_stage_taskall,name='create-stage-taskall'), + path('drag-and-drop-stage',views.drag_and_drop_stage,name='drag-and-drop-stage'), + + + # Timesheet + path("view-time-sheet", views.time_sheet_view, name="view-time-sheet"), + path("create-time-sheet", views.time_sheet_creation, name="create-time-sheet"), + path( + "update-time-sheet//", + views.time_sheet_update, + name="update-time-sheet", + ), + path( + "delete-time-sheet-ajax//", + views.time_sheet_delete_ajax, + name="delete-time-sheet-ajax", + ), + path("filter-time-sheet", views.time_sheet_filter, name="filter-time-sheet"), + path("time-sheet-initial", views.time_sheet_initial, name="time-sheet-initial"), + + path("view-time-sheet", views.time_sheet_view, name="view-time-sheet"), + path("create-time-sheet", views.time_sheet_creation, name="create-time-sheet"), + path( + "create-project-time-sheet", + views.time_sheet_project_creation, + name="create-project-time-sheet", + ), + path( + "create-task-time-sheet", + views.time_sheet_task_creation, + name="create-task-time-sheet", + ), + path( + "update-time-sheet//", + views.time_sheet_update, + name="update-time-sheet", + ), + path( + "delete-time-sheet//", + views.time_sheet_delete, + name="delete-time-sheet", + ), + path("filter-time-sheet", views.time_sheet_filter, name="filter-time-sheet"), + path("time-sheet-initial", views.time_sheet_initial, name="time-sheet-initial"), + path( + "personal-time-sheet-view//", + views.personal_time_sheet_view, + name="personal-time-sheet-view", + ), + path("personal-time-sheet/", views.personal_time_sheet, name="personal-time-sheet"), + path("view-single-time-sheet/", views.time_sheet_single_view, name="view-single-time-sheet"), + path('time-sheet-bulk-delete',views.time_sheet_bulk_delete,name="time-sheet-bulk-delete"), + + +] diff --git a/project/views.py b/project/views.py new file mode 100644 index 000000000..e23679c03 --- /dev/null +++ b/project/views.py @@ -0,0 +1,1489 @@ +import calendar +from collections import defaultdict +import datetime +from urllib.parse import parse_qs +from django.shortcuts import render, redirect +from django.http import HttpResponse,JsonResponse,HttpResponseRedirect +from django.urls import reverse +import pandas as pd +from horilla.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.template.loader import render_to_string +from django.contrib import messages +from .forms import * +from .models import * +from .decorator import * +from .methods import is_projectmanager_or_member_or_perms, is_task_manager,is_task_member +from .filters import TimeSheetFilter, ProjectFilter, TaskFilter,TaskAllFilter +from django.core.exceptions import ValidationError +import json +from django.core.paginator import Paginator +import calendar +import datetime +from django.core import serializers +import json +import xlsxwriter +from horilla.decorators import hx_request_required + +from project.methods import strtime_seconds , paginator_qry, generate_colors, time_sheet_delete_permissions, time_sheet_update_permissions +from base.methods import filtersubordinates, get_key_instances + + +# Create your views here. +# Dash board view + +@login_required +def dashboard_view(request): + """ + Dashboard view of project + Returns: + it will redirect to dashboard. + """ + + # Get the current date + today = datetime.date.today() + # Find the last day of the current month + last_day = calendar.monthrange(today.year, today.month)[1] + # Construct the last date of the current month + last_date = datetime.date(today.year, today.month, last_day) + + total_projects= Project.objects.all().count() + new_projects= Project.objects.filter(status='new').count() + projects_in_progress= Project.objects.filter(status='in_progress').count() + date_range = {'end_till':last_date} + projects_due_in_this_month = ProjectFilter(date_range).qs + unexpired_project =[] + for project in projects_due_in_this_month: + if project.status != 'expired': + unexpired_project.append(project) + + context = { + "total_projects":total_projects, + "new_projects":new_projects, + "projects_in_progress":projects_in_progress, + 'unexpired_project':unexpired_project + } + return render(request,'dashboard/project_dashboard.html',context=context) + +@login_required +def project_status_chart(request): + """ + This method is used generate project dataset for the dashboard + """ + initial_data = [] + data_set = [] + choices = Project.PROJECT_STATUS + labels = [type[1] for type in choices] + for label in choices: + initial_data.append( + { + "label": label[1], + "data": [], + } + ) + + for status in choices: + count = Project.objects.filter(status=status[0]).count() + data = [] + for index, label in enumerate(initial_data): + if status[1] == initial_data[index]['label']: + data.append(count) + else: + data.append(0) + data_set.append( + { + "label": status[1], + "data": data, + } + ) + return JsonResponse({"dataSet":data_set, "labels": labels}) + + +@login_required +def task_status_chart(request): + """ + This method is used generate project dataset for the dashboard + """ + # projects = Project.objects.all() + initial_data = [] + data_set = [] + choices = Task.TASK_STATUS + labels = [type[1] for type in choices] + for label in choices: + initial_data.append( + { + "label": label[1], + "data": [], + } + ) + # for status in choices: + # count = Project.objects.filter(status=status[0]).count() + + for status in choices: + count = Task.objects.filter(status=status[0]).count() + data = [] + for index, label in enumerate(initial_data): + if status[1] == initial_data[index]['label']: + data.append(count) + else: + data.append(0) + data_set.append( + { + "label": status[1], + "data": data, + } + ) + return JsonResponse({"dataSet":data_set, "labels": labels}) + +@login_required +def project_detailed_view(request,project_id): + project = Project.objects.get(id=project_id) + task_count = project.task_set.count() + context ={ + 'project':project, + "task_count":task_count, + } + return render(request,'dashboard/project_details.html',context=context) + +# Project views + +@login_required +@is_projectmanager_or_member_or_perms(perm="project.view_project") +def project_view(request): + """ + Overall view of project, the default view + """ + form = ProjectFilter() + view_type = 'card' + if request.GET.get('view') == 'list': + view_type = 'list' + projects = Project.objects.all() + if request.GET.get("search") is not None: + projects = ProjectFilter(request.GET).qs + previous_data = request.environ['QUERY_STRING'] + page_number = request.GET.get('page') + context = {'view_type':view_type, + 'projects':paginator_qry(projects,page_number), + "pd": previous_data, + "f": form, + } + return render(request,'project/new/overall.html', context) + + +@permission_required(perm="project.add_project") +@login_required +def create_project(request): + """ + For creating new project + """ + form = ProjectForm() + if request.method == "POST": + form = ProjectForm(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request,_('New project created')) + response = render(request, + "project/new/forms/project_creation.html", + context={"form": form}, + ) + + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, "project/new/forms/project_creation.html", context={"form": form}) + + +@login_required +@project_update_permission() +def project_update(request, project_id): + """ + Update an existing project. + + Args: + request: The HTTP request object. + project_id: The ID of the project to update. + + Returns: + If the request method is POST and the form is valid, redirects to the project overall view. + Otherwise, renders the project update form. + + """ + project = Project.objects.get(id=project_id) + project_form = ProjectForm(instance=project) + if request.method == "POST": + project_form = ProjectForm(request.POST, request.FILES, instance=project) + if project_form.is_valid(): + project_form.save() + messages.success(request, _("Project updated")) + response = render( + request, "project/new/forms/project_update.html", {"form": project_form , 'project_id' : project_id} + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "project/new/forms/project_update.html", + { + "form": project_form, 'project_id' : project_id + }, + ) + + +@login_required +@project_delete_permission() +def project_delete(request, project_id): + """ + For deleting existing project + """ + view_type = request.GET.get('view') + project_view_url = reverse('project-view') + redirected_url = f'{project_view_url}?view={view_type}' + Project.objects.get(id=project_id).delete() + + return redirect(redirected_url) + + +@login_required +def project_filter(request): + """ + For filtering projects + """ + projects = ProjectFilter(request.GET).qs + templete = 'project/new/project_kanban_view.html' + if request.GET.get('view') == 'list': + templete = 'project/new/project_list_view.html' + previous_data = request.environ['QUERY_STRING'] + page_number = request.GET.get('page') + filter_obj = projects + data_dict = parse_qs(previous_data) + get_key_instances(Project, data_dict) + context = { + 'projects':paginator_qry(projects,page_number), + "pd": previous_data, + "f": filter_obj, + "filter_dict": data_dict, + + } + return render(request, templete,context) + + +def convert_nan(field, dicts): + """ + This method is returns None or field value + """ + field_value = dicts.get(field) + try: + float(field_value) + return None + except ValueError: + return field_value + + +@login_required +def project_import(request): + """ + This method is used to import Project instances and creates related objects + """ + data_frame = pd.DataFrame( + columns=["Title", "Manager Badge id","Member Badge id","Status", "Start Date", "End Date", "Description"] + ) + # Export the DataFrame to an Excel file + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = 'attachment; filename="project_template.xlsx"' + data_frame.to_excel(response, index=False) + + if request.method == "POST" and request.FILES.get("file") is not None: + file = request.FILES["file"] + data_frame = pd.read_excel(file) + project_dicts = data_frame.to_dict("records") + error_lists = [] + for project in project_dicts: + try: + # getting datas from imported file + title = project['Title'] + manager_badge_id = convert_nan("Manager Badge id", project) + member_badge_id = convert_nan("Member Badge id", project) + status = project['Status'] + start_date = project["Start Date"] + end_date = project["End Date"] + description = project['Description'] + + # checcking all the imported values + is_save = True + # getting employee using badge id, for manager + if manager_badge_id : + if Employee.objects.filter(badge_id = manager_badge_id).exists(): + manager = Employee.objects.filter(badge_id = manager_badge_id).first() + else: + project ["Manager error"] = f"{manager_badge_id} - This badge not exist" + is_save = False + + # getting employee using badge id, for member + if member_badge_id: + ids = member_badge_id.split(',') + error_ids = [] + employees = [] + for id in ids: + if Employee.objects.filter(badge_id = id).exists(): + employee = Employee.objects.filter(badge_id = id).first() + employees.append(employee) + else: + error_ids.append(id) + is_save = False + if error_ids: + ids = ','.join(map(str, error_ids)) + project ["Member error"] = f"{ids} - This id not exists" + + if status: + if status not in [stat for stat, _ in Project.PROJECT_STATUS]: + project ["Status error"] = f'{status} not available in Project status' + is_save = False + else: + project ["Status error"] = 'Status is a required field' + is_save = False + + format = '%Y-%m-%d' + if start_date: + + # using try-except to check for truth value + try: + res = bool(datetime.datetime.strptime(start_date.strftime("%Y-%m-%d"), format)) + except Exception as e: + res = False + if res == False : + project["Start date error"] = "Date must be in 'YYYY-MM-DD' format" + is_save = False + else: + project["Start date error"] = "Start date is a required field" + is_save = False + + + if end_date: + # using try-except to check for truth value + try: + res = bool(datetime.datetime.strptime(end_date.strftime("%Y-%m-%d"), format)) + if end_date < start_date: + project["end date error"] = "End date must be greater than Start date" + is_save = False + except ValueError: + res = False + if res == False : + project["end date error"] = "Date must be in 'YYYY-MM-DD' format" + is_save = False + + if is_save == True : + # creating new project + if Project.objects.filter(title=title).exists(): + project_obj = Project.objects.filter(title=title).first() + else: + project_obj = Project(title=title) + project_obj.start_date = start_date.strftime("%Y-%m-%d") + project_obj.end_date = end_date.strftime("%Y-%m-%d") + project_obj.manager=manager + project_obj.status = status + project_obj.description = description + project_obj.save() + for member in employees: + project_obj.members.add(member) + project_obj.save() + else: + error_lists.append(project) + + except Exception as e: + error_lists.append(project) + if error_lists: + res = defaultdict(list) + for sub in error_lists: + for key in sub: + res[key].append(sub[key]) + data_frame = pd.DataFrame(error_lists, columns=error_lists[0].keys()) + # Create an HTTP response object with the Excel file + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = 'attachment; filename="ImportError.xlsx"' + data_frame.to_excel(response, index=False) + return response + return HttpResponse("Imported successfully") + return response + + + +@login_required +# @permission_required("employee.delete_employee") +# @require_http_methods(["POST"]) +def project_bulk_export(request): + """ + This method is used to export bulk of Project instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + data_list=[] + # Add headers to the worksheet + headers = ["Title", "Manager","Members", "Status", "Start Date", "End Date", "Description"] + + # Get the list of field names for your model + for project_id in ids: + project = Project.objects.get(id=project_id) + data = { + "Title":f"{project.title}", + "Manager":f"{project.manager.employee_first_name + ' ' + project.manager.employee_last_name if project.manager else ''}", + "Members":f"{',' .join([member.employee_first_name + ' ' + member.employee_last_name for member in project.members.all()]) if project.members.exists() else ''}", + "Status":f'{project.status}', + "Start Date":f'{project.start_date.strftime("%Y-%m-%d")}', + "End Date":f'{project.end_date.strftime("%Y-%m-%d") if project.end_date else ""}', + "Description":f'{project.description}', + } + data_list.append(data) + data_frame = pd.DataFrame(data_list, columns=headers) + # Export the DataFrame to an Excel file + response = HttpResponse(content_type="application/ms-excel") + response["Content-Disposition"] = 'attachment; filename="project details.xlsx"' + writer = pd.ExcelWriter(response, engine="xlsxwriter") + # data_frame.to_excel(response, index=False) + data_frame.to_excel( + writer, + sheet_name="Project details", + index=False, + startrow=3 , + ) + workbook = writer.book + worksheet = writer.sheets["Project details"] + max_columns = len(data) + heading_format = workbook.add_format( + { "bg_color": "#ffd0cc", + "bold": True, + "font_size": 14, + "align": "center", + "valign": "vcenter", + "font_size": 20, + } + ) + header_format = workbook.add_format({ + "bg_color": "#EDF1FF", + "bold": True, + "text_wrap": True, + "font_size": 12, + "align": "center", + "border": 1 , + }) + worksheet.set_row(0, 30) + worksheet.merge_range( + 0, + 0, + 0, + max_columns - 1, + "Project details ", + heading_format, + ) + for col_num, value in enumerate(data_frame.columns.values): + worksheet.write(3, col_num, value, header_format) + col_letter = chr(65 + col_num) + header_width = max(len(value) + 2, len(data_frame[value].astype(str).max()) + 2) + worksheet.set_column(f"{col_letter}:{col_letter}", header_width) + + # worksheet.set_row(4, 30) + + writer.close() + + + return response + +@login_required +# @permission_required("employee.delete_employee") +# @require_http_methods(["POST"]) +def project_bulk_archive(request): + """ + This method is used to archive bulk of Project instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + is_active = False + if request.GET.get("is_active") == "True": + is_active = True + for project_id in ids: + project = Project.objects.get(id=project_id) + project.is_active = is_active + project.save() + message = _("archived") + if is_active: + message = _("un-archived") + messages.success(request, f"{project} is {message}") + return JsonResponse({"message": "Success"}) + +@login_required +# @permission_required("employee.delete_employee") +def project_bulk_delete(request): + """ + This method is used to delete set of Employee instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for project_id in ids: + project = Project.objects.get(id=project_id) + try: + project.delete() + messages.success( + request, _("%(project)s deleted.") % {"project": project} + ) + except Exception as error: + messages.error(request, error) + messages.error( + request, _("You cannot delete %(project)s.") % {"project": project} + ) + + return JsonResponse({"message": "Success"}) + +@login_required +# @permission_required("employee.delete_employee") +def project_archive(request,project_id): + """ + This method is used to archive project instance + Args: + project_id : Project instance id + """ + project = Project.objects.get(id=project_id) + project.is_active = not project.is_active + project.save() + message = _(f"{project} un-archived") + if not project.is_active: + message = _(f"{project} archived") + messages.success(request, message) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +# Task views + +@login_required +@project_update_permission() +def task_view(request,project_id,**kwargs): + """ + For showing tasks + """ + form = TaskAllFilter() + view_type = 'card' + project = Project.objects.get(id=project_id) + stages = ProjectStage.objects.filter(project = project).order_by('sequence') + tasks = Task.objects.filter(project=project) + if request.GET.get('view') == 'list': + view_type = 'list' + context = {'view_type':view_type, + 'tasks':tasks, + 'stages':stages, + 'project_id':project_id, + "project":project, + "f":form, + } + return render(request,'task/new/overall.html',context) + +@login_required +def create_task(request,stage_id): + """ + For creating new task in project view + """ + project_stage = ProjectStage.objects.get(id = stage_id) + project = project_stage.project + if ( + request.user.employee_get == project.manager or + request.user.has_perm('project.delete_project') + ): + form = TaskForm(initial={'project': project}) + if request.method == 'POST': + form = TaskForm(request.POST, request.FILES) + if form.is_valid(): + instance = form.save(commit=False) + instance.stage = project_stage + instance.save() + + messages.success(request,_('New task created')) + response = render(request, + "task/new/forms/create_task.html", + context={"form": form,'stage_id':stage_id}, + ) + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, "task/new/forms/create_task.html", context={"form": form, 'stage_id':stage_id}) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + +@login_required +def create_task_in_project(request,project_id): + """ + For creating new task in project view + """ + project = Project.objects.get(id=project_id) + stages = project.project_stages.all() + + # Serialize the queryset to JSON + + serialized_data = serializers.serialize('json', stages) + if ( + request.user.employee_get == project.manager or + request.user.has_perm('project.delete_project') + ): + form = TaskFormCreate(initial={'project': project}) + if request.method == 'POST': + form = TaskFormCreate(request.POST, request.FILES) + if form.is_valid(): + form.save() + messages.success(request,_('New task created')) + response = render(request, + "task/new/forms/create_task_project.html", + context={"form": form,'project_id':project_id}, + ) + return HttpResponse(response.content.decode("utf-8") + "") + context ={"form": form, + 'project_id':project_id, + 'stages':serialized_data, + } + return render(request, "task/new/forms/create_task_project.html", context=context) + messages.info(request,'You dont have permission.') + return HttpResponseRedirect(request. META. get('HTTP_REFERER', '/')) + + + +@login_required +@task_update_permission() +def update_task(request, task_id): + """ + For updating task in project view + """ + + task = Task.objects.get(id=task_id) + project = task.project + task_form = TaskForm(instance=task) + if request.method == "POST": + task_form = TaskForm(request.POST, request.FILES, instance=task) + if task_form.is_valid(): + task_form.save() + messages.success(request, _("Task updated")) + response = render(request, "task/new/forms/update_task.html", {"form": task_form, 'task_id':task_id}) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "task/new/forms/update_task.html", + { + "form": task_form, + 'task_id': task_id, + }, + ) + + +@login_required +@task_update_permission() +def delete_task(request, task_id): + """ + For delete task + """ + task = Task.objects.get(id=task_id) + project_id = task.project.id + task.delete() + view_type = request.GET.get('view') + # Build the URL for task_view with query parameters + task_view_url = reverse('task-view', args=[project_id]) + if request.GET.get("task_all"): + task_view_url = reverse('task-all') + + redirected_url = f'{task_view_url}?view={view_type}' + + return redirect(redirected_url) + +@login_required +def task_details(request,task_id): + """ + For showing all details about task + """ + task = Task.objects.get(id=task_id) + return render(request,'task/new/task_details.html',context={'task':task}) + +@login_required +@project_update_permission() +def task_filter(request, project_id): + """ + For filtering task + """ + view_type = 'card' + templete = 'task/new/task_kanban_view.html' + if request.GET.get('view') == 'list': + view_type = 'list' + templete ='task/new/task_list_view.html' + tasks = TaskFilter(request.GET).qs + stages = ProjectStage.objects.filter(project = project_id).order_by('sequence') + previous_data = request.environ['QUERY_STRING'] + data_dict = parse_qs(previous_data) + get_key_instances(Task, data_dict) + + context = {"tasks": tasks, + 'stages':stages, + 'view_type':view_type, + 'project_id':project_id, + "filter_dict": data_dict, + } + return render(request,templete,context) + +@login_required +def task_stage_change(request): + """ + This method is used to change the current stage of a task + """ + task_id = request.POST['task'] + stage_id = request.POST['stage'] + stage = ProjectStage.objects.get(id=stage_id) + Task.objects.filter(id=task_id).update(stage=stage) + return JsonResponse({ + 'type': 'success', + 'message': _('Task stage updated'), + }) + + +@login_required +def task_timesheet(request,task_id): + """ + For showing all timesheet related to task + """ + task = Task.objects.get(id=task_id) + time_sheets=task.task_timesheet.all() + # time_sheets = [] + context={'time_sheets':time_sheets, + 'task_id':task_id} + return render(request, "task/new/task_timesheet.html",context=context,) + +@login_required +def create_timesheet_task(request,task_id): + task=Task.objects.get(id=task_id) + project = task.project + form = TimesheetInTaskForm(initial={'project_id':project,'task_id':task}) + if request.method == 'POST': + form = TimesheetInTaskForm(request.POST) + if form.is_valid(): + form.save() + messages.success(request, _("Timesheet created")) + response = render(request, "task/new/forms/create_timesheet.html", {"form": form,'task_id':task_id}) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + context = {'form':form, + 'task_id':task_id, + } + return render(request,'task/new/forms/create_timesheet.html',context=context) + +@login_required +def update_timesheet_task(request,timesheet_id): + timesheet = TimeSheet.objects.get(id=timesheet_id) + form = TimesheetInTaskForm(instance = timesheet) + if request.method == 'POST': + form = TimesheetInTaskForm(request.POST,instance=timesheet) + if form.is_valid(): + form.save() + messages.success(request, _("Timesheet updated")) + response = render(request, "task/new/forms/update_timesheet.html", {"form": form,'timesheet_id':timesheet_id}) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + context = {'form':form, + 'timesheet_id':timesheet_id, + } + return render(request,'task/new/forms/update_timesheet.html',context=context) + +@login_required +def drag_and_drop_task(request): + """ + For drag and drop task into new stage + """ + updated_stage_id = request.POST['updated_stage_id'] + previous_task_id = request.POST['previous_task_id'] + previous_stage_id = request.POST['previous_stage_id'] + change = False + task = Task.objects.get(id=previous_task_id) + project=task.project + if (request.user.has_perm('project.change_task') or + request.user.has_perm('project.change_project') or + request.user.employee_get == task.task_manager or + request.user.employee_get in task.task_members.all() or + request.user.employee_get == project.manager or + request.user.employee_get in project.members.all() + ): + if previous_stage_id != updated_stage_id: + task.stage=ProjectStage.objects.get(id = updated_stage_id) + task.save() + change = True + sequence = json.loads(request.POST['sequence']) + for key, val in sequence.items(): + if Task.objects.get(id=key).sequence != val: + Task.objects.filter(id=key).update(sequence=val) + change = True + return JsonResponse({'type': 'success','message':_('Task stage updated'),'change':change}) + change = True + return JsonResponse({'type': 'info','message':_('You dont have permission.'),'change':change}) + + +# Task all views + +@login_required +def task_all(request): + """ + For showing all task + """ + form = TaskAllFilter() + view_type='card' + tasks= TaskAllFilter(request.GET).qs + if request.GET.get('view') == 'list': + view_type = 'list' + context = { + "tasks":paginator_qry(tasks,request.GET.get('page')), + "pd":request.GET.urlencode(), + "f":form, + "view_type":view_type, + } + return render(request,'task_all/task_all_overall.html',context=context) + + +@login_required +def task_all_create(request): + """ + For creating new task in task all view + """ + form = TaskAllForm() + if request.method == 'POST': + form = TaskAllForm(request.POST, request.FILES) + if form.is_valid(): + task = form.save(commit=False) + task.save() + messages.success(request,_('New task created')) + response = render(request, + "task_all/forms/create_taskall.html", + context={"form": form,}, + ) + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, "task_all/forms/create_taskall.html", context={"form": form,}) + +@login_required +def update_task_all(request,task_id): + task = Task.objects.get(id=task_id) + form = TaskAllForm(instance=task) + if request.method == 'POST': + form = TaskAllForm(request.POST,request.FILES,instance=task) + if form.is_valid(): + form.save() + messages.success(request,_('Task updated successfully')) + response = render( + request, + "task_all/forms/update_taskall.html", + context={"form":form, + 'task_id':task_id}, + ) + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, "task_all/forms/update_taskall.html", context={"form": form,'task_id':task_id}) + + +@login_required +def task_all_filter(request): + """ + For filtering tasks in task all view + """ + view_type = 'card' + templete = 'task_all/task_all_card.html' + if request.GET.get('view') == 'list': + view_type = 'list' + templete ='task_all/task_all_list.html' + + tasks = TaskAllFilter(request.GET).qs + page_number = request.GET.get('page') + previous_data = request.environ['QUERY_STRING'] + data_dict = parse_qs(previous_data) + get_key_instances(Task, data_dict) + # tasks = tasks.filter(project_id=project_id) + + context = {"tasks":paginator_qry(tasks,page_number), + 'view_type':view_type, + "pd":previous_data, + "filter_dict": data_dict, + } + return render(request,templete,context) + +@login_required +# @permission_required("employee.delete_employee") +# @require_http_methods(["POST"]) +def task_all_bulk_archive(request): + """ + This method is used to archive bulk of Task instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + is_active = False + if request.GET.get("is_active") == "True": + is_active = True + for task_id in ids: + task = Task.objects.get(id=task_id) + task.is_active = is_active + task.save() + message = _("archived") + if is_active: + message = _("un-archived") + messages.success(request, f"{task} is {message}") + return JsonResponse({"message": "Success"}) + +@login_required +# @permission_required("employee.delete_employee") +def task_all_bulk_delete(request): + """ + This method is used to delete set of Task instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for task_id in ids: + task = Task.objects.get(id=task_id) + try: + task.delete() + messages.success( + request, _("%(task)s deleted.") % {"task": task} + ) + except Exception as error: + messages.error(request, error) + messages.error( + request, _("You cannot delete %(task)s.") % {"task": task} + ) + + return JsonResponse({"message": "Success"}) + +@login_required +# @permission_required("employee.delete_employee") +def task_all_archive(request,task_id): + """ + This method is used to archive project instance + Args: + task_id : Task instance id + """ + task = Task.objects.get(id=task_id) + task.is_active = not task.is_active + task.save() + message = _(f"{task} un-archived") + if not task.is_active: + message = _(f"{task} archived") + messages.success(request, message) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + + +# Project stage views +@login_required +@project_delete_permission() +@hx_request_required +def create_project_stage(request,project_id): + """ + For create project stage + """ + project = Project.objects.get(id = project_id) + form = ProjectStageForm(initial = {'project': project}) + if request.method == 'POST': + form = ProjectStageForm(request.POST,) + if form.is_valid() : + instance = form.save(commit=False) + instance.save() + context = {'form':form, 'project_id':project_id} + + messages.success(request,_('New project stage created')) + response = render(request, + 'project_stage/forms/create_project_stage.html', + context, + ) + return HttpResponse(response.content.decode("utf-8")+ "") + context = {'form':form,'project_id':project_id} + return render(request,'project_stage/forms/create_project_stage.html',context) + + +@login_required +@project_stage_update_permission() +def update_project_stage(request,stage_id): + """ + For update project stage + """ + stage = ProjectStage.objects.get(id = stage_id) + form = ProjectStageForm(instance=stage) + if request.method == 'POST': + form =ProjectStageForm(request.POST,instance=stage) + if form.is_valid(): + form.save() + messages.success(request,_('Project stage updated successfully')) + response = render(request, + "project_stage/forms/update_project_stage.html", + context={'form':form,"stage_id":stage_id}) + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, + "project_stage/forms/update_project_stage.html", + context={'form':form, 'stage_id':stage_id}) + + +@login_required +@project_stage_delete_permission() +def delete_project_stage(request,stage_id): + """ + For delete project stage + """ + view_type = request.GET.get('view') + stage = ProjectStage.objects.get(id=stage_id) + project_id = stage.project.id + task_view_url = reverse('task-view', args=[project_id]) + redirected_url = f'{task_view_url}?view={view_type}' + + stage.delete() + + return redirect(redirected_url) + +@login_required +def get_stages(request): + """ + This is an ajax method to return json response to take only stages related + to the project in the task-all form fields + """ + project_id = request.GET["project_id"] + stages = ProjectStage.objects.filter(project=project_id).values("title", "id") + return JsonResponse({"data": list(stages)}) + +@login_required +def create_stage_taskall(request): + """ + This is an ajax method to return json response to create stage related + to the project in the task-all form fields + """ + if request.method == 'GET': + project_id = request.GET["project_id"] + project = Project.objects.get(id=project_id) + form = ProjectStageForm(initial = {'project':project}) + if request.method == 'POST': + form = ProjectStageForm(request.POST) + if form.is_valid(): + instance = form.save() + return JsonResponse({"id": instance.id, "name": instance.title}) + errors = form.errors.as_json() + return JsonResponse({"errors": errors}) + return render( + request, + "task_all/forms/create_project_stage_taskall.html", + context={"form": form}, + ) + + + +@login_required +def drag_and_drop_stage(request): + """ + For drag and drop project stage into new sequence + """ + sequence = request.POST['sequence'] + sequence = json.loads(sequence) + stage_id = (list(sequence.keys())[0]) + project = ProjectStage.objects.get(id=stage_id).project + change = False + if (request.user.has_perm('project.change_project') or + request.user.employee_get == project.manager or + request.user.employee_get in project.members.all() + ): + for key, val in sequence.items(): + if val != ProjectStage.objects.get(id=key).sequence: + change = True + ProjectStage.objects.filter(id=key).update(sequence=val) + return JsonResponse({'type': 'success','message':_('Stage sequence updated',),'change':change}) + change = True + return JsonResponse({'type': 'info','message':_('You dont have permission.'),'change':change}) + +# Time sheet views + +@permission_required(perm='project.view_timesheet') +@login_required +def time_sheet_view(request): + """ + View function to display time sheets based on user permissions. + + If the user is a superuser, all time sheets will be shown. + Otherwise, only the time sheets for the current user will be displayed. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + HttpResponse: The rendered HTTP response displaying the time sheets. + """ + form = TimeSheetFilter() + view_type= "card" + if request.GET.get("view")=="list": + view_type='list' + time_sheet_filter = TimeSheetFilter(request.GET).qs + time_sheet_filter = filtersubordinates(request,time_sheet_filter,"project.view_timesheet" ) + time_sheet_filter=list(time_sheet_filter) + for item in TimeSheet.objects.filter(employee_id=request.user.employee_get): + if item not in time_sheet_filter: + time_sheet_filter.append(item) + + time_sheets = paginator_qry(time_sheet_filter, request.GET.get("page")) + context = { + "time_sheets": time_sheets, + "f": form, + "view_type":view_type + } + return render( + request, + "time_sheet/time_sheet_view.html", + context=context, + ) + + +@login_required +def time_sheet_creation(request): + """ + View function to handle the creation of a new time sheet. + + If the request method is POST and the submitted form is valid, + a new time sheet will be created and saved. + + Parameters: + request (HttpRequest): The HTTP request object. + + Returns: + HttpResponse: The rendered HTTP response displaying the form or + redirecting to a new page after successful time sheet creation. + """ + user = request.user.employee_get + form = TimeSheetForm(initial={"employee_id": user}, request=request) + # form = TimeSheetForm(initial={"employee_id": user}) + if request.method == "POST": + form = TimeSheetForm(request.POST, request.FILES, request=request) + if form.is_valid(): + form.save() + messages.success(request, _("Time sheet created")) + response = render( + request, "time_sheet/form-create.html", context={"form": form} + ) + return HttpResponse(response.content.decode("utf-8") + "") + return render(request, "time_sheet/form-create.html", context={"form": form}) + +@login_required +def time_sheet_project_creation(request): + """ + View function to handle the creation of a new project from time sheet form. + + If the request method is POST and the submitted form is valid, + a new project will be created and saved. + + Returns: + HttpResponse or JsonResponse: Depending on the request type, it returns + either an HTTP response rendering the form or a JSON response with the + created project ID and name in case of successful creation, + or the validation errors in case of an invalid form submission. + """ + form = ProjectTimeSheetForm() + if request.method == "POST": + form = ProjectTimeSheetForm(request.POST, request.FILES) + if form.is_valid(): + instance = form.save() + return JsonResponse({"id": instance.id, "name": instance.title}) + errors = form.errors.as_json() + return JsonResponse({"errors": errors}) + return render( + request, "time_sheet/form_project_time_sheet.html", context={"form": form} + ) + + +@login_required +def time_sheet_task_creation(request): + """ + View function to handle the creation of a new task from time sheet form. + + If the request method is GET, it initializes the task form with the + provided project ID as an initial value. + If the request method is POST and the submitted form is valid, + a new task time sheet will be created and saved. + + Returns: + HttpResponse or JsonResponse: Depending on the request type, it returns + either an HTTP response rendering the form or a JSON response with the + created task time sheet's ID and name in case of successful creation, + or the validation errors in case of an invalid form submission. + """ + if request.method == "GET": + project_id = request.GET["project_id"] + project = Project.objects.get(id = project_id) + stages = ProjectStage.objects.filter(project__id=project_id) + task_form = TaskTimeSheetForm(initial={"project": project}) + task_form.fields["stage"].queryset = stages + + if request.method == "POST": + task_form = TaskTimeSheetForm(request.POST, request.FILES) + if task_form.is_valid(): + instance = task_form.save() + return JsonResponse({"id": instance.id, "name": instance.title}) + errors = task_form.errors.as_json() + return JsonResponse({"errors": errors}) + return render( + request, + "time_sheet/form_task_time_sheet.html", + context={"form": task_form, + 'project_id':project_id + }, + ) + + +@login_required +def time_sheet_update(request, time_sheet_id): + """ + Update an existing time sheet. + + Args: + request: The HTTP request object. + time sheet_id: The ID of the time sheet to update. + + Returns: + If the request method is POST and the form is valid, redirects to the time sheet view. + Otherwise, renders the time sheet update form. + + """ + if time_sheet_update_permissions(request,time_sheet_id): + time_sheet = TimeSheet.objects.get(id=time_sheet_id) + update_form = TimeSheetForm(instance=time_sheet, request=request) + update_form.fields["task_id"].queryset = time_sheet.project_id.task_set.all() + + if request.method == "POST": + update_form = TimeSheetForm(request.POST, instance=time_sheet) + + + if update_form.is_valid(): + update_form.save() + messages.success(request, _("Time sheet updated")) + form = TimeSheetForm() + response = render( + request, "./time_sheet/form-create.html", context={"form": form} + ) + return HttpResponse( + response.content.decode("utf-8") + "" + ) + return render( + request, + "./time_sheet/form-update.html", + { + "form": update_form, + }, + ) + else: + return render (request,"error.html") + + +@login_required +def time_sheet_delete(request, time_sheet_id): + """ + View function to handle the deletion of a time sheet. + + Parameters: + request (HttpRequest): The HTTP request object. + time_sheet_id (int): The ID of the time sheet to be deleted. + + Returns: + HttpResponseRedirect: A redirect response to the time sheet view page. + """ + if time_sheet_delete_permissions(request,time_sheet_id): + TimeSheet.objects.get(id=time_sheet_id).delete() + view_type = "list" + if request.GET.get("view")=="card": + view_type="card" + return redirect('/project/view-time-sheet'+"?view="+view_type) + else: + return render (request,"error.html") + +@login_required +def time_sheet_delete_ajax(request, time_sheet_id): + """ + View function to handle the deletion of a time sheet. + + Parameters: + request (HttpRequest): The HTTP request object. + time_sheet_id (int): The ID of the time sheet to be deleted. + + Returns: + JsonResponse: A success message after deleting timeshhet or a info message if the user don't have the permission to delete . + """ + if time_sheet_delete_permissions(request,time_sheet_id): + TimeSheet.objects.get(id=time_sheet_id).delete() + return JsonResponse({'type': 'success','message':_('Timesheet deleted successfully.')}) + return JsonResponse({'type': 'info','message':_("You don't have permission."),}) + + +def time_sheet_filter(request): + """ + Filter Time sheet based on the provided query parameters. + + Args: + request: The HTTP request object containing the query parameters. + + Returns: + Renders the Time sheet list template with the filtered Time sheet. + + """ + emp_id = request.user.employee_get.id + filtered_time_sheet = TimeSheetFilter(request.GET).qs + + time_sheet_filter = filtersubordinates(request,filtered_time_sheet,"project.view_timesheet" ) + if filtered_time_sheet.filter(employee_id__id =emp_id).exists(): + time_sheet_filter=list(time_sheet_filter) + for item in TimeSheet.objects.filter(employee_id=request.user.employee_get): + if item not in time_sheet_filter: + time_sheet_filter.append(item) + time_sheets = paginator_qry(time_sheet_filter, request.GET.get("page")) + previous_data = request.environ['QUERY_STRING'] + data_dict = parse_qs(previous_data) + get_key_instances(TimeSheet, data_dict) + view_type = request.GET.get("view") + template = "time_sheet/time_sheet_list_view.html" + if view_type == "card": + template="time_sheet/time_sheet_card_view.html" + elif view_type== "chart": + return redirect("personal-time-sheet-view"+"?view="+emp_id) + return render( + request, + template, + { + "time_sheets": time_sheets, + "filter_dict": data_dict, + }, + ) + + +@login_required +def time_sheet_initial(request): + """ + This is an ajax method to return json response to take only tasks related + to the project in the timesheet form fields + """ + project_id = request.GET["project_id"] + tasks = Task.objects.filter(project=project_id).values("title", "id") + return JsonResponse({"data": list(tasks)}) + + +def personal_time_sheet(request): + """ + This is an ajax method to return json response for generating bar charts to employees. + """ + emp_id = request.GET["emp_id"] + selected = request.GET["selected"] + month_number =request.GET["month"] + year = request.GET["year"] + week_number = request.GET["week"] + + time_spent = [] + dataset = [] + + projects = Project.objects.filter(project_timesheet__employee_id=emp_id).distinct() + + time_sheets = TimeSheet.objects.filter(employee_id=emp_id).order_by("date") + + time_sheets=time_sheets.filter(date__week=week_number) + + # check for labels to be genarated weeky or monthly + if selected == "week": + start_date = datetime.date.fromisocalendar(int(year), int(week_number), 1) + + date_list = [] + labels = [] + for i in range(7): + day = start_date + datetime.timedelta(days=i) + date_list.append(day) + day=day.strftime("%d-%m-%Y %A") + labels.append(day) + + elif selected == "month": + days_in_month = calendar.monthrange(int(year), int(month_number)+1)[1] + start_date = datetime.datetime(int(year), int(month_number)+1, 1).date() + labels = [] + date_list = [] + for i in range(days_in_month): + day = start_date + datetime.timedelta(days=i) + date_list.append(day) + day=day.strftime("%d-%m-%Y") + labels.append(day) + colors = generate_colors(len(projects)) + + for project, color in zip(projects, colors): + dataset.append( + { + "label": project.title, + "data": [], + "backgroundColor": color, + } + ) + + # Calculate total hours for each project on each date + total_hours_by_project_and_date = defaultdict(lambda: defaultdict(float)) + + #addding values to the response + for label in date_list: + time_sheets = TimeSheet.objects.filter(employee_id=emp_id, date=label) + for time in time_sheets: + time_spent = strtime_seconds(time.time_spent) / 3600 + total_hours_by_project_and_date[time.project_id.title][label] += time_spent + for data in dataset: + project_title = data["label"] + data["data"] = [total_hours_by_project_and_date[project_title][label] for label in date_list] + + response = { + "dataSet": dataset, + "labels": labels, + } + return JsonResponse(response) + + +def personal_time_sheet_view(request,emp_id): + """ + Function for viewing the barcharts for timesheet of a specific employee. + + Args: + emp_id: id of the employee whose barchat to be rendered. + + Returns: + Renders the chart.html template containing barchat of the specific employee. + + """ + try: + Employee.objects.get(id=emp_id) + except : + return render (request,"error.html") + emp_last_name = Employee.objects.get(id=emp_id).employee_last_name if Employee.objects.get(id=emp_id).employee_last_name!=None else "" + employee_name = f"{Employee.objects.get(id=emp_id).employee_first_name} {emp_last_name}" + context = { + "emp_id" :emp_id, + "emp_name":employee_name, + } + + return render(request,"time_sheet/chart.html",context=context) + + +def time_sheet_single_view(request,time_sheet_id): + """ + Renders a single timesheet view page. + + Parameters: + - request (HttpRequest): The HTTP request object. + - time_sheet_id (int): The ID of the timesheet to view. + + Returns: + The rendered timesheet single view page. + + """ + timesheet = TimeSheet.objects.get(id=time_sheet_id) + context = {"time_sheet": timesheet} + return render(request, "time_sheet/time_sheet_single_view.html", context) + +def time_sheet_bulk_delete(request): + """ + This method is used to delete set of Task instances + """ + ids = request.POST["ids"] + ids = json.loads(ids) + for timesheet_id in ids: + timesheet = TimeSheet.objects.get(id=timesheet_id) + try: + timesheet.delete() + messages.success( + request, _("%(timesheet)s deleted.") % {"timesheet": timesheet} + ) + except Exception as error: + messages.error(request, error) + messages.error( + request, _("You cannot delete %(timesheet)s.") % {"timesheet": timesheet} + ) + return JsonResponse({"message": "Success"}) diff --git a/static/images/ui/project.png b/static/images/ui/project.png new file mode 100644 index 000000000..8b58a217e Binary files /dev/null and b/static/images/ui/project.png differ diff --git a/static/images/ui/project/brief.png b/static/images/ui/project/brief.png new file mode 100644 index 000000000..53a926bd6 Binary files /dev/null and b/static/images/ui/project/brief.png differ diff --git a/static/images/ui/project/document.png b/static/images/ui/project/document.png new file mode 100644 index 000000000..a6bf60346 Binary files /dev/null and b/static/images/ui/project/document.png differ diff --git a/static/images/ui/project/project.png b/static/images/ui/project/project.png new file mode 100644 index 000000000..5024f76f6 Binary files /dev/null and b/static/images/ui/project/project.png differ diff --git a/static/images/ui/project/project_sidebar.png b/static/images/ui/project/project_sidebar.png new file mode 100644 index 000000000..a82b0631e Binary files /dev/null and b/static/images/ui/project/project_sidebar.png differ diff --git a/static/images/ui/project/task.png b/static/images/ui/project/task.png new file mode 100644 index 000000000..fc7423b7c Binary files /dev/null and b/static/images/ui/project/task.png differ diff --git a/static/images/ui/project/timesheet.png b/static/images/ui/project/timesheet.png new file mode 100644 index 000000000..4751f3fde Binary files /dev/null and b/static/images/ui/project/timesheet.png differ diff --git a/static/images/ui/project/waterfall.png b/static/images/ui/project/waterfall.png new file mode 100644 index 000000000..40cb5752a Binary files /dev/null and b/static/images/ui/project/waterfall.png differ