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

Feat/task screen time block #1738

Merged
merged 7 commits into from
Nov 8, 2023
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
2 changes: 1 addition & 1 deletion apps/mobile/app/components/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default Accordion
const styles = StyleSheet.create({
accordContainer: {
borderRadius: 8,
marginVertical: 5,
marginVertical: 6,
width: "100%",
},
accordHeader: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { Avatar } from "react-native-paper"
import { imgTitleProfileAvatar } from "../../../../helpers/img-title-profile-avatar"
import { typography, useAppTheme } from "../../../../theme"
import { limitTextCharaters } from "../../../../helpers/sub-text"

Check warning on line 8 in apps/mobile/app/components/Task/EstimateBlock/components/ProfileInfoWithTime.tsx

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Charaters)
import { useNavigation } from "@react-navigation/native"
import {
DrawerNavigationProp,
Expand All @@ -18,13 +18,15 @@
userId?: string
largerProfileInfo?: boolean
estimationsBlock?: boolean
time?: string
}

const ProfileInfoWithTime: React.FC<IProfileInfo> = ({
profilePicSrc,
names,
userId,
largerProfileInfo,
time,
}) => {
const { colors } = useAppTheme()

Expand All @@ -38,7 +40,13 @@
}, 50)
}
return (
<View style={{ flexDirection: "row", justifyContent: "space-between", paddingRight: 12 }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
paddingRight: !time && 12,
}}
>
<TouchableOpacity onPress={userId && navigateToProfile} style={styles.container}>
{profilePicSrc ? (
<Avatar.Image
Expand All @@ -62,13 +70,21 @@
fontWeight: "600",
}}
>
{limitTextCharaters({

Check warning on line 73 in apps/mobile/app/components/Task/EstimateBlock/components/ProfileInfoWithTime.tsx

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Charaters)
text: names.trim(),
numChars: 12,
})}
</Text>
</TouchableOpacity>
<Text style={{ fontSize: 12, fontWeight: "600", color: "#938FA3" }}>6 h: 40 m</Text>
<Text
style={{
fontSize: 12,
fontWeight: "600",
color: time ? colors.primary : "#938FA3",
}}
>
{time ? time : "6 h: 40 m"}
</Text>
</View>
)
}
Expand Down
336 changes: 336 additions & 0 deletions apps/mobile/app/components/Task/TimeBlock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
/* eslint-disable camelcase */
/* eslint-disable react-native/no-inline-styles */
/* eslint-disable react-native/no-color-literals */
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
import React, { useCallback, useEffect, useState } from "react"
import Accordion from "../../Accordion"
import { useStores } from "../../../models"
import { useOrganizationTeam } from "../../../services/hooks/useOrganization"
import useAuthenticateUser from "../../../services/hooks/features/useAuthentificateUser"

Check warning on line 9 in apps/mobile/app/components/Task/TimeBlock/index.tsx

View workflow job for this annotation

GitHub Actions / Cspell

Unknown word (Authentificate)
import { IOrganizationTeamList, OT_Member } from "../../../services/interfaces/IOrganizationTeam"
import { secondsToTime } from "../../../helpers/date"
import { ITasksTimesheet } from "../../../services/interfaces/ITimer"
import TaskRow from "../DetailsBlock/components/TaskRow"
import { ProgressBar } from "react-native-paper"
import { ITeamTask } from "../../../services/interfaces/ITask"
import { Feather } from "@expo/vector-icons"
import { useAppTheme } from "../../../theme"
import ProfileInfoWithTime from "../EstimateBlock/components/ProfileInfoWithTime"
import { translate } from "../../../i18n"

export interface ITime {
hours: number
minutes: number
}

const TimeBlock = () => {
const {
TaskStore: { detailedTask: task },
} = useStores()
const { currentTeam: activeTeam } = useOrganizationTeam()
const { user } = useAuthenticateUser()
const { colors } = useAppTheme()

const [userTotalTime, setUserTotalTime] = useState<ITime>({
hours: 0,
minutes: 0,
})
const [userTotalTimeToday, setUserTotalTimeToday] = useState<ITime>({
hours: 0,
minutes: 0,
})
const [timeRemaining, setTimeRemaining] = useState<ITime>({
hours: 0,
minutes: 0,
})
const [groupTotalTime, setGroupTotalTime] = useState<ITime>({
hours: 0,
minutes: 0,
})

const members = activeTeam?.members || []

const currentUser: OT_Member | undefined = members.find((m) => {
return m.employee.user?.id === user?.id
})

const userTotalTimeOnTask = useCallback((): void => {
const totalOnTaskInSeconds: number =
currentUser?.totalWorkedTasks?.find((object) => object.id === task?.id)?.duration || 0

const { h, m } = secondsToTime(totalOnTaskInSeconds)

setUserTotalTime({ hours: h, minutes: m })
}, [currentUser?.totalWorkedTasks, task?.id])

useEffect(() => {
userTotalTimeOnTask()
}, [userTotalTimeOnTask])

const userTotalTimeOnTaskToday = useCallback((): void => {
const totalOnTaskInSeconds: number =
currentUser?.totalTodayTasks?.find((object) => object.id === task?.id)?.duration || 0

const { h, m } = secondsToTime(totalOnTaskInSeconds)

setUserTotalTimeToday({ hours: h, minutes: m })
}, [currentUser?.totalTodayTasks, task?.id])

useEffect(() => {
userTotalTimeOnTaskToday()
}, [userTotalTimeOnTaskToday])

useEffect(() => {
const matchingMembers: OT_Member[] | undefined = activeTeam?.members.filter((member) =>
task?.members.some((taskMember) => taskMember.id === member.employeeId),
)

const usersTaskArray: ITasksTimesheet[] | undefined = matchingMembers
?.flatMap((obj) => obj.totalWorkedTasks)
.filter((taskObj) => taskObj?.id === task?.id)

const usersTotalTimeInSeconds: number | undefined = usersTaskArray?.reduce(
(totalDuration, item) => totalDuration + item.duration,
0,
)

const usersTotalTime: number =
usersTotalTimeInSeconds === null || usersTotalTimeInSeconds === undefined
? 0
: usersTotalTimeInSeconds

const timeObj = secondsToTime(usersTotalTime)
const { h: hoursTotal, m: minutesTotal } = timeObj
setGroupTotalTime({ hours: hoursTotal, minutes: minutesTotal })

const remainingTime: number =
task?.estimate === null ||
task?.estimate === 0 ||
task?.estimate === undefined ||
usersTotalTimeInSeconds === undefined
? 0
: task?.estimate - usersTotalTimeInSeconds

const { h, m } = secondsToTime(remainingTime)
setTimeRemaining({ hours: h, minutes: m })
if (remainingTime <= 0) {
setTimeRemaining({ hours: 0, minutes: 0 })
}
}, [task?.members, task?.id, task?.estimate])

const getTimePercentage = () => {
if (task) {
if (!task.estimate) {
return 0
}

let taskTotalDuration = 0
activeTeam?.members?.forEach((member) => {
const totalWorkedTasks =
member?.totalWorkedTasks?.find((item) => item.id === task?.id) || null

if (totalWorkedTasks) {
taskTotalDuration += totalWorkedTasks.duration
}
})

return taskTotalDuration ? taskTotalDuration / task?.estimate : 0
} else {
return 0
}
}

return (
<Accordion title={translate("taskDetailsScreen.time")}>
<View style={{ paddingBottom: 12, gap: 12 }}>
{/* Progress Bar */}
<TaskRow
alignItems={true}
labelComponent={
<View style={[styles.labelComponent, { marginLeft: 12 }]}>
<Text style={styles.labelText}>
{translate("taskDetailsScreen.progress")}
</Text>
</View>
}
>
<Progress task={task} percent={getTimePercentage} />
</TaskRow>
{/* Total Time */}
<TaskRow
alignItems={true}
labelComponent={
<View style={[styles.labelComponent, { marginLeft: 12 }]}>
<Text style={styles.labelText}>
{translate("tasksScreen.totalTimeLabel")}
</Text>
</View>
}
>
<Text style={[styles.timeValues, { color: colors.primary }]}>
{userTotalTime.hours}h : {userTotalTime.minutes}m
</Text>
</TaskRow>
{/* Total Time Today */}
<TaskRow
alignItems={true}
labelComponent={
<View style={[styles.labelComponent, { marginLeft: 12 }]}>
<Text style={styles.labelText}>
{translate("taskDetailsScreen.timeToday")}
</Text>
</View>
}
>
<Text style={[styles.timeValues, { color: colors.primary }]}>
{userTotalTimeToday.hours}h : {userTotalTimeToday.minutes}m
</Text>
</TaskRow>
{/* Total Group Time */}
{/* TODO */}
<TaskRow
labelComponent={
<View style={[styles.labelComponent, { marginLeft: 12, marginTop: 3 }]}>
<Text style={styles.labelText}>
{translate("taskDetailsScreen.totalGroupTime")}
</Text>
</View>
}
>
<TotalGroupTime
totalTime={`${groupTotalTime.hours}h : ${groupTotalTime.minutes}m`}
task={task}
activeTeam={activeTeam}
/>
</TaskRow>
{/* Time Remaining */}
<TaskRow
alignItems={true}
labelComponent={
<View style={[styles.labelComponent, { marginLeft: 12 }]}>
<Text style={styles.labelText}>
{translate("taskDetailsScreen.timeRemaining")}
</Text>
</View>
}
>
<Text style={[styles.timeValues, { color: colors.primary }]}>
{timeRemaining.hours}h : {timeRemaining.minutes}m
</Text>
</TaskRow>
</View>
</Accordion>
)
}

export default TimeBlock

interface IProgress {
task: ITeamTask
percent: () => number
}

const Progress: React.FC<IProgress> = ({ task, percent }) => {
return (
<View style={styles.progressBarContainer}>
<View style={{ width: "79%" }}>
<ProgressBar
style={styles.progressBar}
progress={percent()}
color={task && task.estimate > 0 ? "#27AE60" : "#F0F0F0"}
/>
</View>
<Text style={{ fontSize: 12, color: "#28204880" }}>{Math.floor(percent() * 100)}%</Text>
</View>
)
}

interface ITotalGroupTime {
totalTime: string
task: ITeamTask
activeTeam: IOrganizationTeamList
}

const TotalGroupTime: React.FC<ITotalGroupTime> = ({ totalTime, activeTeam, task }) => {
const [expanded, setExpanded] = useState(false)
const { colors } = useAppTheme()

function toggleItem() {
setExpanded(!expanded)
}

const matchingMembers: OT_Member[] | undefined = activeTeam?.members.filter((member) =>
task?.members.some((taskMember) => taskMember.id === member.employeeId),
)

const findUserTotalWorked = (user: OT_Member, id: string | undefined) => {
return user?.totalWorkedTasks.find((task: any) => task?.id === id)?.duration || 0
}

return (
<View style={[styles.accordContainer, { backgroundColor: colors.background }]}>
<TouchableOpacity style={styles.accordHeader} onPress={toggleItem}>
<Text style={[styles.accordTitle, { color: colors.primary }]}>{totalTime}</Text>
<Feather
name={expanded ? "chevron-up" : "chevron-down"}
size={20}
color={colors.primary}
/>
</TouchableOpacity>
{expanded && <View style={{ marginBottom: 5 }} />}
<View style={{ gap: 7 }}>
{expanded &&
matchingMembers?.map((member, idx) => {
const taskDurationInSeconds = findUserTotalWorked(member, task?.id)

const { h, m } = secondsToTime(taskDurationInSeconds)

const time = `${h}h : ${m}m`

return (
<ProfileInfoWithTime
key={idx}
names={member?.employee?.fullName}
profilePicSrc={member?.employee?.user?.imageUrl}
userId={member?.employee?.userId}
time={time}
/>
)
})}
</View>
</View>
)
}

const styles = StyleSheet.create({
accordContainer: {
paddingRight: 12,
width: "100%",
},
accordHeader: {
alignItems: "center",
flexDirection: "row",
justifyContent: "space-between",
},
accordTitle: {
fontSize: 12,
fontWeight: "600",
},
labelComponent: {
alignItems: "center",
flexDirection: "row",
gap: 7,
},
labelText: {
color: "#A5A2B2",
fontSize: 12,
},
progressBar: { backgroundColor: "#E9EBF8", borderRadius: 3, height: 6, width: "100%" },
progressBarContainer: {
alignItems: "center",
flexDirection: "row",
justifyContent: "space-between",
paddingRight: 12,
},
timeValues: { fontSize: 12, fontWeight: "600" },
})
Loading
Loading