From eb05f7030a151372640180f8860e036dee3a9826 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:23:26 +0200 Subject: [PATCH] [Feat]: Set Working Hours (#3501) * feat: add TimePicker component with time selection and input editing * feat(WorkingHours): add configurable working hours component * fix: add timezone component * feat: create reusable ToggleSwitch component with dynamic styles and icons --- .../app/[locale]/settings/personal/page.tsx | 7 + .../[locale]/timesheet/[memberId]/page.tsx | 35 ++-- apps/web/app/hooks/useLeftSettingData.ts | 5 + apps/web/components/ui/time-picker.tsx | 125 +++++++++++++ apps/web/lib/components/toggler.tsx | 17 +- apps/web/lib/settings/working-hours.tsx | 165 ++++++++++++++++++ yarn.lock | 155 +++++++--------- 7 files changed, 394 insertions(+), 115 deletions(-) create mode 100644 apps/web/components/ui/time-picker.tsx create mode 100644 apps/web/lib/settings/working-hours.tsx diff --git a/apps/web/app/[locale]/settings/personal/page.tsx b/apps/web/app/[locale]/settings/personal/page.tsx index 63bb4889d..cc38d86e4 100644 --- a/apps/web/app/[locale]/settings/personal/page.tsx +++ b/apps/web/app/[locale]/settings/personal/page.tsx @@ -6,6 +6,7 @@ import { Accordian } from 'lib/components/accordian'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { SyncZone } from 'lib/settings/sync.zone'; +import { WorkingHours } from '@/lib/settings/working-hours'; const Personal = () => { const t = useTranslations(); @@ -28,6 +29,12 @@ const Personal = () => { + + + ) : ( - + + {selectTimesheetId.length > 0 && } - loading={loadingTimesheet} - /> + )} {shouldRenderPagination && ( - {selectTimesheetId.length > 0 && - } + diff --git a/apps/web/app/hooks/useLeftSettingData.ts b/apps/web/app/hooks/useLeftSettingData.ts index 60172af88..940e8e408 100644 --- a/apps/web/app/hooks/useLeftSettingData.ts +++ b/apps/web/app/hooks/useLeftSettingData.ts @@ -8,6 +8,11 @@ export const useLeftSettingData = () => { color: '#7E7991', href: '#general' }, + { + title: 'Working hours', + color: '#7E7991', + href: '#working-hours', + }, // { // title: t('pages.settingsPersonal.WORK_SCHEDULE'), // color: '#7E7991', diff --git a/apps/web/components/ui/time-picker.tsx b/apps/web/components/ui/time-picker.tsx new file mode 100644 index 000000000..472cfa3fd --- /dev/null +++ b/apps/web/components/ui/time-picker.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; +import { Input } from './input'; + +interface TimePickerProps { + value: string; + onChange: (value: string) => void; + disabled?: boolean; +} + +const generateTimeOptions = () => { + const options: string[] = []; + for (let hour = 0; hour < 24; hour++) { + for (let minute = 0; minute < 60; minute += 10) { + const formattedHour = hour.toString().padStart(2, '0'); + const formattedMinute = minute.toString().padStart(2, '0'); + options.push(`${formattedHour}:${formattedMinute}`); + } + } + return options; +}; + +const isValidTimeFormat = (time: string): boolean => { + const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; + return timeRegex.test(time); +}; + +const formatTime = (time: string): string => { + if (!time) return ''; + const [hours, minutes] = time.split(':'); + const formattedHours = hours.padStart(2, '0'); + const formattedMinutes = minutes ? minutes.padStart(2, '0') : '00'; + return `${formattedHours}:${formattedMinutes}`; +}; + +export const TimePicker: React.FC = ({ value, onChange, disabled }) => { + const [inputValue, setInputValue] = useState(value); + const [isEditing, setIsEditing] = useState(false); + const timeOptions = generateTimeOptions(); + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setInputValue(newValue); + + // Auto-format as user types + if (newValue.length === 2 && !newValue.includes(':')) { + setInputValue(newValue + ':'); + } + }; + + const handleInputBlur = () => { + setIsEditing(false); + if (isValidTimeFormat(inputValue)) { + const formattedTime = formatTime(inputValue); + setInputValue(formattedTime); + onChange(formattedTime); + } else { + setInputValue(value); + } + }; + + const handleSelectChange = (newValue: string) => { + setInputValue(newValue); + onChange(newValue); + }; + + return ( +
+ {isEditing ? ( + + ) : ( + +
+
+ {timeOptions.map((time) => ( + + {time} + + ))} +
+ + + )} + + ); +}; diff --git a/apps/web/lib/components/toggler.tsx b/apps/web/lib/components/toggler.tsx index af90396ee..a829be297 100644 --- a/apps/web/lib/components/toggler.tsx +++ b/apps/web/lib/components/toggler.tsx @@ -138,7 +138,7 @@ export function DataSyncToggler({ className }: IClassName) { className={clsxm( 'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] ml-[-2px]', dataSync && - 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' + 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' )} > @@ -149,7 +149,7 @@ export function DataSyncToggler({ className }: IClassName) { className={clsxm( 'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] mr-[-2px]', !dataSync && - 'bg-red-400 shadow-md dark:bg-transparent dark:bg-red-400' + 'bg-red-400 shadow-md dark:bg-transparent dark:bg-red-400' )} > @@ -186,7 +186,7 @@ export function DataSyncModeToggler({ className }: IClassName) { className={clsxm( 'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] ml-[-2px]', dataSyncMode == 'REAL_TIME' && - 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' + 'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]' )} > @@ -197,7 +197,7 @@ export function DataSyncModeToggler({ className }: IClassName) { className={clsxm( 'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] mr-[-2px]', dataSyncMode == 'PULL' && - 'bg-white shadow-md dark:bg-transparent dark:bg-[#3B4454]' + 'bg-white shadow-md dark:bg-transparent dark:bg-[#3B4454]' )} > @@ -234,9 +234,8 @@ export function CommonToggle({ >