Skip to content

Commit

Permalink
Merge pull request #3471 from ever-co/stage
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
evereq authored Dec 23, 2024
2 parents cd6fddc + 7d514db commit 4d902e8
Show file tree
Hide file tree
Showing 25 changed files with 860 additions and 344 deletions.
6 changes: 3 additions & 3 deletions apps/extensions/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4042,9 +4042,9 @@ mz@^2.7.0:
thenify-all "^1.0.0"

nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==

napi-build-utils@^1.0.1:
version "1.0.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function GroupBySelect({ defaultValues, onChange }: IProps) {
return (
<Listbox multiple value={selected} onChange={handleChange}>
<div className="relative h-[2.2rem] w-[12rem]">
<Listbox.Button className="relative w-full h-full cursor-default rounded-lg bg-white py-2 pl-1 pr-6 text-left border focus:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-primary sm:text-sm">
<Listbox.Button className="relative w-full h-full cursor-default rounded-lg bg-white dark:border-gray-700 dark:bg-dark--theme-light py-2 pl-1 pr-6 text-left border focus:outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-primary sm:text-sm">
<div className=" items-center w-full h-full flex gap-1">
{selected.map((option) => (
<Badge
Expand All @@ -83,18 +83,20 @@ export function GroupBySelect({ defaultValues, onChange }: IProps) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-60 w-full z-[999] overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
<Listbox.Options className="absolute mt-1 max-h-60 w-full z-[999] overflow-auto rounded-md bg-white dark:bg-dark--theme-light py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{options.map((option, index) => (
<Listbox.Option
disabled={
// Prevents users from clearing all selections, ensuring at least one option is always selected.
selected.includes(option) && selected.length == 1
}
key={index}
className={({ active }) =>
className={({ active, selected }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-primary/10 text-primary' : 'text-gray-900'
}`
active
? 'bg-primary/10 text-primary dark:text-white dark:bg-primary/10'
: 'text-gray-900 dark:text-white'
} ${selected && 'dark:bg-primary/10'}`
}
value={option}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@c
import { DatePickerFilter } from './TimesheetFilterDate';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@components/ui/select';
import { useTimesheet } from '@/app/hooks/features/useTimesheet';
import { toUTC } from '@/app/helpers';
export interface IAddTaskModalProps {
isOpen: boolean;
closeModal: () => void;
Expand All @@ -22,6 +23,18 @@ interface Shift {
totalHours: string;
dateFrom: Date | string,
}
interface FormState {
isBillable: boolean;
notes: string;
projectId: string;
taskId: string;
employeeId: string;
shifts: {
dateFrom: Date;
startTime: string;
endTime: string;
}[];
}

export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
const { tasks } = useTeamTasks();
Expand All @@ -32,6 +45,7 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {

const timeOptions = generateTimeOptions(5);
const t = useTranslations();

const [formState, setFormState] = React.useState({
notes: '',
isBillable: true,
Expand Down Expand Up @@ -77,37 +91,53 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
},
];

const handleAddTimesheet = async () => {
const createUtcDate = (baseDate: Date, time: string): Date => {
const [hours, minutes] = time.split(':').map(Number);
return new Date(Date.UTC(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), hours, minutes));
};

const handleAddTimesheet = async (formState: FormState) => {
const payload = {
isBillable: formState.isBillable,
description: formState.notes,
projectId: formState.projectId,
logType: TimeLogType.MANUAL as any,
source: TimerSource.BROWSER as any,
taskId: formState.taskId,
employeeId: formState.employeeId
}
const createUtcDate = (baseDate: Date, time: string): Date => {
const [hours, minutes] = time.split(':').map(Number);
return new Date(Date.UTC(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), hours, minutes));
employeeId: formState.employeeId,
organizationContactId: null || "",
organizationTeamId: null,
};

try {
await Promise.all(formState.shifts.map(async (shift) => {
const baseDate = shift.dateFrom instanceof Date ? shift.dateFrom : new Date(shift.dateFrom ?? new Date());
const startedAt = createUtcDate(baseDate, shift.startTime.toString().slice(0, 5));
const stoppedAt = createUtcDate(baseDate, shift.endTime.toString().slice(0, 5));
await createTimesheet({
...payload,
startedAt,
stoppedAt,
});
}));
closeModal();
if (!formState.shifts || formState.shifts.length === 0) {
throw new Error('No shifts provided.');
}
await Promise.all(
formState.shifts.map(async (shift) => {
if (!shift.dateFrom || !shift.startTime || !shift.endTime) {
throw new Error('Incomplete shift data.');
}

const baseDate = shift.dateFrom instanceof Date ? shift.dateFrom : new Date(shift.dateFrom);
const start = createUtcDate(baseDate, shift.startTime);
const end = createUtcDate(baseDate, shift.endTime);
const startedAt = toUTC(start).toISOString();
const stoppedAt = toUTC(end).toISOString();
if (stoppedAt <= startedAt) {
throw new Error('End time must be after start time.');
}
await createTimesheet({
...payload,
startedAt,
stoppedAt,
});
})
);
console.log('Timesheets successfully created.');
} catch (error) {
console.error('Failed to create timesheet:', error);
}
}
};

return (
<Modal
Expand Down Expand Up @@ -152,13 +182,14 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
<span className="text-[#de5505e1] ml-1">*</span>:
</label>
<CustomSelect
classNameGroup='max-h-[40vh] '
classNameGroup='max-h-[40vh] !text-white '
ariaLabel='Task issues'
className='w-full font-medium'
className='w-full font-medium text-white'
options={activeTeam?.members as any}
onChange={(value: any) => updateFormState('employeeId', value.id)}
renderOption={(option: any) => (
<div className="flex items-center gap-x-2">
<img className='h-6 w-6 rounded-full' src={option.employee.user.imageUrl} />
<span>{option.employee.fullName}</span>
</div>
)}
Expand Down Expand Up @@ -235,7 +266,7 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
</button>
<button
disabled={loadingCreateTimesheet}
onClick={handleAddTimesheet}
onClick={() => handleAddTimesheet(formState as any)}
type="submit"
className={clsxm(
'bg-primary dark:bg-primary-light h-[2.3rem] w-[5.5rem] justify-center font-normal flex items-center text-white px-2 rounded-lg',
Expand Down Expand Up @@ -321,21 +352,15 @@ const OptimizedAccordion = ({ setShifts, shifts, timeOptions, t }: {

const handleAddShift = () => {
setShifts([...shifts,
{ startTime: '', endTime: '', totalHours: '00:00h', dateFrom: new Date() },]);
{ startTime: '', endTime: '', totalHours: '00:00:00 h', dateFrom: new Date() },]);
};

const handleRemoveShift = (index: number) => {
const updatedShifts = shifts.filter((_, i) => i !== index);
setShifts(updatedShifts);
};

const convertMinutesToTime = (minutes: number): string => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const formattedHours = hours % 12 || 12;
return `${String(formattedHours).padStart(2, '0')}:${String(mins).padStart(2, '0')} ${period}`;
};


const handleShiftChange = (index: number, field: keyof Shift, value: string) => {
const updatedShifts = [...shifts];
Expand All @@ -346,11 +371,7 @@ const OptimizedAccordion = ({ setShifts, shifts, timeOptions, t }: {

if (!startTime || !endTime) return;

if (startTime === endTime) {
const startMinutes = convertToMinutesHour(startTime);
updatedShifts[index].endTime = convertMinutesToTime(startMinutes + 5);
return;
}

if (convertToMinutesHour(startTime) >= convertToMinutesHour(endTime)) {
return;
}
Expand Down Expand Up @@ -445,15 +466,15 @@ const ShiftManagement = (
<ShiftTimingSelect
label="Start"
timeOptions={timeOptions}
placeholder="00:00"
placeholder="00:00:00"
className="bg-[#30B3661A]"
value={value.startTime}
onChange={(value) => onChange(index, 'startTime', value)}
/>
<ShiftTimingSelect
label="End"
timeOptions={timeOptions}
placeholder="00:00"
placeholder="00:00:00"
className="bg-[#DA27271A]"
value={value.endTime}
onChange={(value) => onChange(index, 'endTime', value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
<div className="flex items-center w-full gap-2">
<div className={cn('p-2 rounded', statusColor(status).bg)}></div>
<div className="flex items-center gap-x-1">
<span className="text-base font-normal text-gray-400 uppercase !text-[14px]">
<span className="text-base font-medium text-[#71717A] uppercase !text-[14px]">
{status === 'DENIED' ? 'REJECTED' : status}
</span>
<span className="text-gray-400 text-[14px]">({rows.length})</span>
Expand Down Expand Up @@ -144,7 +144,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
<span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}
timesheetLog={task}

/>
</div>
Expand Down Expand Up @@ -218,7 +218,7 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa
</div>
</div>
</AccordionTrigger>
<AccordionContent className="flex flex-col w-full gap-y-2 overflow-auto">
<AccordionContent className="flex flex-col gap-y-2 overflow-auto items-start p-0 flex-none order-1 flex-grow-0">
{rows.map((task) => (
<div
key={task.id}
Expand All @@ -228,35 +228,43 @@ const BaseCalendarDataView = ({ data, daysLabels, t, CalendarComponent }: BaseCa

}}
className={cn(
'border-l-4 rounded-l flex flex-col p-2 gap-2 items-start space-x-4 h-[110px] !w-full',
'border-l-4 rounded-l space-x-4 box-border flex flex-col items-start py-2.5 gap-2 w-[258px] rounded-tr-md rounded-br-md flex-none order-1 self-stretch flex-grow',
)}>
<div className="flex px-3 justify-between items-center w-full">
<div className="flex pl-3 justify-between items-center w-full">
<div className="flex items-center gap-x-1">
<EmployeeAvatar
className="w-[28px] h-[28px] drop-shadow-[0_4px_4px_rgba(0,0,0,0.25)] rounded-full"
imageUrl={task.employee.user.imageUrl ?? ''}
/>
<span className=" font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
<span className="font-normal text-[#3D5A80] dark:text-[#7aa2d8]">{task.employee.fullName}</span>
</div>
<DisplayTimeForTimesheet
duration={task.timesheet.duration}
timesheetLog={task}

/>
</div>
<TaskNameInfoDisplay
task={task.task}
className={cn(
'rounded-sm h-auto !px-[0.3312rem] py-[0.2875rem] shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent'
'rounded-sm h-auto !px-[0.3312rem] py-[0.2875rem] shadow-[0px_0px_15px_0px_#e2e8f0] dark:shadow-transparent '
)}
taskTitleClassName={cn(
'text-sm !text-ellipsis !overflow-hidden !truncate !text-[#293241] dark:!text-white '
'text-sm !text-ellipsis overflow-hidden !truncate !text-[#293241] dark:!text-white'
)}
showSize={true}
showSize={false}
dash
taskNumberClassName="text-sm"
/>
<div className="flex items-center gap-x-2">
{task.project && <ProjectLogo imageUrl={task.project.imageUrl as string} />}
<span className="flex-1 font-medium">{task.project && task.project.name}</span>
<div className="flex flex-row items-center py-0 gap-2 flex-none order-2 self-stretch flex-grow-0">
{task.project?.imageUrl && (
<ProjectLogo
className="w-[28px] h-[28px] drop-shadow-[0_2px_2px_rgba(0,0,0,0.15)] rounded-[8px]"
imageUrl={task.project.imageUrl}
/>
)}
<span className="!text-ellipsis !overflow-hidden !truncate !text-[#3D5A80] dark:!text-[#7aa2d8] flex-1">
{task.project?.name ?? 'No Project'}
</span>
</div>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full" }:
{ imageUrl: string; alt: string; className?: string }) => {
const [isLoading, setIsLoading] = React.useState(true);
return (
<div className="relative w-6 h-6">
<div className="relative">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-200 rounded-full">
<LoadingSpinner className="w-5 h-5" />
Expand All @@ -44,11 +44,11 @@ const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full" }:
);
};

export const EmployeeAvatar = ({ imageUrl }: { imageUrl: string }) => (
<ImageWithLoader imageUrl={imageUrl} alt="Employee" />
export const EmployeeAvatar = ({ imageUrl, className }: { imageUrl: string, className?: string }) => (
<ImageWithLoader className={className} imageUrl={imageUrl} alt="Employee" />
);


export const ProjectLogo = ({ imageUrl }: { imageUrl: string }) => (
<ImageWithLoader imageUrl={imageUrl} alt="Project Logo" />
export const ProjectLogo = ({ imageUrl, className }: { imageUrl: string, className?: string }) => (
<ImageWithLoader className={className} imageUrl={imageUrl} alt="Project Logo" />
);
Loading

0 comments on commit 4d902e8

Please sign in to comment.