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