Skip to content

Commit

Permalink
[Feature] Projects management | Create New Project (#3615)
Browse files Browse the repository at this point in the history
* add 'create project form'

* add 'financial settings form'

* add 'categorization form'

* add a reusable select (mono/multi) for the creation steps

* add the final review view

* add summary review , validations & dark mode

* finish validations & project creation flow

* connect the project creation flow with the api

* create image assets for the new project

* fix spell typo

* clean up ..., internationalization

* add coderabit suggestions
  • Loading branch information
CREDO23 authored Feb 23, 2025
1 parent 9d54e21 commit b3c6787
Show file tree
Hide file tree
Showing 35 changed files with 2,986 additions and 33 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"caseSensitive": false,
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"words": [
"Złoty",
" X",
" X ",
"hookform",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export function MultiSelectWithSearch<T extends { value: string | number; id: st

const handleSelect = useCallback(
(selectedOption: T) => {
const newSelectedOptions = selectedOptions;
let newSelectedOptions = selectedOptions;

if (!selectedOptions.map((el) => el.value).includes(selectedOption.value)) {
newSelectedOptions.push(selectedOption);
} else {
newSelectedOptions.splice(newSelectedOptions.indexOf(selectedOption), 1);
newSelectedOptions = newSelectedOptions.filter((el) => el.id !== selectedOption.id);
}
onChange(newSelectedOptions);
},
Expand Down
5 changes: 4 additions & 1 deletion apps/web/app/[locale]/projects/components/page-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { LAST_SELECTED_PROJECTS_VIEW } from '@/app/constants';
import { useTranslations } from 'next-intl';
import { useSearchParams } from 'next/navigation';
import FiltersCardModal from './filters-card-modal';
import AddOrEditProjectModal from '@/lib/features/project/add-or-edit-project';

type TViewMode = 'GRID' | 'LIST';

Expand All @@ -29,6 +30,7 @@ function PageComponent() {
closeModal: closeFiltersCardModal,
openModal: openFiltersCardModal
} = useModal();
const { isOpen: isProjectModalOpen, closeModal: closeProjectModal, openModal: openProjectModal } = useModal();
const { isTrackingEnabled } = useOrganizationTeams();
const lastSelectedView = useMemo(() => {
try {
Expand Down Expand Up @@ -153,7 +155,7 @@ function PageComponent() {
</div>

<div className="h-full flex items-end">
<Button variant="grey" className=" text-primary font-medium">
<Button onClick={openProjectModal} variant="grey" className=" text-primary font-medium">
<Plus size={15} /> <span>{t('pages.projects.CREATE_NEW_PROJECT')}</span>
</Button>
</div>
Expand Down Expand Up @@ -233,6 +235,7 @@ function PageComponent() {
) : null}
</div>
<FiltersCardModal closeModal={closeFiltersCardModal} open={isFiltersCardModalOpen} />
<AddOrEditProjectModal closeModal={closeProjectModal} open={isProjectModalOpen} />
</div>
</MainLayout>
);
Expand Down
36 changes: 36 additions & 0 deletions apps/web/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,39 @@ export const statusOptions = [
];

export const DEFAULT_WORK_HOURS_PER_DAY = 8;

// 20+ Major currencies
export enum CurrencyEnum {
USD = 'US Dollar',
EUR = 'Euro',
JPY = 'Japanese Yen',
GBP = 'British Pound Sterling',
AUD = 'Australian Dollar',
CAD = 'Canadian Dollar',
CHF = 'Swiss Franc',
CNY = 'Chinese Yuan',
HKD = 'Hong Kong Dollar',
NZD = 'New Zealand Dollar',
SEK = 'Swedish Krona',
KRW = 'South Korean Won',
SGD = 'Singapore Dollar',
NOK = 'Norwegian Krone',
MXN = 'Mexican Peso',
INR = 'Indian Rupee',
RUB = 'Russian Ruble',
BRL = 'Brazilian Real',
ZAR = 'South African Rand',
TRY = 'Turkish Lira',
THB = 'Thai Baht',
IDR = 'Indonesian Rupiah',
PLN = 'Polish Złoty',
TWD = 'New Taiwan Dollar'
}

export const predefinedLabels = [
{ name: 'backend', color: '#234356' },
{ name: 'frontend', color: '#456789' },
{ name: 'mobile', color: '#A3B4C5' },
{ name: 'UX/UI', color: '#C7D0D9' },
{ name: 'data', color: '#D9E2E8' }
];
3 changes: 2 additions & 1 deletion apps/web/app/hooks/features/useOrganizationProjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useAtom } from 'jotai';
import { useQuery } from '../useQuery';
import { organizationProjectsState } from '@/app/stores/organization-projects';
import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers';
import { ICreateProjectInput } from '@/app/interfaces';

export function useOrganizationProjects() {
const [user] = useAtom(userState);
Expand Down Expand Up @@ -77,7 +78,7 @@ export function useOrganizationProjects() {
);

const createOrganizationProject = useCallback(
async (data: { name: string }) => {
async (data: Partial<ICreateProjectInput>) => {
try {
const organizationId = getOrganizationIdCookie();
const tenantId = getTenantIdCookie();
Expand Down
72 changes: 72 additions & 0 deletions apps/web/app/hooks/features/useTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ITag } from '@app/interfaces';
import { createTagAPI, deleteTagAPI, getTagsAPI, updateTagAPI } from '@app/services/client/api';
import { useCallback } from 'react';
import { useAtom } from 'jotai';
import { useQuery } from '../useQuery';
import cloneDeep from 'lodash/cloneDeep';
import { tagsState } from '@/app/stores/tags';

export const useTags = () => {
const [tags, setTags] = useAtom(tagsState);

const { loading, queryCall: getTagsQueryCall } = useQuery(getTagsAPI);
const { loading: createTagLoading, queryCall: createTagQueryCall } = useQuery(createTagAPI);
const { loading: updateTagLoading, queryCall: updateTagQueryCall } = useQuery(updateTagAPI);
const { loading: deleteTagLoading, queryCall: deleteTagQueryCall } = useQuery(deleteTagAPI);

const getTags = useCallback(() => {
getTagsQueryCall().then((response) => {
if (response.data.items.length) {
setTags(response.data.items);
}
});
}, [getTagsQueryCall, setTags]);

const createTag = useCallback(
async (tag: Omit<ITag, 'id'>) => {
return createTagQueryCall(tag).then((response) => {
setTags((prevTags) => [response.data, ...prevTags]);
});
},
[createTagQueryCall, setTags]
);

const updateTag = useCallback(
async (tag: ITag) => {
updateTagQueryCall(tag).then(() => {
const index = tags.findIndex((item) => item.id === tag.id);
const tempTags = cloneDeep(tags);
if (index >= 0) {
tempTags[index].name = tag.name;
}

setTags(tempTags);
});
},
[tags, setTags, updateTagQueryCall]
);

const deleteTag = useCallback(
async (id: string) => {
deleteTagQueryCall(id).then(() => {
setTags(tags.filter((tag) => tag.id !== id));
});
},
[deleteTagQueryCall, setTags, tags]
);

return {
tags,
loading,
getTags,

createTag,
createTagLoading,

deleteTag,
deleteTagLoading,

updateTag,
updateTagLoading
};
};
69 changes: 63 additions & 6 deletions apps/web/app/interfaces/IProject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { IEmployee } from './IEmployee';
import { IOrganization } from './IOrganization';
import { IOrganizationTeam, OT_Member } from './IOrganizationTeam';
import { ITeamTask } from './ITask';
import { TaskStatusEnum } from './ITaskStatus';
import { ITenant } from './ITenant';
import { ITimeLog } from './timer/ITimerLogs';

export interface IProjectRepository {
Expand Down Expand Up @@ -65,18 +67,14 @@ export interface IProject {
isArchived?: boolean;
archivedAt: string | null;
deletedAt: string | null;
tags?: ITag[];
website?: string;
}

export interface CustomFields {
repositoryId: any;
}

export interface IProjectCreate {
name: string;
organizationId: string;
tenantId: string;
}

export enum ProjectBillingEnum {
RATE = 'RATE',
FLAT_FEE = 'FLAT_FEE',
Expand All @@ -92,3 +90,62 @@ export enum OrganizationProjectBudgetTypeEnum {
HOURS = 'hours',
COST = 'cost'
}

export interface ITag {
id: string;
name: string;
color: string;
textColor?: string;
icon?: string;
description?: string;
isSystem?: boolean;
tagTypeId?: string;
organizationId?: string;
organization?: IOrganization;
tenantId?: string;
tenant?: ITenant;
}

export interface ILabel {
id: string;
name: string;
color: string;
textColor?: string;
icon?: string;
description?: string;
organizationId?: string;
organization?: IOrganization;
tenantId?: string;
tenant?: ITenant;
}

export enum ProjectRelationEnum {
RelatedTo = 'related to',
BlockedBy = 'blocked by',
Blocking = 'blocking'
}

export interface IProjectRelation {
projectId: string;
relationType: ProjectRelationEnum;
}

export interface ICreateProjectInput {
name: string;
organizationId: string;
tenantId: string;
website?: string;
description?: string;
color?: string;
tags?: ITag[];
imageUrl?: string;
imageId?: string;
budget?: number;
budgetType?: OrganizationProjectBudgetTypeEnum;
startDate: string;
endDate: string;
billing?: ProjectBillingEnum;
currency?: string;
memberIds?: string[];
managerIds?: string[];
}
10 changes: 10 additions & 0 deletions apps/web/app/interfaces/IRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum RolesEnum {
SUPER_ADMIN = 'SUPER_ADMIN',
ADMIN = 'ADMIN',
DATA_ENTRY = 'DATA_ENTRY',
EMPLOYEE = 'EMPLOYEE',
CANDIDATE = 'CANDIDATE',
MANAGER = 'MANAGER',
VIEWER = 'VIEWER',
INTERVIEWER = 'INTERVIEWER'
}
1 change: 1 addition & 0 deletions apps/web/app/services/client/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export * from './activity/activity';
export * from './activity';
export * from './default';
export * from './projects';
export * from './tags';
5 changes: 2 additions & 3 deletions apps/web/app/services/client/api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { IProject, IProjectCreate } from '@app/interfaces';
import { ICreateProjectInput, IProject } from '@app/interfaces';
import { post } from '../axios';

export function createOrganizationProjectAPI(data: IProjectCreate) {

export function createOrganizationProjectAPI(data: Partial<ICreateProjectInput>) {
return post<IProject>(`/organization-projects`, data);
}
30 changes: 30 additions & 0 deletions apps/web/app/services/client/api/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ITag, PaginationResponse } from '@app/interfaces';
import { deleteApi, get, post, put } from '../axios';
import { getOrganizationIdCookie, getTenantIdCookie } from '@/app/helpers';
import qs from 'qs';

export function getTagsAPI() {
const organizationId = getOrganizationIdCookie();
const tenantId = getTenantIdCookie();

const obj = {
'where[organizationId]': organizationId,
'where[tenantId]': tenantId
} as Record<string, string>;

const query = qs.stringify(obj);

return get<PaginationResponse<ITag>>(`/tags?${query}`);
}

export function createTagAPI(data: Omit<ITag, 'id'>) {
return post<ITag>('/tags', data);
}

export function deleteTagAPI(id: string) {
return deleteApi<ITag>(`/tags/${id}`);
}

export function updateTagAPI(data: ITag) {
return put<ITag>(`/tags/${data.id}`, data);
}
4 changes: 2 additions & 2 deletions apps/web/app/services/server/requests/project.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IProjectCreate, IProject } from '@app/interfaces/';
import { ICreateProjectInput, IProject } from '@app/interfaces/';
import { serverFetch } from '../fetch';

export function createOrganizationProjectRequest(datas: IProjectCreate, bearer_token: string) {
export function createOrganizationProjectRequest(datas: Partial<ICreateProjectInput>, bearer_token: string) {
return serverFetch<IProject>({
path: '/organization-projects',
method: 'POST',
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/stores/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ITag } from '@app/interfaces/';
import { atom } from 'jotai';

export const tagsState = atom<ITag[]>([]);
18 changes: 15 additions & 3 deletions apps/web/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { clsxm } from '@app/utils';

const Select = SelectPrimitive.Root;

const SelectPortal = SelectPrimitive.Portal;

const SelectGroup = SelectPrimitive.Group;

const SelectValue = SelectPrimitive.Value;
Expand Down Expand Up @@ -41,7 +43,7 @@ const SelectContent = React.forwardRef<
className={cn(
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
Expand All @@ -51,7 +53,7 @@ const SelectContent = React.forwardRef<
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}
>
{children}
Expand Down Expand Up @@ -102,4 +104,14 @@ const SelectSeparator = React.forwardRef<
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;

export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator };
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectPortal
};
Loading

0 comments on commit b3c6787

Please sign in to comment.