Skip to content

Commit

Permalink
[Feat]: Adapt ManageOrMemberComponent to new design with dynamic fiel…
Browse files Browse the repository at this point in the history
…ds and combobox (#2988)

* feat: adapt ManageOrMemberComponent to new design with dynamic fields and comboboxes

* fix: cspell
  • Loading branch information
Innocent-Akim authored Sep 7, 2024
1 parent 46aae43 commit 4d7bf5f
Show file tree
Hide file tree
Showing 4 changed files with 227 additions and 65 deletions.
1 change: 1 addition & 0 deletions apps/web/app/[locale]/calendar/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const CalendarPage = () => {
closeModal={closeManualTimeModal}
isOpen={isManualTimeModalOpen}
params='AddManuelTime'
timeSheetStatus='ManagerTimesheet'
/>
<div
className='fixed top-20 flex flex-col border-b-[1px] dark:border-gray-800 z-10 mx-0 w-full bg-white dark:bg-dark-high shadow-lg shadow-gray-100 dark:shadow-gray-700 '
Expand Down
33 changes: 11 additions & 22 deletions apps/web/lib/components/combobox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
"use client"
import * as React from "react";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { cn } from "lib/utils";
Expand Down Expand Up @@ -29,26 +28,9 @@ interface ComboboxProps<T> {
onChangeValue?: (value: T | null) => void
className?: string
popoverClassName?: string
selectedItem?: T | null
}
/**
*
*
* @export
* @template T
* @param {ComboboxProps<T>} {
* items,
* itemToString,
* itemToValue,
* placeholder = "Select item...",
* buttonWidth = "w-[200px]",
* commandInputHeight = "h-9",
* noResultsText = "No item found.",
* onChangeValue,
* className,
* popoverClassName
* }
* @return {*}
*/

export function CustomCombobox<T>({
items,
itemToString,
Expand All @@ -59,10 +41,11 @@ export function CustomCombobox<T>({
noResultsText = "No item found.",
onChangeValue,
className,
popoverClassName
popoverClassName,
selectedItem = null
}: ComboboxProps<T>) {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState<T | null>(null)
const [value, setValue] = React.useState<T | null>(selectedItem)
const [popoverWidth, setPopoverWidth] = React.useState<number | null>(null);
const triggerRef = React.useRef<HTMLButtonElement>(null);

Expand All @@ -80,6 +63,12 @@ export function CustomCombobox<T>({
setPopoverWidth(triggerRef.current.offsetWidth);
}
}, [triggerRef.current]);


React.useEffect(() => {
setValue(selectedItem);
}, [selectedItem]);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
Expand Down
119 changes: 76 additions & 43 deletions apps/web/lib/features/manual-time/add-manual-time-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IAddManualTimeRequest } from '@app/interfaces/timer/ITimerLogs';
import { cn } from 'lib/utils';
import { CalendarDays } from 'lucide-react';
import { IoTime } from 'react-icons/io5';
import { Item, ManageOrMemberComponent, getNestedValue } from './manage-member-component';

/**
* Interface for the properties of the `AddManualTimeModal` component.
Expand All @@ -29,11 +30,12 @@ import { IoTime } from 'react-icons/io5';
interface IAddManualTimeModalProps {
isOpen: boolean;
params: "AddManuelTime" | "AddTime";
timeSheetStatus?: "ManagerTimesheet" | "TeamMemberTimesheet",
closeModal: () => void;
}

export function AddManualTimeModal(props: IAddManualTimeModalProps) {
const { closeModal, isOpen, params } = props;
const { closeModal, isOpen, params, timeSheetStatus } = props;
const t = useTranslations();
const [isBillable, setIsBillable] = useState<boolean>(false);
const [description, setDescription] = useState<string>('');
Expand All @@ -45,7 +47,6 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
const [team, setTeam] = useState<IOrganizationTeamList>();
const [taskId, setTaskId] = useState<string>('');
const [timeDifference, setTimeDifference] = useState<string>('');
const [memberId, setMemberId] = useState<string>('')
const { activeTeamTask, tasks, activeTeam } = useTeamTasks();
const { teams } = useOrganizationTeams();

Expand Down Expand Up @@ -141,6 +142,56 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
}
}, [addManualTimeLoading, closeModal, timeLog]);


const memberItemsLists = {
'Project': activeTeam?.projects,
'Employee': activeTeam?.members,
'Task': tasks,
};
const selectedValues = {
'Teams': null,
'Members': null,
"Task": null
};
const fields = [
{
label: 'Project',
placeholder: 'Select a project',
isRequired: true,
valueKey: 'id',
displayKey: 'name',
element: 'Project'
},
...(timeSheetStatus === 'ManagerTimesheet' ?
[{
label: t('manualTime.EMPLOYEE'),
placeholder: 'Select an employee',
isRequired: true,
valueKey: 'id',
displayKey: 'employee.fullName',
element: 'Employee'
}] : []),
{
label: t('manualTime.TASK'),
placeholder: 'Select a Task',
isRequired: true,
valueKey: 'id',
displayKey: 'title',
element: 'Task'
}
];




const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => {
console.log(values);
};

const handleChange = (field: string, selectedItem: Item | null) => {
console.log(`Field: ${field}, Selected Item:`, selectedItem);
};

return (
<Modal
isOpen={isOpen}
Expand Down Expand Up @@ -237,50 +288,19 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
</div>
</div>

<div className="">
<label className="block text-gray-500 mb-1">
{t('manualTime.TEAM')}<span className="text-[#de5505e1] ml-1">*</span>
</label>
<SelectItems
defaultValue={activeTeam!}
items={teams}
onValueChange={(team) => setTeam(team)}
itemId={(team) => (team ? team.id : '')}
itemToString={(team) => (team ? team.name : '')}
triggerClassName="border-gray-300 dark:border-slate-600"
/>
</div>

{
params === 'AddManuelTime' ? (
<>

<div className="">
<label className="block text-gray-500 mb-1">
{t('manualTime.EMPLOYEE')}<span className="text-[#de5505e1] ml-1">*</span>
</label>
<SelectItems
items={activeTeam?.members ?? []}
onValueChange={(member) => setMemberId(member ? member.id : memberId)}
itemId={(member) => (member ? member.id : memberId)}
itemToString={(member) => (member ? member.employee.fullName : '')}
triggerClassName="border-gray-300 dark:border-slate-600"
/>
</div>

<div className="">
<label className="block text-gray-500 mb-1">
{t('manualTime.TASK')}<span className="text-[#de5505e1] ml-1">*</span>
</label>
<SelectItems
items={manualTimeReasons.map((reason) => t(`manualTime.reasons.${reason}`))}
onValueChange={(reason) => setReason(reason)}
itemId={(reason) => reason}
defaultValue={t('manualTime.reasons.DEFAULT')}
itemToString={(reason) => reason}
triggerClassName="border-gray-300 dark:border-slate-600"
/>
</div>
<ManageOrMemberComponent
fields={fields}
itemsLists={memberItemsLists}
selectedValues={selectedValues}
onSelectedValuesChange={handleSelectedValuesChange}
handleChange={handleChange}
itemToString={(item, displayKey) => getNestedValue(item, displayKey) || ''}
itemToValue={(item, valueKey) => getNestedValue(item, valueKey) || ''}
/>
<div className="flex flex-col">
<label className="block text-gray-500 shrink-0">{t('manualTime.DESCRIPTION')} ({t('manualTime.OPTIONAL')})</label>
<textarea
Expand All @@ -290,10 +310,23 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
className="w-full resize-none p-2 grow border border-gray-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md h-32"
/>
</div>

</>
) : (
<>

<div className="">
<label className="block text-gray-500 mb-1">
{t('manualTime.TEAM')}<span className="text-[#de5505e1] ml-1">*</span>
</label>
<SelectItems
defaultValue={activeTeam!}
items={teams}
onValueChange={(team) => setTeam(team)}
itemId={(team) => (team ? team.id : '')}
itemToString={(team) => (team ? team.name : '')}
triggerClassName="border-gray-300 dark:border-slate-600"
/>
</div>
<div className="">
<label className="block text-gray-500 mb-1">
{t('manualTime.TASK')}<span className="text-[#de5505e1] ml-1">*</span>
Expand Down
139 changes: 139 additions & 0 deletions apps/web/lib/features/manual-time/manage-member-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { CustomCombobox } from "lib/components/combobox";


/**
* Represents an item with dynamic properties.
* @interface Item
*/
export interface Item {
[key: string]: any; // Allows for dynamic properties
}

/**
* Props interface for the ManageOrMemberComponent component.
* @template T - The type of items in the component.
*/
interface ManageOrMemberComponentProps<T extends Item> {
/**
* An array of field definitions for the component.
* Each field specifies the label, placeholder, whether it's required, and keys for value and display.
* @type {Object[]}
* @property {string} label - The label for the field.
* @property {string} placeholder - The placeholder text for the field.
* @property {boolean} isRequired - Indicates if the field is required.
* @property {string} valueKey - The key to extract the value of the item.
* @property {string} displayKey - The key to extract the display value of the item.
*/
fields: {
label: string;
element: string,
placeholder: string;
isRequired: boolean;
valueKey: string; // Key for extracting the item's value
displayKey: string; // Key for extracting the item's display text
}[];

/**
* A dictionary of item lists for each field.
* The key corresponds to the field label, and the value is an array of items or undefined.
* @type {Object.<string, T[] | undefined>}
*/
itemsLists: { [key: string]: T[] | undefined };

/**
* A dictionary of currently selected values for each field.
* The key corresponds to the field label, and the value is the selected item or null.
* @type {Object.<string, T | null>}
*/
selectedValues: { [key: string]: T | null };

/**
* Callback function to handle changes in selected values.
* @param {Object.<string, T | null>} values - The updated selected values for each field.
*/
onSelectedValuesChange: (values: { [key: string]: T | null }) => void;

/**
* Optional callback function to handle additional logic when a value changes.
* @param {string} field - The label of the field where the change occurred.
* @param {T | null} selectedItem - The newly selected item or null.
*/
handleChange?: (field: string, selectedItem: T | null) => void;

/**
* Function to convert an item to a display string based on the displayKey.
* @param {T | null} item - The item to convert.
* @param {string} displayKey - The key to extract the display text.
* @returns {string} - The display string for the item.
*/
itemToString: (item: T | null, displayKey: string) => string;

/**
* Function to convert an item to a value string based on the valueKey.
* @param {T | null} item - The item to convert.
* @param {string} valueKey - The key to extract the value.
* @returns {string} - The value string for the item.
*/
itemToValue: (item: T | null, valueKey: string) => string;
}

/**
* A React component that renders a set of fields with dynamic item lists and handles user selections.
* @template T - The type of items in the component.
* @param {ManageOrMemberComponentProps<T>} props - The props for the component.
* @returns {JSX.Element} - The rendered component.
*/
export const ManageOrMemberComponent = <T extends Item>({
fields,
itemsLists,
selectedValues,
onSelectedValuesChange,
handleChange,
itemToString,
itemToValue,
}: ManageOrMemberComponentProps<T>): JSX.Element => {

/**
* Internal handler for value changes.
* Calls the external handleChange function if provided, and updates the selected values.
* @param {string} field - The label of the field where the change occurred.
* @param {T | null} selectedItem - The newly selected item or null.
*/
const handleInternalChange = (field: string, selectedItem: T | null) => {
if (handleChange) {
handleChange(field, selectedItem);
}
onSelectedValuesChange({
...selectedValues,
[field]: selectedItem,
});
};

return (
<div>
{fields.map((field, index) => (
<div key={index} className="mb-4">
<label className="block text-gray-600 mb-1 text-sm">
<span className="text-[14px]">{field.label}</span>
{field.isRequired && <span className="text-[#de5505e1] ml-1 text-sm">*</span>}
</label>
<CustomCombobox
popoverWidth='w-full'
buttonWidth='w-full'
itemToString={(item) => itemToString(item, field.displayKey)}
itemToValue={(item) => itemToValue(item, field.valueKey)}
items={itemsLists[field.element] || []}
onChangeValue={(selectedItem) => handleInternalChange(field.element, selectedItem)}
placeholder={field.placeholder}
selectedItem={selectedValues[field.element] || null}
/>
</div>
))}
</div>
);
};


export const getNestedValue = (obj: any, key: string) => {
return key.split('.').reduce((o, i) => o && o[i], obj);
};

0 comments on commit 4d7bf5f

Please sign in to comment.