diff --git a/.cspell.json b/.cspell.json index beb86e6fa..be2df850e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -355,6 +355,7 @@ "tailess", "Tailess", "tailwindcss", + "timesheet-viewMode", "tanstack", "taskid", "taskstatus", diff --git a/apps/web/app/[locale]/timesheet/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/components/CalendarView.tsx new file mode 100644 index 000000000..b7a4fd146 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/CalendarView.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export function CalendarView() { + return ( +
+ +
+ ) +} diff --git a/apps/web/app/[locale]/timesheet/components/FilterWithStatus.tsx b/apps/web/app/[locale]/timesheet/components/FilterWithStatus.tsx new file mode 100644 index 000000000..ed38c2d23 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/FilterWithStatus.tsx @@ -0,0 +1,35 @@ +import React, { HTMLAttributes } from 'react'; +import { Button } from 'lib/components'; +import { clsxm } from '@app/utils'; + +export type FilterStatus = "All Tasks" | "Pending" | "Approved" | "Rejected"; +export function FilterWithStatus({ + activeStatus, + onToggle, + className +}: { + activeStatus: FilterStatus; + onToggle: (status: FilterStatus) => void; + className?: HTMLAttributes +}) { + const buttonData: { label: FilterStatus; count: number; icon: React.ReactNode }[] = [ + { label: 'All Tasks', count: 46, icon: }, + { label: 'Pending', count: 12, icon: }, + { label: 'Approved', count: 28, icon: }, + { label: 'Rejected', count: 6, icon: }, + ]; + + return ( +
+ {buttonData.map(({ label, count, icon }, index) => ( + + ))} +
+ ); +} diff --git a/apps/web/app/[locale]/timesheet/components/FrequencySelect.tsx b/apps/web/app/[locale]/timesheet/components/FrequencySelect.tsx new file mode 100644 index 000000000..656e63aa0 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/FrequencySelect.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@components/ui/select" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@components/ui/dropdown-menu" +import { Button } from "lib/components/button"; + +export function FrequencySelect() { + const [selectedValue, setSelectedValue] = React.useState(undefined); + + const handleSelectChange = (value: string) => { + setSelectedValue(value); + }; + + return ( + + ); +} + + + + +export const FilterTaskActionMenu = () => { + // const handleCopyPaymentId = () => navigator.clipboard.writeText(idTasks); + return ( + + + + + + + Today + + + Last 7 days + + + Last 30 days + + + This year (2024) + {/* ({new Date().getFullYear()}) */} + + {/* */} + + + + ); +}; + +export const CustomDateRange = () => { + return ( + + + Custom Date Range + + + + +
+
+ Calendar +
+
+
+
+
+ ) +} diff --git a/apps/web/app/[locale]/timesheet/components/TimesheetCard.tsx b/apps/web/app/[locale]/timesheet/components/TimesheetCard.tsx new file mode 100644 index 000000000..687473020 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/TimesheetCard.tsx @@ -0,0 +1,65 @@ + +import { clsxm } from '@app/utils'; +import { ArrowRightIcon } from 'assets/svg'; +import { Button, Card } from 'lib/components'; +import React, { ReactNode } from 'react' + +interface ITimesheetCard { + title?: string; + date?: string + description?: string; + hours?: string; + count?: number; + color?: string; + icon?: ReactNode; + classNameIcon?: string + onClick?: () => void; +} + + +export function TimesheetCard({ ...props }: ITimesheetCard) { + const { icon, title, date, description, hours, count, onClick, classNameIcon } = props; + return ( + +
+
+

{hours ?? count}

+

{title}

+ {date ?? description} +
+ +
+ +
+ ) +} diff --git a/apps/web/app/[locale]/timesheet/components/TimesheetFilter.tsx b/apps/web/app/[locale]/timesheet/components/TimesheetFilter.tsx new file mode 100644 index 000000000..179d5ccc8 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/TimesheetFilter.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { FilterWithStatus } from './FilterWithStatus' +import { FrequencySelect } from '.'; +import { Button } from 'lib/components'; +import { SettingFilterIcon } from 'assets/svg'; + +export function TimesheetFilter() { + return ( +
+
+ { + console.log(label) + }} + /> +
+
+
+
+ + + +
+
+
+ + ) +} diff --git a/apps/web/app/[locale]/timesheet/components/TimesheetView.tsx b/apps/web/app/[locale]/timesheet/components/TimesheetView.tsx new file mode 100644 index 000000000..91b1d73d5 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/TimesheetView.tsx @@ -0,0 +1,10 @@ +import { DataTableTimeSheet } from 'lib/features/integrations/calendar' +import React from 'react' + +export function TimesheetView() { + return ( +
+ +
+ ) +} diff --git a/apps/web/app/[locale]/timesheet/components/index.tsx b/apps/web/app/[locale]/timesheet/components/index.tsx new file mode 100644 index 000000000..12cf616a7 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/components/index.tsx @@ -0,0 +1,6 @@ +export * from './TimesheetCard'; +export * from './TimesheetView'; +export * from './CalendarView'; +export * from './TimesheetFilter'; +export * from './FrequencySelect'; +export * from './FilterWithStatus'; diff --git a/apps/web/app/[locale]/timesheet/page.tsx b/apps/web/app/[locale]/timesheet/page.tsx new file mode 100644 index 000000000..c278f7fa5 --- /dev/null +++ b/apps/web/app/[locale]/timesheet/page.tsx @@ -0,0 +1,162 @@ +"use client" +import React, { useMemo } from 'react'; +import { useParams } from 'next/navigation'; +import { useTranslations } from 'next-intl'; + +import { withAuthentication } from 'lib/app/authenticator'; +import { Breadcrumb, Container, Divider } from 'lib/components'; +import { Footer, MainLayout } from 'lib/layout'; + +import { useLocalStorageState, useOrganizationTeams } from '@app/hooks'; +import { clsxm } from '@app/utils'; +import { fullWidthState } from '@app/stores/fullWidth'; +import { useAtomValue } from 'jotai'; +import { ArrowLeftIcon } from 'assets/svg'; +import { CalendarView, TimesheetCard, TimesheetFilter, TimesheetView } from './components'; +import { CalendarDaysIcon, Clock, User2 } from 'lucide-react'; +import { GrTask } from "react-icons/gr"; +import { GoSearch } from "react-icons/go"; + +type TimesheetViewMode = "ListView" | "CalendarView" +type ViewToggleButtonProps = { + mode: TimesheetViewMode; + active: boolean; + icon: React.ReactNode; + onClick: () => void; +}; + +function TimeSheetPage() { + const t = useTranslations(); + const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState('timesheet-viewMode', 'ListView'); + + const fullWidth = useAtomValue(fullWidthState); + const { isTrackingEnabled, activeTeam } = useOrganizationTeams(); + + const params = useParams<{ locale: string }>(); + const currentLocale = params ? params.locale : null; + const breadcrumbPath = useMemo( + () => [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: 'Timesheet', href: `/${currentLocale}/timesheet` } + ], + [activeTeam?.name, currentLocale, t] + ); + return ( + < > + +
+ +
+
+ + +
+
+
+
+
+ +
+
+

Good morning, Ruslan !

+ This is your personal timesheet dashboard, showing you what needs your attention now. +
+
+ } + classNameIcon='bg-[#FBB650] shadow-[#fbb75095]' + /> + } + classNameIcon='bg-[#3D5A80] shadow-[#3d5a809c] ' + /> + } + classNameIcon='bg-[#30B366] shadow-[#30b3678f]' + /> +
+
+
+
+ } + mode='ListView' + active={timesheetNavigator === 'ListView'} + onClick={() => setTimesheetNavigator('ListView')} + /> + } + mode='CalendarView' + active={timesheetNavigator === 'CalendarView'} + onClick={() => setTimesheetNavigator('CalendarView')} + /> +
+
+ + +
+
+ {/* */} +
+ +
+ {timesheetNavigator === 'ListView' ? + + : + } +
+
+
+
+
+ + + ) +} + +export default withAuthentication(TimeSheetPage, { displayName: 'TimeSheet' }); + + +const FooterTimeSheet = ({ fullWidth }: { fullWidth: boolean }) => { + return ( +
+ +
+
+ ) +} +const ViewToggleButton: React.FC = ({ + mode, + active, + icon, + onClick +}) => ( + +); diff --git a/apps/web/app/stores/user.ts b/apps/web/app/stores/user.ts index e09c7752e..7d5285e7e 100644 --- a/apps/web/app/stores/user.ts +++ b/apps/web/app/stores/user.ts @@ -3,3 +3,4 @@ import { atom } from 'jotai'; export const userState = atom(null); export const userDetailAccordion = atom(''); +export const stayOpen = atom(false) diff --git a/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx index d5ff98662..e6c9789a3 100644 --- a/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx +++ b/apps/web/lib/features/integrations/calendar/setup-full-calendar.tsx @@ -47,8 +47,6 @@ export function SetupFullCalendar() { extendedProps: { icon: , }, - - }, { id: '13', diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx index f18ae4c5e..7f479781d 100644 --- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx +++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx @@ -256,8 +256,7 @@ export function DataTableTimeSheet() { + className="h-24 text-center"> No results. @@ -417,7 +416,7 @@ const TaskDetails = ({ description, name }: { description: string; name: string {name} -
{description}
+
{description}
); }; diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 11a53ea4c..4bb960e9f 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -226,9 +226,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); diff --git a/apps/web/lib/features/task/task-input-kanban.tsx b/apps/web/lib/features/task/task-input-kanban.tsx index c25ea6de7..59000339f 100644 --- a/apps/web/lib/features/task/task-input-kanban.tsx +++ b/apps/web/lib/features/task/task-input-kanban.tsx @@ -169,9 +169,9 @@ export function TaskInputKanban(props: Props) { }, [editMode, setFilter]); /* - If task is passed then we don't want to set the active task for the authenticated user. - after task creation - */ + If task is passed then we don't want to set the active task for the authenticated user. + after task creation + */ const handleTaskCreation = useCallback(async () => { /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */ datas && diff --git a/apps/web/lib/features/task/task-input.tsx b/apps/web/lib/features/task/task-input.tsx index 7ca9cc253..213990f05 100644 --- a/apps/web/lib/features/task/task-input.tsx +++ b/apps/web/lib/features/task/task-input.tsx @@ -234,10 +234,10 @@ export function TaskInput(props: Props) { }, [editMode, setFilter]); /* - If task is passed then we don't want to set the active task for the authenticated user. - after task creation - */ - const autoActiveTask = props.task !== undefined ? false : true; + If task is passed then we don't want to set the active task for the authenticated user. + after task creation + */ + const autoActiveTask: boolean = props.task === undefined; const handleTaskCreation = useCallback(() => { /* Checking if the `handleTaskCreation` is available and if the `hasCreateForm` is true. */ datas && @@ -322,6 +322,14 @@ export function TaskInput(props: Props) { inputTaskTitle, setEditMode ]); + const [openPopoverId, setOpenPopoverId] = useState(null); + const handlePopoverToggle = useCallback((popoverId: string) => { + if (openPopoverId === popoverId) { + setOpenPopoverId(null); + } else { + setOpenPopoverId(popoverId); + } + }, [openPopoverId]); // Handling Hotkeys const handleCommandKeySequence = useCallback(() => { @@ -424,7 +432,7 @@ export function TaskInput(props: Props) { defaultIssueType ? defaultIssueType.name : (localStorage.getItem('lastTaskIssue') as ITaskIssue) || - null + null } /> )} @@ -461,7 +469,9 @@ export function TaskInput(props: Props) { return viewType === 'one-view' ? ( taskCard ) : ( - + handlePopoverToggle('popover1')} + className="relative z-20 w-full" ref={inputRef}> { return ( <>
e.stopPropagation()} >
@@ -186,8 +187,8 @@ export const TaskLabelForm = ({ formOnly = false, onCreated }: StatusForm) => { active={ edit ? (iconList.find( - (icon) => icon.path === edit.icon - ) as IIcon) + (icon) => icon.path === edit.icon + ) as IIcon) : null } />