Skip to content

Commit

Permalink
[Feat]: Set Working Hours (#3501)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Innocent-Akim authored Jan 10, 2025
1 parent 2da1192 commit eb05f70
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 115 deletions.
7 changes: 7 additions & 0 deletions apps/web/app/[locale]/settings/personal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -28,6 +29,12 @@ const Personal = () => {
<ProfileAvatar />
<PersonalSettingForm />
</Accordian>
<Accordian
title='Working hours'
className="p-4 mt-4 dark:bg-dark--theme"
id="working-hours">
<WorkingHours />
</Accordian>
<Accordian
title={t('pages.settingsPersonal.DATA_SYNCHRONIZATION')}
className="p-4 mt-4 dark:bg-dark--theme"
Expand Down
35 changes: 19 additions & 16 deletions apps/web/app/[locale]/timesheet/[memberId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,15 +267,25 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
loading={loadingTimesheet}
/>
) : (
<CalendarView
user={user}
data={
shouldRenderPagination ?
paginatedGroups :
filterDataTimesheet
<>
<CalendarView
user={user}
data={
shouldRenderPagination ?
paginatedGroups :
filterDataTimesheet
}
loading={loadingTimesheet}
/>
{selectTimesheetId.length > 0 && <SelectedTimesheet
deleteTaskTimesheet={deleteTaskTimesheet}
fullWidth={fullWidth}
selectTimesheetId={selectTimesheetId}
setSelectTimesheetId={setSelectTimesheetId}
updateTimesheetStatus={updateTimesheetStatus}
/>
}
loading={loadingTimesheet}
/>
</>
)}
{shouldRenderPagination && (
<TimesheetPagination
Expand All @@ -292,14 +302,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
)}

</div>
{selectTimesheetId.length > 0 && <SelectedTimesheet
deleteTaskTimesheet={deleteTaskTimesheet}
fullWidth={fullWidth}
selectTimesheetId={selectTimesheetId}
setSelectTimesheetId={setSelectTimesheetId}
updateTimesheetStatus={updateTimesheetStatus}
/>
}

</Container>
</div>
</MainLayout>
Expand Down
5 changes: 5 additions & 0 deletions apps/web/app/hooks/useLeftSettingData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
125 changes: 125 additions & 0 deletions apps/web/components/ui/time-picker.tsx
Original file line number Diff line number Diff line change
@@ -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<TimePickerProps> = ({ value, onChange, disabled }) => {
const [inputValue, setInputValue] = useState(value);
const [isEditing, setIsEditing] = useState(false);
const timeOptions = generateTimeOptions();

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="relative bg-light--theme-light dark:bg-dark--theme-light dark:text-white font-normal">
{isEditing ? (
<Input
type="text"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
placeholder="HH:MM"
className="w-[120px]"
maxLength={5}
disabled={disabled}
pattern="[0-2][0-9]:[0-5][0-9]"
/>
) : (
<Select
value={value}
onValueChange={handleSelectChange}
disabled={disabled}
onOpenChange={(open) => {
if (!open) {
setIsEditing(false);
}
}}>
<SelectTrigger
className="w-[120px] bg-light--theme-light dark:bg-dark--theme-light dark:text-white font-normal border-[#0000001A] dark:border-[#26272C] "
onClick={() => setIsEditing(true)}>
<SelectValue placeholder="Select time">
{value}
</SelectValue>
</SelectTrigger>
<SelectContent className="max-h-[200px] overflow-y-auto ">
<div className="p-2 border-b">
<Input
type="text"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
placeholder="HH:MM"
className="mb-2 bg-light--theme-light dark:bg-dark--theme-light dark:text-white font-normal "
maxLength={5}
/>
</div>
<div className="overflow-y-auto">
{timeOptions.map((time) => (
<SelectItem
key={time}
value={time}
className="hover:bg-gray-100 dark:hover:bg-gray-800 bg-light--theme-light dark:bg-dark--theme-light dark:text-white font-normal ">
{time}
</SelectItem>
))}
</div>
</SelectContent>
</Select>
)}
</div>
);
};
17 changes: 8 additions & 9 deletions apps/web/lib/components/toggler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
)}
>
<UpdateIcon className="dark:text-white" />
Expand All @@ -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'
)}
>
<Cross2Icon className={clsxm(!dataSync && 'text-white')} />
Expand Down Expand Up @@ -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]'
)}
>
<LightningBoltIcon className="dark:text-white" />
Expand All @@ -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]'
)}
>
<UpdateIcon className="dark:text-white" />
Expand Down Expand Up @@ -234,9 +234,8 @@ export function CommonToggle({
>
<span
aria-hidden="true"
className={`${
enabled ? 'translate-x-9 bg-[#3826A6]' : 'translate-x-1'
}
className={`${enabled ? 'translate-x-9 bg-[#3826A6]' : 'translate-x-1'
}
pointer-events-none inline-block h-[30px] w-[30px] mt-[2.5px] transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out`}
/>
</Switch>
Expand Down Expand Up @@ -270,7 +269,7 @@ export function FullWidthToggler({ className }: IClassName) {
className={clsxm(
'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] ml-[-2px]',
fullWidth &&
'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]'
'bg-white text-primary shadow-md dark:bg-transparent dark:bg-[#3B4454]'
)}
>
<AllSidesIcon className="dark:text-white" />
Expand All @@ -281,7 +280,7 @@ export function FullWidthToggler({ className }: IClassName) {
className={clsxm(
'flex flex-row justify-center items-center p-2 w-8 h-8 rounded-[60px] mr-[-2px]',
!fullWidth &&
'bg-red-400 shadow-md dark:bg-transparent dark:bg-red-400'
'bg-red-400 shadow-md dark:bg-transparent dark:bg-red-400'
)}
>
<Cross2Icon className={clsxm(!fullWidth && 'text-white')} />
Expand Down
Loading

0 comments on commit eb05f70

Please sign in to comment.