Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the approve issue. #402

Merged
merged 1 commit into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 62 additions & 3 deletions client/src/components/NotificationDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,27 @@
</v-card>
</v-col>
</v-row>

<v-card-actions class="d-flex justify-end">
<v-btn color="info" variant="flat" @click="closeDialog()">Close</v-btn>
<v-row class="d-flex justify-end mt-3">
<div v-if="couldApprove && vacationStatus == 'pending'">
<v-btn variant="tonal" color="primary" class="ma-1" @click="handleApprove">Approve</v-btn>
<v-btn variant="tonal" color="error" class="ma-1" @click="handleReject">Reject</v-btn>
<v-spacer></v-spacer>
</div>
<v-btn color="info" class="ma-1" variant="flat" @click="closeDialog()">Close</v-btn>
</v-row>
</v-card-actions>
</v-card>
</template>

<script lang="ts">
import { computed, type PropType } from 'vue'
import { computed, ref, type PropType } from 'vue'
import { getStatusColor } from '@/utils'
import type { Api } from '@/types'
import { ApiClientBase } from '@/clients/api/base'
import { useAsyncState } from '@vueuse/core'
import { $api } from '@/clients'

export default {
name: 'NotificationDetails',
Expand All @@ -71,22 +83,69 @@ export default {
type: Array as PropType<any[]>,
required: true
},
vacation: {
type: Object as PropType<Api.Vacation>,
required: true
},
onClose: {
type: Function as PropType<() => void>,
required: true
}
},
setup(props) {
const color = computed(() => getStatusColor(props.status))
const user = ApiClientBase.user
const vacationStatus = ref(props.status)

const closeDialog = () => {
props.onClose()
}

async function handleApprove() {
return useAsyncState($api.vacations.approve.update(props.vacation.id), [], {
onSuccess() {
vacationStatus.value = 'approved'
}
})
}

async function handleReject() {
return useAsyncState($api.vacations.reject.update(props.vacation.id), [], {
onSuccess() {
vacationStatus.value = 'rejected'
}
})
}

const couldApprove = computed(() => {
const vacation = props.vacation
if (user.value && vacation) {
// supervisor could approve vacation
if (
user.value.fullUser.user_type.toLocaleLowerCase() === 'admin' ||
user.value.fullUser.user_type.toLocaleLowerCase() === 'supervisor'
) {
// Could approve if applying user reports to the logged in supervisor
if (
vacation.applying_user.reporting_to &&
vacation.applying_user.reporting_to[0]?.id === user.value.fullUser.id
) {
return true
}
return false
}
}
return false
})

return {
color,
couldApprove,
vacationStatus,
closeDialog,
getStatusColor
getStatusColor,
handleApprove,
handleReject
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions client/src/components/NotificationDetailsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
:eventId="notification.state.value.event_id"
:sections="getSections(notification.state.value)"
:status="notification.state.value.status"
:vacation="
notification.state.value.type === 'vacations' ? notification.state.value : undefined
"
@close="closeDialog"
/>
</VDialog>
Expand Down Expand Up @@ -86,8 +89,11 @@ export default {
{
title: 'Request Details',
details: [
{ label: 'From Date', value: data.from_date },
{ label: 'End Date', value: data.end_date }
{
label: 'From Date',
value: `${new Date(data.from_date).toDateString()}`
},
{ label: 'End Date', value: `${new Date(data.end_date).toDateString()}` }
]
},
{
Expand All @@ -100,8 +106,8 @@ export default {
{
title: 'Approval User',
details: [
{ label: 'Name', value: data.approval_user.full_name },
{ label: 'Email', value: data.approval_user.email }
{ label: 'Name', value: data.approval_user.full_name || '-' },
{ label: 'Email', value: data.approval_user.email || '-' }
]
}
]
Expand Down
16 changes: 5 additions & 11 deletions client/src/components/cards/vacationCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,15 @@ export default {
const couldApprove = computed(() => {

if (user.value) {
// only admins or supervisors could approve vacation
// supervisor could approve vacation
if (
user.value.fullUser.user_type === 'Admin' ||
user.value.fullUser.user_type === 'Supervisor'
user.value.fullUser.user_type.toLocaleLowerCase() === 'admin' ||
user.value.fullUser.user_type.toLocaleLowerCase() === 'supervisor'
) {
// Could approve if user signed in is the same user applied for vacation
if (props.vacation.applying_user.id == user.value.fullUser.id) {
return true
}
// Could approve if applying user reports to the supervisor or admin logged in
// and works from the same office
// Could approve if applying user reports to the logged in supervisor
if (
props.vacation.applying_user.reporting_to &&
props.vacation.applying_user.reporting_to[0]?.id === props.vacation.applying_user?.id &&
props.vacation.applying_user.location.name === user.value.fullUser.location.name
props.vacation.applying_user.reporting_to[0]?.id === user.value.fullUser.id
) {
return true
}
Expand Down
5 changes: 4 additions & 1 deletion server/cshr/serializers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ class BaseUserSerializer(ModelSerializer):
skills = SerializerMethodField()
user_certificates = SerializerMethodField()
location = SerializerMethodField()
reporting_to = SerializerMethodField()

class Meta:
model = User
Expand All @@ -334,6 +335,7 @@ class Meta:
"user_certificates",
"is_active",
"telegram_link",
"reporting_to",
"location"
]

Expand All @@ -350,7 +352,8 @@ def get_user_certificates(self, obj):
training_courses = get_training_courses_for_a_user(obj.id)
return TrainingCoursesSerializer(training_courses, many=True).data


def get_reporting_to(self, obj):
return BasicUserSerializer(obj.reporting_to, many=True).data

class TeamSerializer(ModelSerializer):
"""Class team serilaizer to return user team leaders and team members."""
Expand Down
53 changes: 42 additions & 11 deletions server/cshr/utils/redis_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from components import config
import redis
import json
from datetime import datetime


from cshr.api.response import CustomResponse

Expand All @@ -23,7 +25,9 @@
def set_notification_request_redis(data: Dict) -> bool:
"""this function set requests notifications"""
applying_user = None
if type(data.get("applying_user")) is not int and data.get("applying_user").get("id"):
if type(data.get("applying_user")) is not int and data.get("applying_user").get(
"id"
):
applying_user = data["applying_user"]["id"]
else:
applying_user = data["applying_user"]
Expand Down Expand Up @@ -110,30 +114,56 @@ def notification_commented(data: Dict, user, state: str, event_id: int):


def get_notifications(user: User):
"""this function returns all notifications for certain user"""
keys = redis_instance.keys("user" + str(user.id) + "*")
"""
Retrieve all notifications for a specific user.

Args:
user (User): The user for whom notifications are to be retrieved.

Returns:
list: A list of dictionaries containing notification data.
"""
notifications = []
val = ""
keys = redis_instance.keys("user" + str(user.id) + "*")

for key in keys:
val = redis_instance.hgetall(key)
dval = dict((k.decode("utf8"), v.decode("utf8")) for k, v in val.items())
noti_user_id: int = json.loads(dval.get("user"))["id"]
noti_id: int = int(json.loads(dval.get("event_id")))

# Parse notification data
noti_user_id = json.loads(dval.get("user"))["id"]
noti_id = int(json.loads(dval.get("event_id")))

# Check if the user associated with the notification exists
try:
usernt = User.objects.get(
id=noti_user_id
) # Check if the user is not deleted
usernt = User.objects.get(id=noti_user_id)
dval["user"] = BaseUserSerializer(usernt).data
except User.DoesNotExist:
redis_instance.delete(key)

# Check if the request associated with the notification exists
try:
Requests.objects.get(id=noti_id) # Check if the Request is not deleted
Requests.objects.get(id=noti_id)
except Requests.DoesNotExist:
redis_instance.delete(key)

notifications.append(dval)

return notifications


def sort_notifications_by_created_at(notifications):
# Custom key function to extract timestamp from the notification
def get_timestamp(notification):
created_at_str = notification["created_at"]
# Parse the timestamp string and convert it to datetime object
return datetime.strptime(created_at_str, "%Y-%m-%d | %H:%M")

# Sort the notifications based on the created_at timestamp
sorted_notifications = sorted(notifications, key=get_timestamp, reverse=True)
return sorted_notifications


def ping_redis():
try:
redis_instance.ping()
Expand All @@ -146,11 +176,12 @@ def ping_redis():
def get_redis_conf() -> Dict[str, str]:
return {"host": R_HOST, "port": R_PORT}


def http_ensure_redis_error():
return CustomResponse.bad_request(
message="Connection Refused",
error={
"message": "Redis is not running, please make sure that you run the Redis server on the provided values",
"values": {"host": R_HOST, "port": R_PORT},
},
)
)
5 changes: 2 additions & 3 deletions server/cshr/utils/vacation_balance_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,10 @@ def check_and_update_balance(
if hasattr(v, reason):
curr_balance = getattr(v, reason)
if vacation.status == STATUS_CHOICES.APPROVED:
if delete:
new_value: int= curr_balance + vacation_days
if delete:
new_value: int= curr_balance + vacation_days
return self.update_user_balance(applying_user, reason, new_value)
if curr_balance >= vacation_days:

if vacation.status == STATUS_CHOICES.PENDING:
new_value: int = curr_balance - vacation_days
return self.update_user_balance(applying_user, reason, new_value)
Expand Down
5 changes: 3 additions & 2 deletions server/cshr/views/notifications.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from cshr.utils.redis_functions import get_notifications, http_ensure_redis_error, ping_redis
from cshr.utils.redis_functions import get_notifications, http_ensure_redis_error, ping_redis, sort_notifications_by_created_at
from rest_framework.generics import GenericAPIView, ListAPIView
from cshr.api.permission import UserIsAuthenticated
from rest_framework.response import Response
Expand All @@ -15,4 +15,5 @@ def get(self, request: Request) -> Response:
except:
return http_ensure_redis_error()
res = get_notifications(request.user)
return CustomResponse.success(data=res)
sorted_notifications = sort_notifications_by_created_at(res)
return CustomResponse.success(data=sorted_notifications)
2 changes: 1 addition & 1 deletion server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"
TIME_ZONE = 'Africa/Cairo'

USE_I18N = True

Expand Down
Loading