From 509aceeacdb4a3031aa9e1d92171b97ed8917342 Mon Sep 17 00:00:00 2001 From: Horilla Date: Thu, 13 Feb 2025 17:15:29 +0530 Subject: [PATCH] [ADD] PROJECT: Project management module for Horilla (beta) --- horilla/horilla_apps.py | 3 + project/__init__.py | 0 project/admin.py | 7 + project/apps.py | 12 + project/decorator.py | 138 ++ project/filters.py | 162 ++ project/forms.py | 259 +++ project/methods.py | 196 +++ project/migrations/__init__.py | 0 project/models.py | 249 +++ project/sidebar.py | 105 ++ project/static/dashboard/projectChart.js | 89 + project/static/project/import.js | 252 +++ project/static/project/project_action.js | 408 +++++ project/static/project/project_view.js | 25 + project/static/project/task_pipeline.js | 138 ++ project/static/task_all/task_all_action.js | 220 +++ .../static/time_sheet/time_sheet_action.js | 118 ++ .../dashboard/project_dashboard.html | 152 ++ .../templates/dashboard/project_details.html | 71 + .../templates/project/new/filter_project.html | 44 + .../project/new/forms/project_creation.html | 30 + .../project/new/forms/project_update.html | 30 + project/templates/project/new/navbar.html | 235 +++ project/templates/project/new/overall.html | 34 + .../project/new/project_kanban_view.html | 201 +++ .../project/new/project_list_view.html | 236 +++ .../forms/create_project_stage.html | 30 + .../forms/update_project_stage.html | 30 + project/templates/task/new/filter_task.html | 43 + .../templates/task/new/forms/create_task.html | 30 + .../task/new/forms/create_task_project.html | 55 + .../task/new/forms/create_timesheet.html | 38 + .../templates/task/new/forms/update_task.html | 55 + .../task/new/forms/update_timesheet.html | 37 + project/templates/task/new/overall.html | 144 ++ .../task/new/task_accordion_view.html | 105 ++ project/templates/task/new/task_details.html | 134 ++ .../templates/task/new/task_kanban_view.html | 210 +++ .../templates/task/new/task_list_view.html | 427 +++++ project/templates/task/new/task_navbar.html | 104 ++ .../templates/task/new/task_timesheet.html | 140 ++ .../forms/create_project_stage_taskall.html | 88 + .../task_all/forms/create_taskall.html | 115 ++ .../task_all/forms/update_taskall.html | 110 ++ project/templates/task_all/task_all_card.html | 157 ++ .../templates/task_all/task_all_filter.html | 48 + project/templates/task_all/task_all_list.html | 180 ++ .../templates/task_all/task_all_navbar.html | 168 ++ .../templates/task_all/task_all_overall.html | 123 ++ project/templates/task_all/update_task.html | 42 + project/templates/time_sheet/chart.html | 252 +++ project/templates/time_sheet/filters.html | 66 + project/templates/time_sheet/form-create.html | 126 ++ project/templates/time_sheet/form-update.html | 98 ++ .../time_sheet/form_project_time_sheet.html | 92 + .../time_sheet/form_task_time_sheet.html | 110 ++ .../time_sheet/time_sheet_card_view.html | 149 ++ .../time_sheet/time_sheet_list_view.html | 188 +++ .../time_sheet/time_sheet_navbar.html | 144 ++ .../time_sheet/time_sheet_single_view.html | 94 ++ .../templates/time_sheet/time_sheet_view.html | 65 + project/tests.py | 3 + project/urls.py | 117 ++ project/views.py | 1489 +++++++++++++++++ static/images/ui/project.png | Bin 0 -> 13360 bytes static/images/ui/project/brief.png | Bin 0 -> 6598 bytes static/images/ui/project/document.png | Bin 0 -> 7573 bytes static/images/ui/project/project.png | Bin 0 -> 21381 bytes static/images/ui/project/project_sidebar.png | Bin 0 -> 16344 bytes static/images/ui/project/task.png | Bin 0 -> 12948 bytes static/images/ui/project/timesheet.png | Bin 0 -> 16418 bytes static/images/ui/project/waterfall.png | Bin 0 -> 5213 bytes 73 files changed, 9020 insertions(+) create mode 100644 project/__init__.py create mode 100644 project/admin.py create mode 100644 project/apps.py create mode 100644 project/decorator.py create mode 100644 project/filters.py create mode 100644 project/forms.py create mode 100644 project/methods.py create mode 100644 project/migrations/__init__.py create mode 100644 project/models.py create mode 100644 project/sidebar.py create mode 100644 project/static/dashboard/projectChart.js create mode 100644 project/static/project/import.js create mode 100644 project/static/project/project_action.js create mode 100644 project/static/project/project_view.js create mode 100644 project/static/project/task_pipeline.js create mode 100644 project/static/task_all/task_all_action.js create mode 100644 project/static/time_sheet/time_sheet_action.js create mode 100644 project/templates/dashboard/project_dashboard.html create mode 100644 project/templates/dashboard/project_details.html create mode 100644 project/templates/project/new/filter_project.html create mode 100644 project/templates/project/new/forms/project_creation.html create mode 100644 project/templates/project/new/forms/project_update.html create mode 100644 project/templates/project/new/navbar.html create mode 100644 project/templates/project/new/overall.html create mode 100644 project/templates/project/new/project_kanban_view.html create mode 100644 project/templates/project/new/project_list_view.html create mode 100644 project/templates/project_stage/forms/create_project_stage.html create mode 100644 project/templates/project_stage/forms/update_project_stage.html create mode 100644 project/templates/task/new/filter_task.html create mode 100644 project/templates/task/new/forms/create_task.html create mode 100644 project/templates/task/new/forms/create_task_project.html create mode 100644 project/templates/task/new/forms/create_timesheet.html create mode 100644 project/templates/task/new/forms/update_task.html create mode 100644 project/templates/task/new/forms/update_timesheet.html create mode 100644 project/templates/task/new/overall.html create mode 100644 project/templates/task/new/task_accordion_view.html create mode 100644 project/templates/task/new/task_details.html create mode 100644 project/templates/task/new/task_kanban_view.html create mode 100644 project/templates/task/new/task_list_view.html create mode 100644 project/templates/task/new/task_navbar.html create mode 100644 project/templates/task/new/task_timesheet.html create mode 100644 project/templates/task_all/forms/create_project_stage_taskall.html create mode 100644 project/templates/task_all/forms/create_taskall.html create mode 100644 project/templates/task_all/forms/update_taskall.html create mode 100644 project/templates/task_all/task_all_card.html create mode 100644 project/templates/task_all/task_all_filter.html create mode 100644 project/templates/task_all/task_all_list.html create mode 100644 project/templates/task_all/task_all_navbar.html create mode 100644 project/templates/task_all/task_all_overall.html create mode 100644 project/templates/task_all/update_task.html create mode 100644 project/templates/time_sheet/chart.html create mode 100644 project/templates/time_sheet/filters.html create mode 100644 project/templates/time_sheet/form-create.html create mode 100644 project/templates/time_sheet/form-update.html create mode 100644 project/templates/time_sheet/form_project_time_sheet.html create mode 100644 project/templates/time_sheet/form_task_time_sheet.html create mode 100644 project/templates/time_sheet/time_sheet_card_view.html create mode 100644 project/templates/time_sheet/time_sheet_list_view.html create mode 100644 project/templates/time_sheet/time_sheet_navbar.html create mode 100644 project/templates/time_sheet/time_sheet_single_view.html create mode 100644 project/templates/time_sheet/time_sheet_view.html create mode 100644 project/tests.py create mode 100644 project/urls.py create mode 100644 project/views.py create mode 100644 static/images/ui/project.png create mode 100644 static/images/ui/project/brief.png create mode 100644 static/images/ui/project/document.png create mode 100644 static/images/ui/project/project.png create mode 100644 static/images/ui/project/project_sidebar.png create mode 100644 static/images/ui/project/task.png create mode 100644 static/images/ui/project/timesheet.png create mode 100644 static/images/ui/project/waterfall.png 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 %} + + {% 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 0000000000000000000000000000000000000000..8b58a217e542417f9ca086843897a9f3c889611a GIT binary patch literal 13360 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 literal 0 HcmV?d00001 diff --git a/static/images/ui/project/brief.png b/static/images/ui/project/brief.png new file mode 100644 index 0000000000000000000000000000000000000000..53a926bd65ceb6781497c96a29b709918ded369e GIT binary patch literal 6598 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 PeE^8-z1H1zrJVdX_CZq& literal 0 HcmV?d00001 diff --git a/static/images/ui/project/document.png b/static/images/ui/project/document.png new file mode 100644 index 0000000000000000000000000000000000000000..a6bf60346a1fef6b0f73d0570a0f63d6e4d4ce73 GIT binary patch literal 7573 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 Ua}+%aYf|9wLF*rLEQnYB7do$O?*IS* literal 0 HcmV?d00001 diff --git a/static/images/ui/project/project.png b/static/images/ui/project/project.png new file mode 100644 index 0000000000000000000000000000000000000000..5024f76f6f9f67a173c71d197576e96dfd4830de GIT binary patch literal 21381 zcmb@uc{r3`{6Bopj9vD9*F>@}Wfv-B2_d^OObBDkzRyHu3E9h95y>`Fwg{7Ll2Eb? zW0@$DeaX)JZlCY(_dNeS*KwQbSX?C57fu8{Y045_tJxc(9 zg1{GbZFYIKVZ{EDKxn*jbz?{8=y2ms7or{9olX=VYikS|EzF38Hy zJt)-Wz8er48Y<`M>m7)6@pqH+yYG>|s>KfgqJWX!m0Mv2%Tu8rg{(&?8-q-qr5WGO zvOWFQazS5Tg8}|#+v^G&q(b{aLIP15u0?0}^!(5j-KWznX!NshR9DgQPvdV5twx{N z?P^cPM-HlA^d4N6oY`HfcWz&h2z4JV2vmCSJ~-{&m|h!<@xd^ml`+<6HazPK=K)+o zf}zSh>>{QDFO3hxTj6ix-*0qetWMll>v6|l!585f)CJA;^pcjC$Wr916}t;n z5LDw(Q&^AD*DF2HKqq7TfHl3;*W6d9-sn&{(LABA(_v$)D7QsNV;;NGS_Vf$myw}l z?Vv3lHnw(a>^L;?dTPmLj=yg#oj=<*9bFkX91cWd29XzOgwq5~+``#GNgd7IvvBxO zrZ&C?--mCXePn6sNdQo^lkr7I`g&rHTj)ZJCfK{OCp-E#M#92EgpIA2AXZiObqNQn z$S92iNNAJq{my!No9_ix^lw^yg&7~5oUoGr$UxcH*gO%+2rZk-H=%|ZrYTfCkiNT| z$>0K>%f!^9CBY6Jh}8nAS5#x!*b+VX$VB3m;eG{_Jt0-s2ktMo?kSTmP%u*g3iQw* z$9Bw-V+V=xEwMMcjR#H4+O9Oouj(OsqdIxm(i3vJYL0)<1S&8#o2SR6SM)e!w3 zVPI9qo@m2n@ovoKy=lzYNZww1)phI`9UN}y*NKgMu-o>|1}y)czSmYdhOzi?9oz){ zdbrB#!9YKCrQJ8)f25+EhkW-RgR4iqYag|nQ@P3VA?A+wF;9?ld>FVjf~(@k$8J&a zVcu>E-^>o=|DcbqakHgosG+xab3+h@Cuhe92Bnny^T-jHzMfu$64Y@;ekIy{?&w-g zs(UtYS#^EE;)4Mj+v3zMvN~ZW;&afw9NJpW_qs^F&<2-k>?8xkqR#vBfO% z37xAx!SgPwHgfN%2OY0I+L~VTJ>&^@eVJvu&yOi_hNm)KpaZ~t)+`iAPoi=?N((J0%sga%eM zo+Sn*H`bka)}?}ZNc;Iaxj$!Z2-7~wH2NMO9g03pM+EGG@sqs{b79Qle|z04&W*QEV-Kd;9@@u+4ZzKq6497 zFM)+0kSDYa%t2fSp< zz?NMJ1*OOD9&TAeZ{Ealpb3J6!FRWf4kUUks1w@DL(cd5@_PC_2#)K~o|&G|Ct8JC z6kI(-<@)zazVGs$Whv;fE{g^bcYu4nh4o; zWg#;RnU=`>RToN3Vf;Mw2_H?&di_d+RUp2}Vp&%sJVstxVvkfWMCi>3W%bormfY4z z(qz0#kNR~~n|Cyoq~68ukTLV(SJfYS-3g3RIFW1bTGC2VloqpD)V3|>?@ABu1H$m7GG%l+(|$aSS(}*+97tYKiKH>rJh03yKxPfo?EIBQL)=BIq`pWsAwZ(xfy)c zrdKJ^4g2A^GM+_3HD6`VPv@&FLp#M4c2xQz3y8<@Ri_!qMzHASA`NwbYL?OD^Yy!NwT{p?@RqvdiD@&OLGp@IXQ_m`h?7@3E)Oyp|(f(#H>+K zZV2j&k2%c_J+BFy48-YbfIIE2aZS5-%8EZj7Jlq4evp~=-JD4)`zIBtc%%Ey;S`4^ ztLF`RT{Yyr>ihQg5dhx!+j*gfZOS=ktiS7$?=Kr!z1sk6D%LRZr06);uM>1XU!JE# zpUt5{C8yx*pN8vIKYud9VNaWE!cc?igGk_5Z074#J8@j~+7D%``QdyA@CHG5n-iGe z2)Ui5llx#_EW~X3F4A*(>qAX9vw9Y)dZ*VAI!h_pw_3Z!DHYkA2md>1@+Iu*+2U^ETMy)tmF=Y=^>esh+hH0ujd5EHXfxT`HRiWG1prhjpi zpk(7WQW;L1oHNk=NsN6j5x1(cdK9^)ek}J%k-|w1yBu9+i`Xj3{Oaw@W|C9%S+tCK z)3IT6t7Y=+rvS4+!G)I8t*yT>tsxVqA{C*^j=g{k@j7p0oPo1>z*p)*Lf?ah=FIbw zCReio#Dz+s5eQsRyRl zkEBMU1v)0~Mt;hfHyCR2H?tgbS*tbgqIH?Nu3b*ZZgJk!$kwS$Vn*!7Co!Xi z3rFUE5w*`#sk9kCawB4=`k`js?2A^|dWAc%JJhRqVGSGF6ZX9(xT&=|#4ytLA}+nk z(@mXLeT8Eunz)JelpXuk{`gJS7>4PR26Kz3$8Y^A&FwPa(1Ba+5@9=ND%vWo-s%)dZkgqB1GMNPYzEdKL1}h3SOEjK3~; zNRzyFb9qsR2f z(mp7CHuX3}Jv&HRI^rvAhpH%O)k7Thw)9t@CewRw;Ca%Bp$|Bn^#an>C~+Bq%WNTP zJNY~3ryu#Nyd0b{aB1J0d_jZNQy!&;XN|Kn^6A;fg;5(1CB%Jmvk3)keJM? zRB%i8(e3(dy`Ixb_|%60*!wwL}SC(7vk16805$th*_FMr>z# zdd~2j&^;+jhSSMf6B^_-8knke8qobcsnJ?j1_PfRA}%v}no^a3WDSUxD-Li7i0q|T&wN5qhM!nIEcq&%0PiJGa);WNj9_gdBoo%xqws#x2 z@N}oxRqxgX3;vyiDL@@<&s8iEp-M(!It$JkjXV`kq$B{(2tWmp^B3N|vapkK=5;{$ z*lS#Qi8)(M*`5%%c=?(;;h-8TJ z-2PN+V%bsRk&Za9wl8Qy6V|lVWFI0Q^E4`Vya1RS?Td9J;bv&*oC9Rzr45ULIv4#H z!c077vNDO)LKIJO1Pbk3BKBpgr6q4xrcj#wb~XX1b5#hLYiDXv3-db}{Qi+__T;Lr zXQ9!WGN8pX`>HK2nmV8BsZEZx=o0xNkt3qSTI6zZfM2UUy#Cg2Vf50UXt`LCD%Y9K z_}eXQ*}gvJLTIi2vVZ{LLEWnBf+Y8qvqQy>@0B@9Kgi;`!`enVV+C$(T5GqMH0PBW zz6MX!=SYj9P>fFfdZ^nXWbbT)V4$B?GWM?e&7mcALc>|^GPXUMb!k+YVdyzS%9;nw zjg?3u3qZnM(_Sh zD$ysQXpzRfM*ViWe!^eTOK&MNBD_!UQ?@RIu;mz73+8W3zPM{~Syuk?FivN1SIW}J z80b*r%Xb}4g11Be0Br_ap|6VqI28L%8agf-fBaHAyg6#+`CE#zs&~Jf3cqGkMKf{u=rdme4;vN6*WcU4EnbyqFsfrVL7T(gITF{N{)%CY=}- z^#=L-(Q)tgU=3Q}#su>>1fZlX)lWm7gg;WFlWjtdZfnnpn9@sJ`7`azD&*Qmoq2&#dM{x>a?*2L`@vQO7r8uH_#UE`&C=ARW`cc&hk#ZWucm)oIAqoGEj$Ivyz7 z+uDl^6mD=OE$bd`eLUek6IC=@O&9hQNm8vaJNu@zETo}y9#)I=4sg=-&ORujVm1qq z!3x)=pkrT8-YK=r69>#)%@aM7+BiQ0Oi}83qHwpdWiI>~2gj1g04sF9CCZj_ z4sC_D#*|#37{+3ENwKsB)Dtr;__)*+)t(%*Y?9S_O5(=!vr=VqNTv`;Btf=P)-tl% zX}!&4Gqk(hqG%|83ny@*Ote}wAX+x+yx>7#`OtV;hl& z%tyr!>&k>??hhTKXQ-0&ONSjKZy$(MsT>u1%DOb2*{eQRcC(>mH@uTklS0xgtWJV) zN4|&nD`PL7PR|DazP!Ith)rbzW#e^4iGPW4yFWM>_Wrb>!>pQ4bJqXn#b1 zy|log`gROkNXZeb)i%HpouZ)g-1XzwG0%?F)oDLX)Z%R1dbF)PY!_kv&GVSWUv=XS z-!XHn%d2EiA?05;BWMZZ3W&Sy|Kt)fJWChj*A3#Kxk74CD zCD6`KO@kX7aRRsURFvYvt`c*$7q)AVq@sykXu)7Ev}=R8ZGYD@THfvB6bXeYJgA!d zo8a@9ilE*fP_U*kAYXk^a3iFi2g`s7A7>70yP_2eK`LqNNbn0b+P7$yJZpAZPFO)d zL1rdo0WuT51cS{;`Ka5~LM@ocGhJnV;mGHPZASP+i0dja@Kw!B9JuVrB+x#0d{P9p z`Eqtz9?@FbKl_{pmGfyM8~A+qh8?jl$mGOx&)LV$_K$?nVHnSzHQFwxvk3jJi0_+- z$*UN4g!2}ml1j>g`Qg^TsNa{uvw*C@tB6?g zOWngIS|?1c*}(^*rhV;8{6C`fYjN1JUw}7s7kZs)Vul*O$9>{!1U|(g{x^ONa}M)e z7Df82R#@ywseQeS!wi&VXWCyxsI&K z+-{-~X$dctx}_n8kP*Ts+tuS)A7xh-*_vr^g7`lQ*dV#2q>esR-7 z)kWI6f1lpM9)08uMXcRxzuG)O?Ts`~{GpVv^qD)AlFz;S59LvneE9KV&xk_;&cvPr z{nL(q{@cu&4{@Qm&a9h`ELL@?PW6$wm{!O+W1uwXboU~p(C^e6g1p`^&Ply zxg@pS-2{{Fi@N9!RdIJwo{qfg)!DX|f^Nk^uTj2SNM%Mf+q~cXaJ@{ez1VfF^f96~ zTr1IJh<)146aaS4MKAYHaFUIe>%XhulRny>M6mAgdoYrDQK*3@XaaM1pcPc~bqN*X z`=}#z-PApw_<+h4rJ;7l%W?=oebmK!+P{OvT>Pj!CrV6RoeCr%>vbW^p8F7)4<;no zzXih$kMW%w`}W&<^yrUiG!UE48SDd%m&>LH7MtZrx9rNl-gZB844BWK+$X%GNp!Df z5-`DukHR*gi@s6U5w(_YORM@)O^hx;-lYD(&0csJ#n>_~K7wJ2IyA@ICC%p6sr*Bc zs$ia3ao4lji(Pu_4BfMUTPB(flZ(-q9aUB} z<=+h)=-b!;LAPC&stm`zCJ{ z#p082iQ;aC?a~byESE}dYgG-k_VsrH#$RhlM#LY1T+3IRuV7RWmA41z1kB`w=UVNV z{*yKJL-K(4a%Uv>F0~u?t_TV}W(9HWC5_{SiJYxsHT|MFHqnfcB6p~du@!}$lc}wb z@&x4yzFtl|ZYku`WUYAoN&+ZSDn4r*?|!y6FoHf(gn!l@n-zFCh7*sgWo9)Ip6>qA zA$+B|W?KLqOOx_Oc`UPSsp(DanVbOBp@NOX98MQ^ckwJL$=`#pnG)lB+7V3K=Wp_^k)IB{+Lyi)7-+`$j_3b?%~Tv8`=B zM38&^h~NIfc6;~S#Z=RW=+f&gPgk$AdDS;o9i{(TxRg6;leW~KW-0g{wq!GKRlr6; zB3WbAh*T(6e1G)qcOvwnefNnV)&Q+`h|mvnjpdmlF+m2r17PS(-1PHj3n7}lqh54B zCpiNKI*XlDN~m-rvbT*ksE5Yh4feeitDT8$-g@SP*QZZYFGh_S(Jry6KT}gJKEwvF z+Y2O^_A}boe1KIp{ShkuKBbMB+p62&%X#DL?uklT$|@LKvPZQhu~s^rSI+yDIQU$> z>owSay^;;FG+U%I=v}+Z!*{AGO=a0z&BY2vrm^H6?zHhMF;khhaYDf4)ja{XT@~s` z_6eEd9}ZpT?MxwS>K`#vRqy?6(0fQ2^{ z&LVmRUBLr;HTv_KQ6y)7iv__&x+1%`=2EPDsHB1#S@F;XHpwq8@&kna>}-18@}Cqc zB>(*gEl6X!Drgv$k4zpDi{_7HZc%g(xA5(>+0D;YXt?R4;eYed!r13#yIOqf-e9|s zqw*%{lCzeSUkd_OCD0wKzF+O}lIn$+0rKCs;?Cwj8rTo>v7Z>)?p82GdbOqQj!qOk zsU{s;FZb9On7HsHKE~xwMOFC^JdD515)~jqGtg;ZG}!s2TsyIvq0#9P!St=jx*~bW zPW32#jIX41m+8BQ|0t%`GQ-70#^J8S5s;S+LLu_5ssrjDc@8;ItncCWedR=M%$l~F0B1% zN=W!+La!@Uea3M7jP$GTZ&xsDrHt1>ynFMTG1Q;mMh~9o$KDq91VqKDhGZaiG{m#r zF|^2YlOQx-iUwUdfu~1{?IzPDIj05s)!RU6{l`-ro~`%hdn(bDAF9WS@4AY1xZQ`!@1CweCanFN7UPW?i5^gm1=CfJo04$mlI z!%N|PLDw}JU0z}!_V8w|EszheyvT1)7$#}pkBQ0_UNy}0cu_Jx;@&a{^fMqXgqR9) zYO*F)g2g}Wc5@RFo>`;Mxze5u7&!}36Ac0Jed@Z{Wk9Uvtpsq4`+fq4zk8E{#^B}3oJ6@fy57!M@vS-7R{AT$u{cjMM& z2(ah943Y^9hGRW>mDix_j^uvgm4pP&&-r*hu;Zq`Pk|e>JJ9JAA&_c-eC@7D{DGu} zpFR=L#6o1|713k`xtkss$#qPx;zP3-0I)#4Aq0fZ$@73taLK!GEa!!u31Ny-@Kv+q z40d2*MRhIlecWC3RarnNFIf2%!@Uh5r@-RrDw()Z~6_E#wT$pzDnBtNanrz3B;Qh{Qa`gtN`D zv(cBO(&OFnpYW^rH=l8aicG5@{3o)w(>_SEN+k!Wp`QOGl9yn}8*Bs|Kxm;eRHG?^ z$ysBf3VT_QgzT0_QC*cC)D55ac;90_@QGYYdd(!zTAba6e@B)ldkLp{p~m zC?lX(FqbiWkex>L9e!OsvAlQU012qn*u??)+F`s3eIMj2ax2kkPIOZY zm(cLG`MiC&U|2uyIPZUoW$1OC5lzU$@@v%ach8u~GpQ@rA2I0Zi6y=h1Fi{@1_)lq z$#;JT6B@3?>7RvX=-PXcBN@tW7_3pQIWiFi_UN!s1@LB9^mlN_H1jmEhe`jAnGWhA z8~JAP8Z}4@8P|X9Ou8NJb=&Y$wm4*c`_`L*STz{N8E}Rj=+n!50n-frCl?@9y!04d z4Aa36$S+#97R?aZQ+@Ksh1H-$dTXJqpdyGIVf4m#h6*mL99w{WMwG0AQtpwOI%+us ziJ@@<>hx2)y^YB1%ZCSo&HE-1OR#dtUp2oqRDr5nM(l$?o zbg87fnMJ7wBlzIe6RcC3QdgW`)w${}1MmxT>XC2VlIq@ys7MJ2U{)>1ROBcLWER!m?18A6Y-oVz>zv zwUo_8F7YSCt3Ze_*%})@kScz=K!xgm#D{~We`$gd&x9$5h_aj6+rY9m4=!xvaNbTn z2(dkp_orT`MqEc+viV|NYV8Lzcl{mfmnN8eSC<}r7tD5;G9=VQ+WrP!=pVRT9P)hU7P+QPV}Q7sPf16m+I-ko{Fd`f~FvK>K)^n0Ok z?H_0=4WG?tV`K~-qCp#fVUEU8PJ6QpMx>@H3d=m;3KQJ5)yX!UM!n zppUzm#jr2&+W0&8G|bvd>cGDURxG=MR){E9-P7HOF>ZrYeK9r=7&sL2H(VA~8m3j_ zi>wd0`pZsCzz0kwVZ#$J; z`JeMwSIKiwr7X*)+*U23W(G}$p-C3-ai#(X`#cW*c>3Z1HU`loFqf7cz!of9#~xc= zF$TEn*%Z!)u&N*OVy`ZXialF%F+XM>;~QRcfQy5nsl^h3S@eN6z8SP}qB+piM4@?p*|6Otg;j}FJ zi*l?E`Y<3#1D@iC_zloG(T!igyayeTp@gzG@fYcNMK<;-EoMBQY!(S%GO>Bv>;y3s z1G#{RIek>o0+U3?_}`X@nFK=a?4IO0!@2YO%x{)vPWg}{9@ncS_B;dFA!usx(l=Bz!x8v7m`>y=7aVX68t&fL|LIte|JUk^=Ed{ior zrMEHk)Ls`{zdD>CWf%wO>uoC2?Xj&ZHSjOIWkW(7jqO_{-nMr0R-4p9GW8HU53Umiyd1F0gHh1Du zQdmI(9S$G&hP5$HGE}jv=eSRx<1mEJe$k29*fCZS)K9-qnB0|JdDUn%*v2az$-CiNtD6JDrxobBGNcXg}fS1 zn=QaxlZ-+UM9EPfid99||AxWg43|k(h6C-=>!(?#*Xu6Q1BG?wl3{Y9mU;fE?Xqfs zIKX^*r#l2NDR$;tcO7pDK+Er9XMU#oLFA|c*A%vX=z9-Zqa(PF!`Tzl_tu%@kGyRW-` z_8NX?=$AvalP09sbAXT*j?oMeoh<GP4?%~2(6uP zOC>3|K@O}#~mmQzUY7EoACL9M@2C0 zWcl9D-Y-Ra6F{q-t}u#Ml|QE1{IDMy4s9ahTHT>p1^-p#grGE;!=!GB(mO2@L!Kin z3_1T>MBZ8A&O?NS*w#lR>E)+aPcMKVmzOVSH^LixV{7LYED3?!#WATq3slMQ=i{k9 zmlvd|Uw~J-7QFEp^rk0~LOQwb#fb@q6_OCU;pn>;|CN27hMbK2@>?A^_w>Psi>D!l zy_5-5VFtJIf+|*%x~V6iN~sw=|2l%A*D4j$56GFX@5TuclC00$>v`44(Bsb#8qQ%b z^d~1`h~~L4w@sS$PVSRc3mJF@_J((5d~7U-P3jgsK{0vx z4kD2JlZx~e5(16US8k~;cd;h6U41tu6o)nyB8(ueu_eB`kqk*Wcbqx8ihoGo7=C6? zp`mL{4o{1Ft=;FPqQKn*F#(32(MEBo!H67(Q8|q4w*^=1T*Kx4xfGPUwL{?JYMI?B zfhcKshQo7qG!EY&*?25w^Ll9|o>UKO3{nXn3?pw7uW-hDtn5A?4orHTz^QA02cr9K z^=~&5O&f<4Yr2^LEK;o@HX)+F=FPi$%&{wN`ulYJ*!n-Tk)E3|unMN|sVSV-6NMAW zL(X%Ds=tm+O`=3-YiNt8wMEo(J~BZEKDl!gV*JGQ5?_GMT3FPY2`ykdS2-p-^gFUN zR(#}l1IRJ1(m)@(s2)dRZf{gW?Ls=yCD>r<8Y-110;zRxZ_oqy*8?l6fZP@3s%{S* zGUtN!9Xw5*aric52YSME#B3+wtM>3c)sFG9QI+l8%%-1)7u`$%IXq<`NVRII{%gsY zcu@&XdAQX)is$7nrTQ;R6wvY##ZU#2$)4LFVFgGR?H+z+nGai5^6HGx7lxfMR26OE zY@wqaG1ACy_1g8(xa3=T^U)4#{j#agnj3S`<{q3Sw+7!5Z+&8IZw)6hnMO1yskkp# zlH2iDBTfV*klrc<45%wAQ|pe9wC(#xPi2i37oXY~vmz%{X!qD!bHBC`aci|MRvClc zqnE-PbKCQowGvWDvD28LvQjaCq?zD&&Y^i7L*MDceysSLj zlS3W&z1eJ)M%hT-H6AoYKbRso{TdT?FMDlJ`o%G6se#fAyW1*};$5)e$j~x*+@=z4YP5E&(Rf??rdGAunaHFR7nxDFqFfR-DZe)8gd$JF{CQ(n3HrZIPAx-q^C|$Q887c*W#A2l$pBF_Cl1q?K$)|sjAijGCL}YpNG5X|9!N zmyKHuBr0F~)q?8k_)qlhz;N>f>N`c9JzLvlOlk>7Nux(xyw6Y0RTP@1JNw~-@z2h0 z2qx-CdeW+x?1yuj+2_KTJtdcqMT9N;;fsMA=51=ico1k z5~s?x84(89dJROX{ub^gu3nc+z0A<`6nlFRiTkjYaYw-al8ZO(4Tu>&iBnvW(=q!; z!<-10^Gf}Zw z3(W@GS6;n*TS!ne2lP8Rta|(n7PQ5xMzFqzmu7lb;w38u-{$_U18|&qpfBJ#V!q*g zOXS>gN@&GE(|kkN5jB1cChR=zN98Y3O*wxDKMJSwG&!nGnqNQRqs=}ImK<;z4gN_B z4#YlZ-S-gOCo?M&N~st5 zL0@O+YsjAbtM^#OcZNl6k136ZR9v@duY!QxrFT^8oF~bDLN}S6;{BJbqmvWKj#LuLn&mg2X^B(=wIituf_1M6T-dCtw8XA|hibxLa5pyqlNnA4m-y_`mQ z6@GoyBKhOY`Mt4+eCEA9&^O;6CX@yz1Rg%bn_$ z*x$ltw8HiM*;Tl{&uyKB_9lZ%YlM`DnkxK(m9&HFD<%dd-wfXEWySo`HCcsnl-#6`!^8y}AEA;eLpPddQR z0`6J0K->whI>^7I0!XKPl*LM7Rcs>LMwWIo@oK1z!b9xAp*(fkJBN9Q1k_e@2L};1 z0`k7g0BnoBc0^>gz>72g-x5+pd&P=YGok%ae{urbTJ*t@HuRfOF%V)GfRR|cFj zN}%&eQMqs3;0eN+<`~Cs{mx?57Q}lCj=23J&oEzBl3Igo3m2LZH`qo82E7N6$1ZE_ zX=PEnkDoS#pd6q7s75p;PXMJUoiQ)T|` z-ybxM9qrA(<0nf7Zh~p1M*oTMH?m}YRU!gc*8(JvYxySiNe;=Qoi+){$5W5Icr0JB zt z@p-(D-F++N7T1~YXd*@}nkLegat&qrkaZgHWvHul$`Xe>dmtNh7Cs)P+^v%9QagWj zf=d7qCiOdN$cbl9^QW(rex1;|$Cp6LAK!B*)A)9mxrkCe)C1C&cpndqM%3Qf(DvVi4Z3Hqj{ z)w?^=LrZCXE-!2*T8Um^`lBpiPovsuKiKX5W3rR~`hm!?{~LdqsDuwc4%uN@A9t$| z@js)Gjna8syAXUeT*?v(?7#+>9#@yyJl;`Stb~M&KUglRY6f?PA)4Bw%6?ybyzmfO zb+8zKKVN5~=ShVX*cm?bdW7o-Mvd3ie|yqyK7SA{|2s-(=+<|3^e6mE>N9Z1r@c9_ zue8oaNt8dEfwXa6n7cXVn3wf73KeK4D@oT*8S5U8e%SQfrUPno9pkOl4rTHSt( z@=evR7;1)&w`ctI3Ph*P2Re-3Q)zN{9-8?~VOY++PqH~f8`4vR(2`b79PS1a3bst` z#$xXsquYTHBGya>TBtO+JyMYNP+i&G^M}YqG5Gw(E=I z*WYO-uV{mrevlBtXxE!6_ztMs1O9b;RV-{!?LY&h>1rO!9`nySxdXJn1hhNpG;99O zM|H*)53;X1@z)#Kjg!Cpr^%wIpBe4&%=v6-ycBkt5v>th2}X1lAlHUyj@~@v?OTs_ z8WXL#xdI`>!V7PrzyMXOdITN&bcK4qbaKGmLV(SkJYy0sZ?S;$>qnyoFbX$c{GHZC8Kp=N}ZOC#< zO}|xa=;;XY5Ij9TsN>Fy*o+0EAO|K&P;)3I?Uzs(;{btD=u{Xvyd#CQ`4SfC&Em48 zF`NVHNqSHHWG1(#t`v7S=Wo^J z2Ohr1EL@|?z^x(Oa(^5s>@-W5n`<=g9&wwT+QF_%6Sx&3!lKSslh6>jQT_2*xhLK} zb%pe~{X9M})yGzpGF|%T)NbD>5hS&7$|Q}-Jgn}?9d)y_4YV&vOrbXH%anP&abAT( zG)4Q@43Z5@&Sy*P{Q-L#3dM4@T~*+D#2_ZYvB6|FdH$wg^=6yp=KmS`*CHfL+>Wz) zL#FWPyuZgA-TS}zz8+se7{1YU*nWp}9Q4Cw9rQ5cJ(0^Izwc*Job;0Z$Szm>)sC9_ z)Vd*Kq$LRo+mmoBD|Xf&-hE(-d}nr1qKzqkV_olC0{>RkE8hFz%a#|GOauMMmy)av ztV+L>Az;Lq6jL|G?ydM|p-}RN=}9iBgaXqJcgL#S`-kUfkq+8dz$jrbmPQSHpWFG3Bqj1v`rGjF566Ni z4QKwMFi!)$Qw%i3gfkOO2Zf|#+?fm!c9xlEkbDz&Hs8ko8D+j7oD^C8ffhT` zAHrjXS2ACk_z{{RXEk_;+|mZKi#s3}+TT$W<=5>#7eD{uPs9?dSg^dx>e-cTf;c>s`tvir}~VYqa>kY13};j8+;u@N z3*pL>Kfq@6by8Hw`W|0n2i50GRq#cRE-!@LTD9o>Pm5^)I9kVUCg)*&3qooqO)V3Z_?W%i7P-ry@?*cMgTl|; z>ToJ;nMm8xN;IoP#`S0g3_`TCLdmjwu#+`0gX$a=RFCoJnbBx`S$L7&cTm5w)e2$% zV$RAB+?KUTT$kqPB0A>NRi%fLnF{ott^Jw{r%g5O|2n%n!t9jLukc3bRLzdkq6I$B zcTA47Ws=5ayf~KU$tjfZB*ndD|Biw*kDU0iUo-*XBu!#ff&){5?WrFg#plSN(w84g zi@kkEVmMAfpewsHg<$J9votL1YD{VvRNdA;wQ8UNOG?N7K8;nxzVoQ%FUxlc5$_$< zS?^yZ(oyd1*X%H3L)(5{6l{X)F5h1HA4|#cpQXf0+}9|7bE<+X4^hkM1LI76M3^@&7euWoc*E4Vh!?kFkW7aJ!;9B%P*edv%hA4Gok zWaqL-6LlmZhIU6x#Pf#TaaG{8&;`?QP>?OKP?!&`M!qH9+SKyS+zxe@9tT<8+Tfxad= zIJsm^rz8C0^U7BR5l6Y4o{VXkJD4EA>a)kXdkyHQ70mtbK!UHTMJ)wmr`+;Qu^$ri zD?z&KA1?pO@|yBe2-EMVl>?WmKJDbeUy6=fDP1Z;bjBY!op&_~aO0GOG=2Two-)Huglt75v}au&LE=>8 z98tMTnN~MwN%f4eQiz1= zd&meshri+?d-p4Whq8MduO9Dr6d*$-`^b50cAuEodVAi|Q(_{IR9BKQibQhN7t@y= z^8`Or#q!VM-yuh5)addv?38O=-t&dvz2w7|f3ZmDfL28VjhXzwZHUSr+pFh%wT0QSG9{}(_{tm=o+ z33H4mVPo|^$}yxC47GIhH+pDffD@p~D45_@_g9Y4&==1D_Xfdr;m(*WIQsyH9rS6I z$Lr=sL6S|k4z^}?3hV87V+(gD@gKU@_43u=h5a)JG|6xzD1zsP?gySmWwXvirh8^d zJ`3upOSh+Lq$Y43*YjN-de(IAemJizyhq<~^V=t)QnRa8`}d)b(^8I5ENdmWXg1wO zdKvkc?1_I*!eYSJ=!C#QEol8=HhS)ED3|AvpU?A!2|4QCi$v8O810p}S(^&%?3Y^S z3D*n-SBCBJqQZLWT@q1q1PvOD*?;X3litnpT9R!J}MZ;Xda z_FwH(Yr{!S0bRAc`M##m=ddT8+|kWE`SU6&{vWNk`GzPBpHx#6Y{8Ue{JZ(J3h!5+=jlYD!e<jQ3+#LqW9{w0?Zu37Y_h1gzDohUW!I+cUn8&<0s1JNn=YYlZ5{N3u968ZwwT1F4438QMwre$o!rpDF2_jrL=*S!&ei+mS+SY&H<}PsCcDa zC5Q|SHM7T75Lez1`TtrPw|@d-?&r6vB3{uzk_^v1o^`%?-F< zEKjxRxoAxJy0fnNS51F5nT;d)lbJP_aM~i;|G=r8J^2?G(b*sd<+XPAsC~-gLa1rs zol{K7=7@>%aQjGb_UM8Q!)`cgoTQ2&VLS9p=KZ7WhYyG?TOMdhJHDwyG{<_{XFqp0 znT_oe-^M<*fqVby@KIoXw&kfDm3D?p)R?OJi8S$mE~7ODUyHr}tu>X(_3#w?a`+c`#4}9m**Y36O#>@;^%kkSf3$BG4ecGcNnWcv$q!=|c*8Tq zZFg3B(C^=E#0_#2gFa}W>bx#AOcbcntBLB$_z>9}{Yszw{9k>Xc{tSDAIE3rJBG0| zMOnu-S+b;(vM<@9LJcymZIC5KMhRswDv|XfWC>SEC7BUZ*E$MgDT*-2Qm*70OGM%} z^E%&hf zLw@Z@llMzz<*ZSSX@d5zw2jhh{o(7imu1MZjBiigJIHh9y0^4={7Th5&DzUiQ)Vxl ztR;mww~XsfTut;jiA}s(V#=pib*aQQc}ci6*Jiy}?+4=O&C>p4HZZkq2ywsb294AW z(G7qC{^j9y`E0%H(S3LXIdQLe z-HVV4xn_q)&By*4HkQBF1Yt440&DR(Yy)Om8_&Lmq{_cl3m#7)sm)SB4?l(-z?!py?)ASn*JP?*%r^-@ayiSs(S`NUmb~S&F`qjaY zT3x}OwU0Xefvip6u^dez^&N6r#Y#l#9_=Z;xc!;d3FQ;|X#kb9T+=(M-l%n!zqeJ; z_ilJ?rs_EAYIJ3EXs>6<#V z!w5!!D*f=N5O#l809@=APpTt`6}^3GgGbP`N1QkDJx}kB4dkiS)oQihc{vPkv@DwM zLQJU;U1kIFb25P+geS@~Odfx##Oc0W?sy*wd#U-UG}lWeMZKb{EaIi-T>GY-J$$l(zqQVFH>E$@8M6MDNXmXZZM@jp*E0Jb}G6 zNXu9Yoed=B7iH*+ZHOOr`}C=Wy*nVabMFKl$^i3(ImzE2Ah`T4=HTX_j?g- z&vYVuJAaF{qab*N__iv;-h?WBAbwrGp|;PJv*jWUUFTdozpQL4P4`6G?$p-#UYZ*Y6uqtr zXUEZF)>{4Lt9qhaa%My+a&`EjV^ka-c$D%C-Gn_HzPNbn7A^UrZ+%-lI%WCfyOP5K zyYePlU;g>+?iz7wk_}0ScHU>+$1kQNL&pANEi{afM?@&`3R{%6i8ZBseIUZs(PhSg zc6rR>4KrD-il4H>P2BYaK;x2Z3>FA3-vg*=wEpJH+WhlqS1C6xp)xi%73eR!S}N46 zj};m-7i=#qtqemy9HR` zwgDQ2KiFBFjx@jbEbqg%i-_Vk8Uo6&uOT=0o7SuTmW8*xzhl!I6SIY-63k|<<-lq`2fn0)ImU}ol~+YTk`hiDNhu^_vX0*nIDs|` zmSgb}j76U0Moxk79?43mZ>pulXg5w$8TO{i;?99!kZY8ZWb;uNl53Cik{aNcNh7~J z&jwDUcw?#*+y|HyE*9?KpiYq&16clXrATI|+w6McCtevy4KkQunijOoH31QE5}2kZ zpbwJjU^tIbE93cP$3llVnA@-oVZO8BBwho+f|G2@!-*Q*4~|F|dc4(8hM5HaPFugI zyBc2zb%4PQUffGlm|?`wT=_(JX8kgNN5USRbp&fxH5398PiRyI9x*kYUtbX)2N?a= z^XVZn6ZgP3M5(Xap~VxxuQzy(#SrP~)POJBZgJ&HjS0KKpq-1OtzJdn-o7RNKg7r( z5onAXaNy7Y>jQM+ze9{DIzr$cI`N#xkB?MuF0o0Wx=7TAn0z(8%7sFI%#@}P=mZw2ED=D!THzG)0Xuqd5G z?k9o!&<1#c8$(P6d?j)f!D_z6Nmw$ur@SW2%jEgSLm*2%cIMtUL?|~Ax zMoMt>r$k4FVnqXOWPzXQ_RExW4qO)9UDhYTf*J>REJFyBG2~>w*A##NM*&9@9Gm`7|8a zlCmV!b40^e*ZI*lI+&HOM-W@fU56|2NF%{7WWfyuazJuv^*{*IlQI-_jpTP76b$Yu zWNfQyUwG!hzH?Rh@RitX|xctJdOt8wLZS?$MH!}jqm)K#Rs zr(~H64JYV1A5_8JQ~-5kA=g*GPos}?Ql|M&2|X7J+I3?$0M%-h1>tCa-i+dp?j3AIgWs6WiRc1;M7L&UV9}nw1?8(CiAPjG{412+2t9sWA z$w))n5vZtr$Jx}@iN+3SAl`pUk*IahNUzy*>dX{**$8;KGIkN3H(3)!4~o3NL?Ifi z#AB8lDh!5OpFme)AH-tW>CQsCGbWL6TQ7yU^oc4N-C8M55G z-LoFwgG=&U1V<-^a2SzkP%9KAWtc5`US(NdzgSv4v%upegFe=XN{w3e5+*5dFE!X0 z&mlh(n~vgipvga0)o_0A`8D}W*0DN&4@|DIP1D{Tn@)LuFO(Vpb%Ze2g`D%u?^Aja z2jE^;FxblkZ<%EsvOypv^XfO@LkD0zr&lD@?p@H#t5=OnGOpSXi&%;En!@7N{KITJ zc4rc{=pn80_f(RapOS^6(NP)U6-8Fv9Pin$7Z7sca*mchF#<+6XWkxElOFsA4~2x@ zLpJhF%36Z*akALh8v1049h1ruIH5 zeVgl){osQ0mhLiYS%7Bv^#jgt`PpjJ9@2p`mdWQaxnf?gco=5s%HPg$uu7~RCa_Np zm5CcAWF4|nD!j|2kg84d^1hYbhTRov*0iu(C5&kPkFOYMdtw+WY=MDV_#|F7$3&m z&u%FwJfmu^mY70#!!su6GON(Da;RG3I4r<>9JXp8q=dJYpp6}A8my2Bwu?h(>H!5p#|xoSLq0d^xl*jkq!dVLEr-E2mz!d8l`unBch1(BGLpQB27R* zsZs+7C^a-GQr?s6e(&eY|Nr6r_;4H?Np@#ucV~BJe>*!9V`QL1NzO_R001RYSJMOl zpx{?1Kt=+7><5pYfFDEww~%IJ;9nTo{b=wv+*j8s006EaF8?7wP98H@$nsFj@}a4Z z>qC^IzYBmup~T$19|Sl%`nrhu_`Bt9DYF8=RRF1Z(=0e|a~9RaVcvYSvyIfnVCmFv z6_M+v6<>XIRiD;qgP49f^uUd&*3NSx^uX!U<4~;)TQj2NHFHgM%^c~QWx7Jp!l?aZ z|A@$n06g>Hpu(f^94phdg4K`$z|pn6&h1YH%5}U#25sh}1bqaXhEBt#fsrVukMSY( zvl=+4gP1RuuZk}RK@M+&$Apz4Z#!N^LB9(NK%$^GU@st^d2$>HPQ<Z&0S9-kka6Rl6_f@1v%~$w#=vxo=444|^g~_sZ$y;}< z)kPJr7p<@Vb2e_|?d|@e2lQe=_!ss`nZffd}ENP1i5loWv zMY_NwYg4+k7LHnY&^9%_wYnF&o39=cb$sn?W- z2oT+{3>i-?>Zs!08gH*YuJ1ubivWpL%Faj=?z-rfHKmUy>E^F0$?_HAIH}u9R9%oJ zv^V&+Vz*zencn>padT(M(coFw8-tu=C5X)2AKLyo$SJouI1gE<6F2Dko7Ei!ZxOBc_D!P5EUXrUx(doF-grBEW?h4R#PtfVddYVh& zhh{OSA6d?~zsp+zLU#wgoK)SnD)HV*v<%w3Z>78T1*%><9%~t7{7%$NZjU#k9!M5; zfV{Mn=t<@PrbS%msrdSDnHVwxseFOS#ArSw$9)L6MoFyz-PF;W|DWQ@6eSFWEYTn0 zApSe|>lDN(vtCG*c5abY5k*j)+OOCYMQ~8IN}}i}-_r%tWL=rjWllWi!QbfAALNsn z62R5sma$yOfzt#1xIOgs#rBRDE1EwWtYSj0RVSrgOv>FFfEhquP~WkeHAtJfZ-Kvm z>mc{9PL|Xbl#}b&_Ky?f>0baRplm3{K^sSLhOYLuCw*`{YF|}R`F}# z%6^vvXD1yXvD0}p4J2r=q0Y-O4=|ZaS&O`zx<{Y%zLVgPSRHIqe_ouVyD6kbfMAQC z#(l^RC@p{OFL<@|?hA4olGdx>X0fv-N3-CzYcXv7X*&|=qR4vA!ZiH1W`uG5lo3!t zod?^~rfs>eDaSn#D(jvWxj zHYo@y$e*`H{*q^N4+m*ZGdj+(0a|J0mBE_5x*51`*~b_hHN2XltSjdp;0b3hP>Ok$ zeI-l`WMOq=BrMM$OKkM718rm#&y`|GtZp(}pUut0=z7Fw0;H=3x0c7#tusjJ zUYibPeEVR2bW!&)1y#xTJ$+FiOy4EU8>*_61;cgQo6ssP5nDWv@lwPKCU0eoO5&+; zzp;f+RslUkrBY)H`e}1oGWiJ|;VCF>m;zsml`yu``~b(zA%kb{^{tH*Wz(+H_vLa_ zKggprdL5HZNqEh&>y-<^GvKaw7TmB{41-jT7U=*mEIh)g0+^#}Wj=cJP&?Jk@Cla1 z7)3gmU^4;uYChC1%NJk(reV3-RZCu2IWoS|D_i`^vLEIBRY>`JwPZ$$pjU(LmuNh}@VZ9|$l9VM+Cp@sUZ%-c>gX-l`rTsG%%MfIxkMnx4CrJJ zQzP}(dmhavGi*WTqsCAlUZ!bAtuG8T#9NMASn>tV!a4)VW+aU{%VI!T>zbl}C%_$K z5@5?x>p7rcDKlCGbqMWzhvM(Qm723Y80~T$xL1rMRv##Ns+a1*jr__GAm%yMTg$4E zMh+lT)1!&ePS=wxgFaqv$nhjg*_EtRX@kqXe%0xRg+TwUz;|MPQoUadpGOzXg%!WH zL>Yy}KRX1ZFjCQv)T|C_xqYSmJ_pUe5Xuy|$ClN}i|P@8ji94~?>`$N*0@k@+}#)z z2E_$(bVa#y0_R>rMUE581}}*o(729H`wKn02w|-G6rJ_GN6~_&`=s4RXa$f|a0 zkVj%mT?ZIdNWm?55CMX(?lP;s$hps0qvqI=7fzNCVH-i6Mn>E_h!c@qP3`}KOGA5M zyvT^B-WRq%@)h!Cj=d7YCQ@iD3ec~M;SX`#;5HID1vkf14GBRSZ`9$}F0$a4vF6A_ zxO3~%o~Xl;YGykpc+6S0ih6kndx-N0xb-&bz0jCBmx#ZK*Is*MEXfiqOR(*5j-8n| zPn4H9u@;?}!nI%`Q@wLL7U&YIFhz*L=-67LGBh5#jkXQ#U}Bmd^bg>Vcz__Fj!+3 zf#=KE>&{s(>Ckp-mW2LwLnJbo3%N#F0cOsWc+^j|n=D^r7;t*MZcqXgZ}{^Zg$yO94-)}N_Sj!?*&X|jV%ttTRI02Oz>-}dotZjhcwTQ>>ji# z6;Z=*7o!erI^B{T)<(is0|xJak~vUMc)HyS-GtDtdK{v3PKvBLaCz=rtTP`cu`l<2 zXKl20hb!?Z_EUk`t1R%`NFE9T4afAy=ExH5xhAe)uCWWwMiPUBfmtEO3B<*TO+MSj zLQ@9e$do_khXNyTtU`Mv+-5l2)kGhxR6{kELlQ>5eQS(6;&v?yPwwy$b(=Q~qh?TQasy3Co63_E5q32=_6HAQINNnJAG-uzeW>c%QvaeUR(P22=o-ImPP zC%D~0HSQUeT=n28RbtD`llbi6Bx0Nn=7wb3?{-qMB`Rr`ceT&46R;a!@_U29{iO%b4#SkvO0Tj*Okbbf~q!2gA z%gcfM_0Tf#NuXe1!6(xaH?=3P8lb9o+H{C5p4SXs@IK68uin&^Qv7Lsa&#x*35Xfc zNbHNB=CTGNWC>s5o~gEfB>{n78oK%CH4>9fY9U#xZ_jZ0DFvI(=uT09&r7o&aX0`| zHT>l`uk`@7eYLeTx#ec{s~xU}D}HsiRB4Wn?>!|(k9tibB3)$Xn(~JqwG~YFc*(+p z83-1;a$7EP=bzi?vwQx){f95L)$&J>g5M@+$&YCv7WMUZLsv7;CykLyn8hi}W(R5P^<&5au?tNP)3KV&VkS@37MsP2v1b026 zaC}$I?hH>KJJJZg{_Y>Up3HPpL2zZw?m+q;h%?f74^8PQeLTZN4kvbV;hkxl!;3px z6?>rSYtNjoRezFC+iLr6@!TEDx$iPhDb%t<#A z**~GGHF^69U0;qo?5SMzH%!f!s0mBGHjQOMLk2Zq6TiO{6^y#EEM!FH3aq=w%1WOP z@ogQ9Ckm z<-!iHVcx0XBOk4tn54uqq4~i}>CS-s^Y@fpZ*UdDa<{1Kb|U#0>0FJ?ZZQHTk6H`B znh^N_dokJb3)cB^q1AuKypUbAxf4lc=eH~ggk|q1TztW?GI)JvEXs8a<*zZ-3PHt! zD|cQ;e1KIP(gYnF~$wFGp($^xg)9tuwHk$oO43$;5|Q)FCfxp)Jr zGiNiuNa=?iy74Svr!R8s& zsq%yUsULprrT2@S;Zay6k=c6BEDO3%1P~BAso(74q zY5L}3u2JU3K&!wdjY%HV^wT|DWha+u06GD@SlJyPl2@Nf#r$wLrw6tZ2Wqb`1aCl( zgm!wq-^SOJAu=t_RXRj2;+=x>P(WEs6dGP@U&QJo)e-+sZO=$%nSl)`fee%o_ag+9 z(4xIE`K;;ciM1J9SpJdoZ$*c%&;k`i@HK)vUb;h`p%HpW-E|!|kG2O2pms#c#kke? zDWDP^m+WB@N;PGTKvm2@-S3@0N7Y7@jWP=cMgxBD=_VVKhGw$F2MBLy3sAHK*$>n6 zI8X&OJJm5+`IhDo!fCH^E_D2!_G->suTC>8i?C&{tC-klebgeKeo*o?fMTx zN*0IJG9c5Vvoh6biGJ+}Zy?P;phRZ}CpnN45LLsh%vTl8q1SxbHw(Om|HG1Q^Nv0KKLLamCvS`3;cjW-IBGVHsZ+^{Gl;Q_WX5JLL_C^#xT3a zs5fO8r0K;U23P>54kNcfXeW22Rhypg+K-644;<0#pqOJjCGl!JZMll?!1bm$b|mLM zobf5ze(!18FuZ@o;q2)^&S1$=i-Oh>QQQ2U+>Mat10u@a`D58e{gaNn>7PxNJh zu%|}2xBD6$9C-{e6G}OiWK7(-f;Ov8_+}gzU6pA}r&*xgo7>CT2EkooD{?1soa--i znbnKe%Fpd~oO4t}Yc0<0j^$E`(PoA|d|QXj(8=bW5ACeYTat@{(sg7tJB>R&qM-eG z5!)82#;&$iJLk+Z@qm^<#umwTpJCdjOY_0t#y`u` zo$=YTcwG@d=h!qiLm9>v3tV#D1r5fUi$4A)$oDtl-5g4*YJ}(Nzb#RME!o-Knc3dG z0kYY4$RnEB4kBKA`;bl^O&>La2Epux8t)eBc+GhHxUDyECBDEoiN^sg{@j0UvwYW} zhY_bAvu97FT=(kqc@{{`HG+Ohf1y7}Jbuq5AAwQg8CQ_J3w49~KweOILOsbhkijkZ z4b6f&kDqM_{PW1|?s<_5lFMN&`7TDJ*|WIhlj>9j$PWri?>qlsYvTLnHeUE2ty~BD zsu}?faX035#olzvIy+IDL1g=3X;F5EJc{;THF$t(P`Lwa6(4QyOmVI!s2pFUJMHCC z0QT{x8{b})sX|ouwr7w24z%-wHav7Pu4#TrN84(3JY~H#k#Nt+*hS`*%RHRW*N$4+ zejXVIn!N*p@nS@hQsTS8NegoObcdF{4+w&3-!*9KE-2~I^1S3_n|X#i^NV9Df}8(L|VLWx$RE79v(8KA7(kbMHIX| z-?DSq*S>s7Ves~7n;=(iwQa~d{`UiRy0Pp7{eK#81sgyKc11>US$+*|XQ-V^5}0ZG z_EQW>LdGjN1ESFDN;s6A#*Xoz6b{$&E2WTqqdR70Wn(x&NdvX@OCkGI`bVTY3H-XvoThXSyiGM zm>ca`Rpws|5QTEaY&$PiGlEClZiqrxWTe}{u~mD_k?6kyK@U&*(6>$*-80Y*TR?S0 zpdY>qf@hEi{}td`0k!Gr;o(zg!C_(a(7HL%`!fTy#kmK#hOZT5k!(I8R`r4Ux^}w# z5r$5Mf-5DmKIc;0A=tl1rk{qwM13E0u3lCKT`r?VS<)bwEdR!hv42Nl2vR9YE(V~0 zt%ReEhRbzymJ1JKYzlvFv&A!0pKax*%1V0K)QAu$&;6&{Hj;T|f(O^FHhweS)@bm} zhY#a^6K>E-EJfO@)E9OQ6I zEblG|;49Yw_xY|u{$hJO)AM9QwpesTH~ffG_FQ7?cglpN#0{Nyp&($qR$vPDTIp3$ zUXC3R!2WqHN+lv%JlVv2yQHypr{Ftp^-ec-&2+pBT=iiQnmhCJ?U`8?pfr8V5?TaJ zjJt*R!L5=}`kPNpf{@jy?4R>Esh{P>;1W9yyKMZOl1Y6bS#M=!f*N+AAkO=9KFe-p ze4{L1LQgntzjv}|+&5bacCwiQ-~1%*JI*#^GY|!@o55cL_ORWrKzE#DX3$Cz8#hsc7;uXeT!$1)HE_XikMiqpi41mN_pG^?#vLNcevsp`&;RBEO6s=+bFC>)h7G{Yr^FWl60PtxS18a)4~aT7(as9sPB0Fjbzy! z69V=l6{oL+1=HMSf7Fs=mZuNG4UMSju{Hf!|&8l!_RCr#5e)mVM*13y^H~=?DcSwG2$o?KzW;n_c?oLfQIwp9|pCvMf>xQmU|qQTYs8|x=fAXF?QzeK&LgjPvEyBs00jKA^NMOSQ-Z1P2=_z1 zMeRAsvKZ$_8|@LTYLZcM{Jn+bE)0+shF%vX&(ofFoR0TLU(NT}gJWKz4M@(k1;Y3w z+Z8*%!P}zbCLEOFYnNv1lxnD_wO4%IAG4R4Xw%jJ!B$)f%2B?D0xncu4^AO-+B2x# zPoQZG?%Wuayn_B1^N90arX_TP?$|c0fn?e-o}=+#3KRZ8Su$xBv26TQG8TQaqde#@ zVvDEB1L~-|J)(CsBP)9j!^w1p-w4bbGw$3ReMgR#L5uJNl$+-1%)CY~J*c7Vo=ow` z8adQj@iTd)TxN2YR-9}`kC}IY4ZFSkutT28CG0L6TO|4{`mf#jz3@KP>MRcHyAVQE zZ>x5k3pV>Kk(dg~Y542CjB@_kCmbo+lgJ98g*|6iPo-jPSp2@<&1#Oxx0TWBd$rho zpThLYcnhNP_=eUsxg0LU;cV;MlXMw738jX3r~?Q7ytno4nPWt(IC?PHi##kv9Oh7_ z?9CgWXW2;w-xKePOWL1|)H*HIZZ>-}pagNzwyr+q&XaQK^HRjAdBa6{wk)m3+O<{!J0S+`N;-^D8V*A&}b#kk?qq*NcdU>Yi zIVME!KkgJDXUZd=V82~Aca~6+3xp6Bd&5ZJ8s?no@;A^-bOMiKzTQ~D?%WZDt}*vi zd%x8>x_#dcMMoYZQ1++*EvvT2#rWtTQc%2fw_+?aizDv?5S{&xQ}VFiT5qR9!hS~Y zDG40W);XCmT&{2kA114VwB0{w5$PN3qz1-*QdtImL=68CiPQ}^xTx)`tAf9UZOw;b zS2H9Smi(|5C4CQh2+yLIR}pi{(PwiF&=*?Fb|b( zJ)_|9N(FvqKmIkpH@OQJ0Ow6___5Qe?_bXz%uX1R{#_y}!C=>?1PsLuEDOXQzT573 z*il)TWph}1)v*5a%})E;h~nLfk*O!Xe#w9PBE=ki3mYD>FNo<3pblR5)!pR|yauRp zW%u_ZRwOef3q#b*9IHGUAd943_ii&EzWUO@Pk-FZdZC22)tm1NJ>vJ9AG`hC;9CXV z*c+fph$pGtxD^#nn})XL**&v;+FW*QhYW*_V@n_Ryo2l{n5M|5op;l^zgwlb2tO;^ ztuVK=B}ZosswyZdhV$^uTTA{dU&$e}(L0SfCN>wedkpDl-MvP7Wu0LU{yOz;h+oQx z8D4v_z40S>L+=_Z1hN>idlF-IfA!4K9#<7LSG!$H;*D05xJWyBSR7YRTd3jP2cARD zECh}Za*=0I(HC10)*<7uCQ(p)DXqec;fqEw0b11VkHVAmF$V-%Bu4FKfo0$#|1@^^ zi$sI-*42fC7z9~9{)6kIMWypf;00J2YYK8((JB<>px;DiO}{hzl3bm$N!%&tFLKT8 zr5e#q8UWxzUd94c%m~m8^4eboJ&!}Z`6BSMs5T;YZIu1ndC`#oAcJPjpl%Cd9DHMTsP}QeF>yvbJG@X(dt$ESlkZBqH)nCu<$jil5ENQ zj-cmXXh+aI#f&=hM)A5T`37fmqRUZ`Deu?<6;6c5|H$eUd+gDy$1bY~RX^B6AGwaJ zBDEF|-rmh`@^H8RJ`k|mT!yHwtJ-JP=PxE7`jPE@uk)QWON3nBOary(OWu9u@uV4Ne;A=rA(y6LJ&eE!Kj?7`gUE?UI z!)&jze|%-6rDmI6?dy_73cxLGh3{OjEL%t8z?W#J?hX}HuUCpBj;>4t0{Ct3M94aF zE*(@xhgE}9FtxhV+1{eUF1YmEhDkZ*$UP!z=_A+(-RK)YUWzBFC3-!1qM8yNLVJAP z+F>*1z0LjgOIWgaYyH#%1U~3HyGZ1xmaIynOz^FaxlefhWz7BoS8N;CDWkp`57#t`oKK&2G1-!LZR?=R*nzH zV(~qDnTUkd>bs?S5P(pBX|0?wnak_&-M`y(d!~(Vj?O50G;sZ(#29_v*5b^ytfuF~ z7f9!SOrA8!}H| zz#a>VK?iwSzgsbxq6080h)N$~@MuTTRT_?2Q6KR;mdw*xX6Vn7I!xBVKkA!W z>jpmyR1nW4IAexum;G>@*e}N2rb7G9$%LGxW7>83EAm(M6@@=(;HYr=%?OqSDO?Mp zdhZsV9}96Ymg9AC>lWb!c5A1jJu8Y*mlF@iI;eH;>B7$ zOo|1F&^*~}jS_$Ib~l$iPxB-^OwO^yz20Wvrim-0hUGVj+!vNc7@Kwk({{%bPhOZ-&Mk ztU(AfXs0Az7f5#I&^Ng<53MVkrl$;n6R!R~T&2TXF?dHw#wl3#i2+xYdj`UtZm@3a z?Q?{ckxV;*z-505BU7WgY;&E8&YMZC`h))O_mt>u)4>Rv+gU3%9CQIVGh+Y@!lBD{ zOO(I1fFe9&^-Pio%A0*aI^KB)f3I))oQqB%-&lc4a3JhnciSVf;E2H7S0ADk{wWgz zm{0QNbyn`V5Dgr57ciov2=xT|qnQK(2IWJ2OZmc@!Il zLzfHa!)wdX}170ldZwl(8m3ZPjQ2Cl}3gu5rT zmc{R(y>8Eqr;H1>GRmv%1Tmj=k>e@w$ntsHdO7v1S07@xJ;fX|1`kAjyjx+uI)LyO z7~6>I$9x5UCwhFxGe4p2&yb-f1nXaopT1BiD|mYnfp^Abk~N~sWf>;p2pR?|4{JlM zk-uWjm>djNY)nEq9EE;Jo>W(Oa1*ZM_pnO$Y-O^2%P(xPS8{~&ZvQH3u6Z?N`nAg} zRbd;5f6qtN9SF}A0IjjP(<3ve=?Q#^iugFSx?o{IfKB9p-|lK`0VYXL&`Q z?ufFGP`cX^P7~~YoLeH`VG0rdK8PcQ*ZsO)eBotHc1k7BI9atg^CnLKCYbETy!9MM zjGM;j_stEb>JzW5qnq3;G?^P_XRbb!t%7G9uJ+0MB$(>UB-L!(R?ysEKpt{T6}3P4HJ(3NG7%8mBCxfxkBs8HNyvn zM}hu2=SXS^Fh$AFQKnTo#)-VMb(d)hMQ8(Q`nH6dI@=xB$5~iEd-3I zgY#ncf;9M{vA8Frc-nBagNDPCLd!Ir;=ja}Y?fpde9Eqtx=nw@mBR^V8j*vLbEFWo zNCMQS2`7r`X?hnH430`IS_2FI|KijBUyFaM}aY2Y9((fTO2AN z+qm8ISK`g$&|OtMj5R#p%))h0%jC1nCKV1?D2sH*g!RXEX(*UGDiUb%TXc;*C6P1m zFf>6nZg-Cg&3bKx&nyrk&T`~;f6%3S;@5@OlL${r8|x>V(rbuFVp*MLE8v$S6;b+} zG(HnY+l;VEn1Hnd z!&rhHxA@5AA3wBk87iJ>Blc+fBE3YFJsQdgxVKUSJ8$vvv{DCqKR!@a{iHY=!6|4$ zX$f>rlOKhGHI%?S-yKzY-b1vW$FtIAL^2qL(g9bNw6;%EA(2=0=V*MZt67P%-Xh{t zpmV5Tf(>1B{CEhBBoc$U!l-GgzPr(E$!|b5NGmdr0_<-#A$;$CTE0jYXR6Qai79I9 zq_}0$UHMxEoy)U#Pv#t8?>kcbu21rUIGrojoB5=w`kRT4dj#xpAaQhlMAUka0|8(B!K&$tG@BRt&0gFgmfC8A%T#;s< z=l;s4FoHIvTYl(xUrfiST7f41w3ay-()aaW(p`{rgDZRJs^hcw7rJ>90Kp#a&&D@^ zXId)7s!}wwaq>pm5N#x`;h^f&bqEKEYuYV;eL8BIuTes|_~PIHDeevY7%Ps^GiIVu z6xpI=C-S_h5I|k>f)OHz=)U0EV&XwQ?X1; zb}H?3%mGHaBFmUSVHKpF^tGq%rF5mgq#Ai13Z?Va#}lhZHpEy4(fYcVX`T=SV^Q*X zn!(|QsiWZ2)>%w;*lMUl2v}1CG|4y}k0Pj$sUDzNQ|Y|QdW#-aUlWR zuDSe385hWK0V(LTO;mPXH{@;|v>0tQi_uM8yObcOBibTXWBN+@q?V~`e%T&AlPb=H zG|{&N_E*L5nCN9{z2~}IFA%jGm6N3EsrSA>Uq()7iT?F)aC)WO?M&`_zjJp23B;=R5ftKWk;^mmpHJ1LHDtA8X1qlDl=PHb2P+#{rv`Y;hb4U0N--us{` z`Sva3kgxZ33WJpV4Yhu;-U5)JDny#U0AWRA#&C=YH!HKY40SRSZ=dBQfR*roT~Jhn+Q7;J*r zcH@I4HBI4vPr{oKDMcURnx}-9r~vHh7L(S>-M8{lO>P%Hm7ip#uZi9GBct~;gjAh6 zfE_I_0)M^9@X@I%tqaEnSKEJe5)#JUegny31$17%hMP+hGG{>1~9|F8s# zcC7G`0fMv(q&PW5MNjFIzX1WD=?cswR%d;J6G7_*2%6xY;@Ida*BfKTM;IdPQ{UAB zx^AkzLXI@~#Yb!ePY2^y2WR=N^YGH|bog>Gba&JiG^DdIb~2l6r4&Cf){carzIf2~ z8XMT~EedTNAjnEj{*(tovv?sHZr6M%d{q~$ zw&PoW`^phmJIuPxL4?CTnI+CaV8l%^ga_p-6U}Hbi?5k<#F(XyMrDaqv!ej*dj-DB z`AZ(b!~_+G&)*wOOlB_?T1Txevn4n~8LUWXiB%0q7VpP%;*4=08N#dhN+DqIWuU4r zS&blpw}a493Pwg>+0YK2+X&uEEz+r_R_Ba_B=QYx9n#^Cac#Iz;t3R&BToTnZQ)W3 zw^K*ch}G@(&_0x1nxLy5f$_p@qOGur0VD9neI^(-UDe#w;^?ZkwsET-yGgb6)0X~-- z%(BtlG{s%?=WB9-f$^$&eTo|EX5lK3%b5Pu-8w*b^`9KS<@Pc|p!)XvDh;>>OZl8# zjuHfUYz1!n`}n%%G@*ZvYP+i7ZIxv#;SI zdxYt)T(WV%`*vPZifS1UaWJS@z&bbmL;lpp#tbY>#eb?25@4i@pZhBOTtk`SI zl+r){Aw4o-0+e)Cq}bI+#_wc-LEXRMyZ0cToZ0>B@Ng!7xlob{z5$Zo%#`nd;m;dH zmIKD{aX(gHzJ;H8ePMWFEDwG-`rr;=2M?2U;t+(qr6FNaB%6HvzAN#r4{>4R#Z#<> za&>QRA|C@&0CD;*6;AV(+sCZ1s+Tk6wFekBo&eDk0d&#HE)|+{MIJbtrp#ifJ=MS# zS~3+O-OLVQIy5Mh3F*~fgP7*XH^4>H&66O~T?k8(;scsiDLlt{`RssRWB9W@JI^ldT^7sIVmDe>&hD zXBi|=<8lde*!=1UU7mG(rNy910@(bjcR5mpQ&id6Yd7+P|1ZQ*dxCWjic=Ie=UI{l zS|y#1*G>P6^K9vBH%u{4E;V%n78GxJ-tjM%>H>8op|K7Pd*;!0i0m9v4!#?Ei5Z4-x#3rdG=~o$krT!($cfEL_M2q|?_a9WVopMY8J)}DfYs;L4tAOz2(armurkF#T z74GP-UH^EW!sMUrx8Gz4knTEM{`Yp*lxDP+HZ)b_et#7jB`~o52G>CpGOIs>YEbai zdaMkheJz}5dhVt9;6gjC!0h#FUIb#mgJpdp)^cbHJNQ&kr^;<|6%$1+)2#!%Td)Z;!u*vxZx2lz6g18Go5RKZa<~>=i z0V86$3!XEC%~|0FaZ+SmxD{+O^(bWkTM8>|z!-?iSREH%OpBb({J_GW{o$j(n#w0*kOsOCrJoKXw-ozY7Td{# zWyDK!^ixM^<{(DA^=|-X_Z#?r>^ZbH;oK9yheL?WLXrAlk{o!*idSEWTCBu1U|)PU zWItI2;mm7MDc#_BU)Tc2xgMNj%he*eny(ZLDO_86R%ki4U&u_vNaK1HJmzDG)z7Xo zlOt18cdq~M0x-{>zgZ5`m+je5HB< z&~|~X)8o~?x(w|sbsJ(O-QYAlzqz0oGP{B}@6Jp}0L{@qV=;Q=Cv-ccySMQ~4xho~ z+9KgiQk$En3IQ~ewV-!dCq&YnYUQSb7$LBO4LVIH?1JFu(KS^_0!SdnyA!3A@tu z0D72(P>LJBytQR0&^28}(Ft21{sI)%@K%Z@NpLrh@^C>TQk3A&t4qTH?WuTQIfp$k zkJ99G_*3yjwBpMzDB#(wb}&6YA3zFk5#fJ zxbTn>W7~(I0gzP>R`$JGBUPiaAk$HgIrueq$X%+E0bD^p!bAcS5GLp<#hKi1`_o85 Y#-G~IX0kXi3m-sg8E97Ba(MiI0CK`PKL7v# literal 0 HcmV?d00001 diff --git a/static/images/ui/project/task.png b/static/images/ui/project/task.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7423b7ccfc6616cbfdc6f6e8a6081c85e727a1 GIT binary patch literal 12948 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 literal 0 HcmV?d00001 diff --git a/static/images/ui/project/waterfall.png b/static/images/ui/project/waterfall.png new file mode 100644 index 0000000000000000000000000000000000000000..40cb5752a9f02457e473ba397ee037cee92ee3b1 GIT binary patch literal 5213 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 literal 0 HcmV?d00001