@@ -32,57 +36,62 @@ const Team = () => {
{/* General Settings */}
-
{/* TODO */}
{/* Notification Settings */}
@@ -94,14 +103,15 @@ const Team = () => {
*/}
{/* Danger Zone */}
-
diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 000000000..608e25d71
--- /dev/null
+++ b/apps/web/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,2 @@
+import { handlers } from '../../../../auth';
+export const { GET, POST } = handlers;
diff --git a/apps/web/app/api/auth/register/route.ts b/apps/web/app/api/auth/register/route.ts
index c5ae81330..658fefb2b 100644
--- a/apps/web/app/api/auth/register/route.ts
+++ b/apps/web/app/api/auth/register/route.ts
@@ -8,8 +8,8 @@ import {
createTenantRequest,
createTenantSmtpRequest,
loginUserRequest,
- registerUserRequest,
- refreshTokenRequest
+ refreshTokenRequest,
+ registerUserRequest
} from '@app/services/server/requests';
import { setAuthCookies } from '@app/helpers/cookies';
import { recaptchaVerification } from '@app/services/server/recaptcha';
@@ -18,7 +18,7 @@ import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const url = new URL(req.url);
- const res = new NextResponse();
+ const response = new NextResponse();
const appEmailConfirmationUrl = `${url.origin}${VERIFY_EMAIL_CALLBACK_PATH}`;
@@ -62,6 +62,8 @@ export async function POST(req: Request) {
const password = generateToken(8);
const names = body.name.split(' ');
+ console.log('Random password: ', password);
+
// Register user
const { data: user } = await registerUserRequest({
password: password,
@@ -137,8 +139,8 @@ export async function POST(req: Request) {
languageId: 'en', // TODO: not sure what should be here
userId: user.id
},
- { req, res }
+ { req, res: response }
);
- return NextResponse.json({ loginRes, team, employee });
+ return response;
}
diff --git a/apps/web/app/api/auth/signin-email-social-login/route.ts b/apps/web/app/api/auth/signin-email-social-login/route.ts
new file mode 100644
index 000000000..d42837f62
--- /dev/null
+++ b/apps/web/app/api/auth/signin-email-social-login/route.ts
@@ -0,0 +1,12 @@
+import { signWithSocialLoginsRequest } from '@app/services/server/requests';
+import { ProviderEnum } from '@app/services/server/requests/OAuth';
+
+import { NextResponse } from 'next/server';
+
+export async function POST(req: Request) {
+ const body = (await req.json()) as { provider: ProviderEnum; access_token: string };
+
+ const { data } = await signWithSocialLoginsRequest(body.provider, body.access_token);
+
+ return NextResponse.json(data);
+}
diff --git a/apps/web/app/api/daily-plan/[id]/route.ts b/apps/web/app/api/daily-plan/[id]/route.ts
new file mode 100644
index 000000000..e5cb724e2
--- /dev/null
+++ b/apps/web/app/api/daily-plan/[id]/route.ts
@@ -0,0 +1,65 @@
+import { NextResponse } from 'next/server';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import { deleteDailyPlanRequest, getDayPlansByEmployee, updatePlanRequest } from '@app/services/server/requests';
+import { INextParams, IUpdateDailyPlan } from '@app/interfaces';
+
+export async function GET(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+ const { id } = params;
+ if (!id) {
+ return;
+ }
+
+ const { $res, user, tenantId, organizationId, access_token } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+
+ const response = await getDayPlansByEmployee({
+ bearer_token: access_token,
+ employeeId: id,
+ organizationId,
+ tenantId
+ });
+
+ return $res(response.data);
+}
+
+export async function PUT(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+ const { id } = params;
+ if (!id) {
+ return;
+ }
+
+ const { $res, user, access_token, tenantId } = await authenticatedGuard(req, res);
+
+ if (!user) return $res('Unauthorized');
+
+ const body = (await req.json()) as unknown as IUpdateDailyPlan;
+
+ const response = await updatePlanRequest({
+ bearer_token: access_token,
+ data: body,
+ planId: id,
+ tenantId
+ });
+
+ return $res(response.data);
+}
+
+export async function DELETE(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+ const { id } = params;
+ if (!id) {
+ return;
+ }
+
+ const { $res, user, access_token } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+
+ const response = await deleteDailyPlanRequest({
+ planId: id,
+ bearer_token: access_token
+ });
+
+ return $res(response.data);
+}
diff --git a/apps/web/app/api/daily-plan/plan/[planId]/route.ts b/apps/web/app/api/daily-plan/plan/[planId]/route.ts
new file mode 100644
index 000000000..acdb8064d
--- /dev/null
+++ b/apps/web/app/api/daily-plan/plan/[planId]/route.ts
@@ -0,0 +1,50 @@
+import { IDailyPlanTasksUpdate, INextParams } from '@app/interfaces';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import { addTaskToDailyPlanRequest, removeTaskFromPlanRequest } from '@app/services/server/requests';
+import { NextResponse } from 'next/server';
+
+export async function POST(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+
+ const { planId } = params;
+ if (!planId) {
+ return;
+ }
+
+ const { $res, user, tenantId, access_token } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+
+ const body = (await req.json()) as unknown as IDailyPlanTasksUpdate;
+
+ const response = await addTaskToDailyPlanRequest({
+ bearer_token: access_token,
+ data: body,
+ planId,
+ tenantId
+ });
+
+ return $res(response.data);
+}
+
+export async function PUT(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+
+ const { planId } = params;
+ if (!planId) {
+ return;
+ }
+
+ const { $res, user, tenantId, access_token } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+
+ const body = (await req.json()) as unknown as IDailyPlanTasksUpdate;
+
+ const response = await removeTaskFromPlanRequest({
+ data: body,
+ planId,
+ tenantId,
+ bearer_token: access_token
+ });
+
+ return $res(response.data);
+}
diff --git a/apps/web/app/api/daily-plan/route.ts b/apps/web/app/api/daily-plan/route.ts
new file mode 100644
index 000000000..e186506c8
--- /dev/null
+++ b/apps/web/app/api/daily-plan/route.ts
@@ -0,0 +1,32 @@
+import { ICreateDailyPlan } from '@app/interfaces';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import { createPlanRequest, getAllDayPlans } from '@app/services/server/requests';
+import { NextResponse } from 'next/server';
+
+export async function POST(req: Request) {
+ const res = new NextResponse();
+ const { $res, user, access_token: bearer_token, tenantId } = await authenticatedGuard(req, res);
+
+ if (!user) return $res('Unauthorized');
+
+ const body = (await req.json()) as unknown as ICreateDailyPlan;
+
+ const response = await createPlanRequest({ data: body, bearer_token, tenantId });
+
+ return $res(response.data);
+}
+
+export async function GET(req: Request) {
+ const res = new NextResponse();
+
+ const { $res, user, tenantId, organizationId, access_token } = await authenticatedGuard(req, res);
+ if (!user) return $res('Unauthorized');
+
+ const response = await getAllDayPlans({
+ bearer_token: access_token,
+ organizationId,
+ tenantId
+ });
+
+ return $res(response.data);
+}
diff --git a/apps/web/app/api/daily-plan/task/[taskId]/route.ts b/apps/web/app/api/daily-plan/task/[taskId]/route.ts
new file mode 100644
index 000000000..3ee5a4878
--- /dev/null
+++ b/apps/web/app/api/daily-plan/task/[taskId]/route.ts
@@ -0,0 +1,25 @@
+import { INextParams } from '@app/interfaces';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import { getPlansByTask } from '@app/services/server/requests';
+import { NextResponse } from 'next/server';
+
+export async function GET(req: Request, { params }: INextParams) {
+ const res = new NextResponse();
+ const { taskId } = params;
+ if (!taskId) {
+ return;
+ }
+
+ const { $res, user, tenantId, organizationId, access_token } = await authenticatedGuard(req, res);
+
+ if (!user) return $res('Unauthorized');
+
+ const response = await getPlansByTask({
+ taskId,
+ bearer_token: access_token,
+ organizationId,
+ tenantId
+ });
+
+ return $res(response.data);
+}
diff --git a/apps/web/app/api/roles/options/route.ts b/apps/web/app/api/roles/options/route.ts
new file mode 100644
index 000000000..a365603b7
--- /dev/null
+++ b/apps/web/app/api/roles/options/route.ts
@@ -0,0 +1,84 @@
+import { INVITE_CALLBACK_URL, INVITE_CALLBACK_PATH } from '@app/constants';
+import { validateForm } from '@app/helpers/validations';
+import { IInviteRequest } from '@app/interfaces/IInvite';
+import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
+import {
+ getEmployeeRoleRequest,
+ getTeamInvitationsRequest,
+ inviteByEmailsRequest
+} from '@app/services/server/requests';
+import { NextResponse } from 'next/server';
+
+export async function GET(req: Request) {
+ const res = new NextResponse();
+ const { $res, user, access_token, tenantId } = await authenticatedGuard(req, res);
+ if (!user) return NextResponse.json({ errors: 'Unauthorized' }, { status: 401 });
+
+ const { data: employeeRole } = await getEmployeeRoleRequest({
+ tenantId,
+ role: 'EMPLOYEE',
+ bearer_token: access_token
+ });
+
+ return $res(employeeRole);
+}
+
+export async function POST(req: Request) {
+ const res = new NextResponse();
+ const { $res, user, organizationId, access_token, teamId, tenantId } = await authenticatedGuard(req, res);
+ if (!user) return NextResponse.json({ errors: 'Unauthorized' }, { status: 401 });
+
+ const { origin } = new URL(req.url);
+
+ const callbackUrl = `${origin}${INVITE_CALLBACK_PATH}`;
+
+ const body = (await req.json()) as IInviteRequest;
+
+ const { errors, isValid: formValid } = validateForm(['email', 'name'], body);
+
+ if (!formValid) {
+ return NextResponse.json({ errors }, { status: 400 });
+ }
+
+ const { data: employeeRole } = await getEmployeeRoleRequest({
+ tenantId,
+ role: 'EMPLOYEE',
+ bearer_token: access_token
+ });
+
+ const date = new Date();
+
+ date.setDate(date.getDate() - 1);
+
+ await inviteByEmailsRequest(
+ {
+ emailIds: [body.email],
+ projectIds: [],
+ departmentIds: [],
+ organizationContactIds: [],
+ teamIds: [teamId],
+ roleId: employeeRole?.id || '',
+ invitationExpirationPeriod: 'Never',
+ inviteType: 'TEAM',
+ appliedDate: null,
+ fullName: body.name,
+ callbackUrl: INVITE_CALLBACK_URL || callbackUrl,
+ organizationId,
+ tenantId,
+ startedWorkOn: date.toISOString()
+ },
+ access_token
+ );
+
+ const { data } = await getTeamInvitationsRequest(
+ {
+ tenantId,
+ teamId,
+ organizationId,
+ role: 'EMPLOYEE'
+ },
+ access_token
+ );
+
+ return $res(data);
+}
diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts
index 5cffc679d..efcb8d3dd 100644
--- a/apps/web/app/constants.ts
+++ b/apps/web/app/constants.ts
@@ -2,7 +2,8 @@ import { JitsuOptions } from '@jitsu/jitsu-react/dist/useJitsu';
import { I_SMTPRequest } from './interfaces/ISmtp';
import { getNextPublicEnv } from './env';
import enLanguage from '../messages/en.json';
-
+// import { } from 'country-flag-icons/react/3x2'
+import { BG, CN, DE, ES, FR, IS, IT, NL, PL, PT, RU, SA, US } from 'country-flag-icons/react/1x1';
export const API_BASE_URL = '/api';
export const DEFAULT_APP_PATH = '/auth/passcode';
export const DEFAULT_MAIN_PATH = '/';
@@ -194,3 +195,71 @@ export enum IssuesView {
export const TaskStatus = {
INPROGRESS: 'in-progress'
};
+
+export const languagesFlags = [
+ {
+ Flag: US,
+ country: 'United Kingdom',
+ code: 'en'
+ },
+ {
+ Flag: CN,
+ country: 'China',
+ code: 'zh'
+ },
+ {
+ Flag: ES,
+ country: 'Spain',
+ code: 'es'
+ },
+ {
+ Flag: RU,
+ country: 'Russia',
+ code: 'ru'
+ },
+ {
+ Flag: PT,
+ country: 'Portugal',
+ code: 'pt'
+ },
+ {
+ Flag: IT,
+ country: 'Italy',
+ code: 'it'
+ },
+ {
+ Flag: DE,
+ country: 'Germany',
+ code: 'de'
+ },
+ {
+ Flag: BG,
+ country: 'Bulgaria',
+ code: 'bg'
+ },
+ {
+ Flag: SA,
+ country: 'Saudi Arabia',
+ code: 'ar'
+ },
+ {
+ Flag: NL,
+ country: 'Netherlands',
+ code: 'nl'
+ },
+ {
+ Flag: FR,
+ country: 'France',
+ code: 'fr'
+ },
+ {
+ Flag: PL,
+ country: 'Poland',
+ code: 'pl'
+ },
+ {
+ Flag: IS,
+ country: 'Israel',
+ code: 'he'
+ }
+];
diff --git a/apps/web/app/helpers/cookies/index.ts b/apps/web/app/helpers/cookies/index.ts
index 431e42694..44629f59a 100644
--- a/apps/web/app/helpers/cookies/index.ts
+++ b/apps/web/app/helpers/cookies/index.ts
@@ -40,8 +40,7 @@ type NextCtx = {
export const setLargeStringInCookies = (
COOKIE_NAME: string,
largeString: string,
- req: any,
- res: any,
+ ctx: NextCtx | undefined,
crossSite = false
) => {
const chunkSize = 4000;
@@ -50,10 +49,10 @@ export const setLargeStringInCookies = (
chunks.forEach((chunk, index) => {
const cookieValue = chunk.join('');
- setCookie(`${COOKIE_NAME}${index}`, cookieValue, { res, req }, crossSite);
+ setCookie(`${COOKIE_NAME}${index}`, cookieValue, ctx, crossSite);
});
- setCookie(`${COOKIE_NAME}_totalChunks`, chunks.length, { res, req }, crossSite);
+ setCookie(`${COOKIE_NAME}_totalChunks`, chunks.length, ctx, crossSite);
};
export const getLargeStringFromCookies = (COOKIE_NAME: string, ctx?: NextCtx) => {
@@ -95,12 +94,12 @@ export function setAuthCookies(datas: DataParams, ctx?: NextCtx) {
// Handle Large Access Token
// Cookie can support upto 4096 characters only!
if (access_token.length <= 4096) {
- setCookie(TOKEN_COOKIE_NAME, access_token, ctx, true); // cross site cookie
+ setCookie(TOKEN_COOKIE_NAME, access_token, ctx); // cross site cookie
} else {
- setLargeStringInCookies(TOKEN_COOKIE_NAME, access_token, ctx?.req, ctx?.res, true); // cross site cookie
+ setLargeStringInCookies(TOKEN_COOKIE_NAME, access_token, ctx); // cross site cookie
}
- setCookie(REFRESH_TOKEN_COOKIE_NAME, refresh_token.token, ctx, true); // cross site cookie
+ setCookie(REFRESH_TOKEN_COOKIE_NAME, refresh_token.token, ctx); // cross site cookie
setCookie(ACTIVE_TEAM_COOKIE_NAME, teamId, ctx);
setCookie(TENANT_ID_COOKIE_NAME, tenantId, ctx);
setCookie(ORGANIZATION_ID_COOKIE_NAME, organizationId, ctx);
diff --git a/apps/web/app/helpers/date.ts b/apps/web/app/helpers/date.ts
index 06e44c3a8..ace5da630 100644
--- a/apps/web/app/helpers/date.ts
+++ b/apps/web/app/helpers/date.ts
@@ -115,3 +115,38 @@ export const calculateRemainingDays = (startDate: string, endDate: string): numb
return moment(endDate).diff(startDate, 'days');
};
+
+export const tomorrowDate = moment().add(1, 'days').toDate();
+
+export const yesterdayDate = moment().subtract(1, 'days').toDate();
+
+export const formatDayPlanDate = (dateString: string | Date, format?: string) => {
+ if (dateString.toString().length > 10) {
+ dateString = dateString.toString().split('T')[0];
+ }
+ const date = moment(dateString, 'YYYY-MM-DD');
+
+ if (date.isSame(moment(), 'day')) return 'Today';
+ if (date.isSame(moment().add(1, 'day'), 'day')) return 'Tomorrow';
+ if (date.isSame(moment().subtract(1, 'day'), 'day')) return 'Yesterday';
+ if (format === 'DD MMM YYYY') return formatDateString(dateString.toString());
+ return date.format('dddd, MMMM DD, YYYY');
+};
+
+// Formats a given number into hours
+export const formatIntegerToHour = (number: number) => {
+ // Separate decimal and in parts
+ const integerPart = Math.floor(number);
+ const decimalPart = number - integerPart;
+
+ // Format int part with 'h'
+ let formattedHour = `${integerPart}h`;
+
+ // if the decimal part is not zero, add minutes
+ if (decimalPart !== 0) {
+ const minutes = Math.round(decimalPart * 60);
+ formattedHour += `${minutes < 10 ? '0' : ''}${minutes}m`;
+ }
+
+ return formattedHour;
+};
diff --git a/apps/web/app/helpers/index.ts b/apps/web/app/helpers/index.ts
index 681b8e9f1..5ec7a1bb8 100644
--- a/apps/web/app/helpers/index.ts
+++ b/apps/web/app/helpers/index.ts
@@ -8,3 +8,4 @@ export * from './regex';
export * from './validations';
export * from './colors';
export * from './strings';
+export * from './plan-day-badge';
diff --git a/apps/web/app/helpers/plan-day-badge.ts b/apps/web/app/helpers/plan-day-badge.ts
new file mode 100644
index 000000000..b40497c5d
--- /dev/null
+++ b/apps/web/app/helpers/plan-day-badge.ts
@@ -0,0 +1,25 @@
+import { IDailyPlan, ITeamTask } from '@app/interfaces';
+import { formatDayPlanDate } from './date';
+
+export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): string | null => {
+ // Search a plan that contains a given task
+ const plan = plans.find((plan) => plan.tasks?.some((task) => task.id === taskId));
+
+ // If at least one plan have this task
+ if (plan) {
+ // Check if the task appears in other plans
+ const otherPlansWithTask = plans.filter(
+ (pl) => pl.id !== plan.id && pl.tasks?.some((tsk) => tsk.id === taskId)
+ );
+
+ // If the task exists in other plans, the its planned many days
+ if (otherPlansWithTask.length > 0) {
+ return 'Planned';
+ } else {
+ return `Planned ${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`;
+ }
+ // The task does not exist in any plan
+ } else {
+ return null;
+ }
+};
diff --git a/apps/web/app/helpers/validations.ts b/apps/web/app/helpers/validations.ts
index d0db0fa89..df20293ff 100644
--- a/apps/web/app/helpers/validations.ts
+++ b/apps/web/app/helpers/validations.ts
@@ -24,7 +24,7 @@ export const authFormValidate = (keys: (keyof IRegisterDataAPI)[], values: IRegi
}
break;
case 'recaptcha':
- if (RECAPTCHA_SITE_KEY) {
+ if (RECAPTCHA_SITE_KEY?.value) {
if (!values['recaptcha'] || values['recaptcha'].trim().length < 2) {
err['recaptcha'] = 'Please check the ReCaptcha checkbox before continue';
}
diff --git a/apps/web/app/hooks/auth/useAuthenticationPasscode.ts b/apps/web/app/hooks/auth/useAuthenticationPasscode.ts
index 986a9c46c..e51746f1e 100644
--- a/apps/web/app/hooks/auth/useAuthenticationPasscode.ts
+++ b/apps/web/app/hooks/auth/useAuthenticationPasscode.ts
@@ -21,11 +21,11 @@ type AuthCodeRef = {
};
export function useAuthenticationPasscode() {
+ const router = useRouter();
const pathname = usePathname();
const query = useSearchParams();
- const queryTeamId = useMemo(() => {
- return query?.get('teamId');
- }, [query]);
+
+ const queryTeamId = query?.get('teamId');
const queryEmail = useMemo(() => {
const emailQuery = query?.get('email') || '';
@@ -52,7 +52,6 @@ export function useAuthenticationPasscode() {
});
const [errors, setErrors] = useState({} as { [x: string]: any });
- const router = useRouter();
// Queries
const { queryCall: sendCodeQueryCall, loading: sendCodeLoading } = useQuery(sendAuthCodeAPI);
@@ -78,10 +77,17 @@ export function useAuthenticationPasscode() {
const verifySignInEmailConfirmRequest = async ({ email, code }: { email: string; code: string }) => {
signInEmailConfirmQueryCall(email, code)
.then((res) => {
+ if ('team' in res.data) {
+ router.replace('/');
+ return;
+ }
+
const checkError: {
message: string;
} = res.data as any;
+
const isError = checkError.message === 'Unauthorized';
+
if (isError) {
setErrors({
code: 'Invalid code. Please try again.'
@@ -89,6 +95,7 @@ export function useAuthenticationPasscode() {
} else {
setErrors({});
}
+
const data = res.data as ISigninEmailConfirmResponse;
if (!data.workspaces) {
return;
@@ -110,6 +117,7 @@ export function useAuthenticationPasscode() {
signInToWorkspaceRequest({
email: email,
+ code: code,
token: currentWorkspace?.token as string,
selectedTeam: queryTeamId as string
});
@@ -156,16 +164,13 @@ export function useAuthenticationPasscode() {
[queryCall]
);
- const signInToWorkspaceRequest = ({
- email,
- token,
- selectedTeam
- }: {
+ const signInToWorkspaceRequest = (params: {
email: string;
token: string;
selectedTeam: string;
+ code?: string;
}) => {
- signInWorkspaceQueryCall(email, token, selectedTeam)
+ signInWorkspaceQueryCall(params)
.then(() => {
setAuthenticated(true);
router.push('/');
@@ -227,6 +232,7 @@ export function useAuthenticationPasscode() {
signInToWorkspaceRequest({
email: formValues.email,
+ code: formValues.code,
token,
selectedTeam
});
@@ -285,7 +291,6 @@ export function useAuthenticationPasscode() {
workspaces,
sendCodeQueryCall,
signInWorkspaceLoading,
- queryCall,
handleWorkspaceSubmit
};
}
diff --git a/apps/web/app/hooks/auth/useAuthenticationPassword.ts b/apps/web/app/hooks/auth/useAuthenticationPassword.ts
index 4c8fe64d2..5858556cb 100644
--- a/apps/web/app/hooks/auth/useAuthenticationPassword.ts
+++ b/apps/web/app/hooks/auth/useAuthenticationPassword.ts
@@ -87,7 +87,7 @@ export function useAuthenticationPassword() {
token: string;
selectedTeam: string;
}) => {
- signInWorkspaceQueryCall(email, token, selectedTeam)
+ signInWorkspaceQueryCall({ email, token, selectedTeam })
.then(() => {
setAuthenticated(true);
router.push('/');
diff --git a/apps/web/app/hooks/auth/useAuthenticationSocialLogin.ts b/apps/web/app/hooks/auth/useAuthenticationSocialLogin.ts
new file mode 100644
index 000000000..10bce10a6
--- /dev/null
+++ b/apps/web/app/hooks/auth/useAuthenticationSocialLogin.ts
@@ -0,0 +1,97 @@
+'use client';
+
+import { setAuthCookies } from '@app/helpers';
+import { useCallback, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import { getUserOrganizationsRequest, signInWorkspaceAPI } from '@app/services/client/api/auth/invite-accept';
+import { ISigninEmailConfirmWorkspaces } from '@app/interfaces';
+import { useSession } from 'next-auth/react';
+type SigninResult = {
+ access_token: string;
+ confirmed_mail: string;
+ organizationId: string;
+ refresh_token: {
+ token: string;
+ decoded: any;
+ };
+ tenantId: string;
+ userId: string;
+};
+
+export function useAuthenticationSocialLogin() {
+ const router = useRouter();
+ const [signInWorkspaceLoading, setSignInWorkspaceLoading] = useState(false);
+
+ const { update: updateNextAuthSession }: any = useSession();
+
+ const updateOAuthSession = useCallback(
+ (
+ signinResult: SigninResult,
+ workspaces: ISigninEmailConfirmWorkspaces[],
+ selectedWorkspace: number,
+ selectedTeam: string
+ ) => {
+ setSignInWorkspaceLoading(true);
+ signInWorkspaceAPI(signinResult.confirmed_mail, workspaces[selectedWorkspace].token)
+ .then(async (result) => {
+ const tenantId = result.user?.tenantId || '';
+ const access_token = result.token;
+ const userId = result.user?.id;
+
+ const organizations = await getUserOrganizationsRequest({
+ tenantId,
+ userId,
+ token: access_token
+ });
+ const organization = organizations?.data.items[0];
+ if (!organization) {
+ return Promise.reject({
+ errors: {
+ email: 'Your account is not yet ready to be used on the Ever Teams Platform'
+ }
+ });
+ }
+
+ updateNextAuthSession({
+ access_token,
+ refresh_token: {
+ token: result.refresh_token
+ },
+ teamId: selectedTeam,
+ tenantId,
+ organizationId: organization?.organizationId,
+ languageId: 'en',
+ noTeamPopup: true,
+
+ userId,
+ workspaces: workspaces,
+ confirmed_mail: signinResult.confirmed_mail
+ });
+
+ setAuthCookies({
+ access_token,
+ refresh_token: {
+ token: result.refresh_token
+ },
+ teamId: selectedTeam,
+ tenantId,
+ organizationId: organization?.organizationId,
+ languageId: 'en',
+ noTeamPopup: undefined,
+ userId
+ });
+ setSignInWorkspaceLoading(false);
+ router.push('/');
+ })
+ .catch((err) => console.log(err));
+ },
+ [router, updateNextAuthSession]
+ );
+
+ return {
+ updateOAuthSession,
+ signInWorkspaceLoading
+ };
+}
+
+export type TAuthenticationSocial = ReturnType
;
diff --git a/apps/web/app/hooks/auth/useAuthenticationTeam.ts b/apps/web/app/hooks/auth/useAuthenticationTeam.ts
index eebec90fe..72d017aa5 100644
--- a/apps/web/app/hooks/auth/useAuthenticationTeam.ts
+++ b/apps/web/app/hooks/auth/useAuthenticationTeam.ts
@@ -9,7 +9,7 @@ import { AxiosError } from 'axios';
import { useCallback, useMemo, useState } from 'react';
import { useQuery } from '../useQuery';
import { RECAPTCHA_SITE_KEY } from '@app/constants';
-import { useSearchParams } from 'next/navigation';
+import { useRouter, useSearchParams } from 'next/navigation';
const FIRST_STEP = 'STEP1' as const;
const SECOND_STEP = 'STEP2' as const;
@@ -25,15 +25,17 @@ const initialValues: IRegisterDataAPI = RECAPTCHA_SITE_KEY
email: '',
team: '',
recaptcha: ''
- }
+ }
: {
name: '',
email: '',
team: ''
- };
+ };
export function useAuthenticationTeam() {
const query = useSearchParams();
+ const router = useRouter();
+
const queryEmail = useMemo(() => {
let localEmail: null | string = null;
@@ -70,6 +72,7 @@ export function useAuthenticationTeam() {
const { errors, valid } = authFormValidate(validationFields, formValues);
if (!valid) {
+ console.log({ errors });
setErrors(errors as any);
return;
}
@@ -78,7 +81,7 @@ export function useAuthenticationTeam() {
infiniteLoading.current = true;
queryCall(formValues)
- .then(() => window.location.reload())
+ .then(() => router.push('/'))
.catch((err: AxiosError) => {
if (err.response?.status === 400) {
setErrors((err.response?.data as any)?.errors || {});
diff --git a/apps/web/app/hooks/features/useAuthTeamTasks.ts b/apps/web/app/hooks/features/useAuthTeamTasks.ts
index c6853703a..ec57b486b 100644
--- a/apps/web/app/hooks/features/useAuthTeamTasks.ts
+++ b/apps/web/app/hooks/features/useAuthTeamTasks.ts
@@ -1,11 +1,12 @@
import { IUser } from '@app/interfaces';
-import { tasksByTeamState } from '@app/stores';
+import { profileDailyPlanListState, tasksByTeamState } from '@app/stores';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { useOrganizationTeams } from './useOrganizationTeams';
export function useAuthTeamTasks(user: IUser | undefined) {
const tasks = useRecoilValue(tasksByTeamState);
+ const plans = useRecoilValue(profileDailyPlanListState);
const { activeTeam } = useOrganizationTeams();
const currentMember = activeTeam?.members?.find((member) => member.employee?.userId === user?.id);
@@ -24,6 +25,11 @@ export function useAuthTeamTasks(user: IUser | undefined) {
});
}, [tasks, user]);
+ const dailyplan = useMemo(() => {
+ if (!user) return [];
+ return plans.items;
+ }, [plans, user]);
+
const totalTodayTasks = useMemo(
() =>
currentMember?.totalTodayTasks && currentMember?.totalTodayTasks.length
@@ -41,6 +47,7 @@ export function useAuthTeamTasks(user: IUser | undefined) {
return {
assignedTasks,
unassignedTasks,
- workedTasks
+ workedTasks,
+ dailyplan
};
}
diff --git a/apps/web/app/hooks/features/useAuthenticateUser.ts b/apps/web/app/hooks/features/useAuthenticateUser.ts
index 0dcd403bf..9d6233dc6 100644
--- a/apps/web/app/hooks/features/useAuthenticateUser.ts
+++ b/apps/web/app/hooks/features/useAuthenticateUser.ts
@@ -10,6 +10,8 @@ import { useRecoilState } from 'recoil';
import { useQuery } from '../useQuery';
import { useIsMemberManager } from './useTeamMember';
+import { useOrganizationTeams } from './useOrganizationTeams';
+import { useUserProfilePage } from './useUserProfilePage';
export const useAuthenticateUser = (defaultUser?: IUser) => {
const [user, setUser] = useRecoilState(userState);
@@ -68,3 +70,19 @@ export const useAuthenticateUser = (defaultUser?: IUser) => {
refreshToken
};
};
+
+/**
+ * A hook to check if the current user is a manager or whom the current profile belongs to
+ *
+ * @description We need, especially for the user profile page, to know if the current user can see some activities, or interact with data
+ * @returns a boolean that defines in the user is authorized
+ */
+
+export const useCanSeeActivityScreen = () => {
+ const { user } = useAuthenticateUser();
+ const { activeTeamManagers } = useOrganizationTeams();
+ const profile = useUserProfilePage();
+
+ const isManagerConnectedUser = activeTeamManagers.findIndex((member) => member.employee?.user?.id == user?.id);
+ return profile.userProfile?.id === user?.id || isManagerConnectedUser != -1;
+};
diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts
new file mode 100644
index 000000000..9482f3195
--- /dev/null
+++ b/apps/web/app/hooks/features/useDailyPlan.ts
@@ -0,0 +1,167 @@
+'use client';
+
+import { useRecoilState } from 'recoil';
+import { useCallback, useEffect } from 'react';
+import { useQuery } from '../useQuery';
+import {
+ dailyPlanFetchingState,
+ dailyPlanListState,
+ profileDailyPlanListState,
+ taskPlans,
+ userState
+} from '@app/stores';
+import {
+ addTaskToPlanAPI,
+ createDailyPlanAPI,
+ deleteDailyPlanAPI,
+ getAllDayPlansAPI,
+ getDayPlansByEmployeeAPI,
+ getPlansByTaskAPI,
+ removeTaskFromPlanAPI,
+ updateDailyPlanAPI
+} from '@app/services/client/api';
+import { ICreateDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces';
+import { useFirstLoad } from '../useFirstLoad';
+
+export function useDailyPlan() {
+ const [user] = useRecoilState(userState);
+
+ const { loading, queryCall } = useQuery(getDayPlansByEmployeeAPI);
+ const { loading: getAllDayPlansLoading, queryCall: getAllQueryCall } = useQuery(getAllDayPlansAPI);
+ const { loading: createDailyPlanLoading, queryCall: createQueryCall } = useQuery(createDailyPlanAPI);
+ const { loading: updateDailyPlanLoading, queryCall: updateQueryCall } = useQuery(updateDailyPlanAPI);
+ const { loading: getPlansByTaskLoading, queryCall: getPlansByTaskQueryCall } = useQuery(getPlansByTaskAPI);
+ const { loading: addTaskToPlanLoading, queryCall: addTaskToPlanQueryCall } = useQuery(addTaskToPlanAPI);
+ const { loading: removeTaskFromPlanLoading, queryCall: removeTAskFromPlanQueryCall } =
+ useQuery(removeTaskFromPlanAPI);
+ const { loading: deleteDailyPlanLoading, queryCall: deleteDailyPlanQueryCall } = useQuery(deleteDailyPlanAPI);
+
+ const [dailyPlan, setDailyPlan] = useRecoilState(dailyPlanListState);
+ const [profileDailyPlans, setProfileDailyPlans] = useRecoilState(profileDailyPlanListState);
+ const [taskPlanList, setTaskPlans] = useRecoilState(taskPlans);
+ const [dailyPlanFetching, setDailyPlanFetching] = useRecoilState(dailyPlanFetchingState);
+ const { firstLoadData: firstLoadDailyPlanData, firstLoad } = useFirstLoad();
+
+ useEffect(() => {
+ if (firstLoad) {
+ setDailyPlanFetching(loading);
+ }
+ }, [loading, firstLoad, setDailyPlanFetching]);
+
+ const getAllDayPlans = useCallback(() => {
+ getAllQueryCall().then((response) => {
+ if (response.data.items.length) {
+ const { items, total } = response.data;
+ setDailyPlan({ items, total });
+ }
+ });
+ }, [getAllQueryCall, setDailyPlan]);
+
+ const getEmployeeDayPlans = useCallback(
+ (employeeId: string) => {
+ queryCall(employeeId).then((response) => {
+ const { items, total } = response.data;
+ setProfileDailyPlans({ items, total });
+ });
+ },
+ [queryCall, setProfileDailyPlans]
+ );
+
+ const getPlansByTask = useCallback(
+ (taskId?: string) => {
+ getPlansByTaskQueryCall(taskId).then((response) => {
+ setTaskPlans(response.data.items);
+ });
+ },
+ [getPlansByTaskQueryCall, setTaskPlans]
+ );
+
+ const createDailyPlan = useCallback(
+ async (data: ICreateDailyPlan) => {
+ if (user?.tenantId) {
+ const res = await createQueryCall(data, user?.tenantId || '');
+ setProfileDailyPlans({
+ total: profileDailyPlans.total + 1,
+ items: [...profileDailyPlans.items, res.data]
+ });
+ return res;
+ }
+ },
+ [createQueryCall, profileDailyPlans.items, profileDailyPlans.total, setProfileDailyPlans, user?.tenantId]
+ );
+
+ const updateDailyPlan = useCallback(
+ async (data: IUpdateDailyPlan, planId: string) => {
+ const res = await updateQueryCall(data, planId);
+ const updated = profileDailyPlans.items.filter((plan) => plan.id != planId);
+ setProfileDailyPlans({ total: profileDailyPlans.total, items: [...updated, res.data] });
+ return res;
+ },
+ [profileDailyPlans.items, profileDailyPlans.total, setProfileDailyPlans, updateQueryCall]
+ );
+
+ const addTaskToPlan = useCallback(
+ async (data: IDailyPlanTasksUpdate, planId: string) => {
+ const res = await addTaskToPlanQueryCall(data, planId);
+ const updated = profileDailyPlans.items.filter((plan) => plan.id != planId);
+ setProfileDailyPlans({ total: profileDailyPlans.total, items: [...updated, res.data] });
+ return res;
+ },
+ [addTaskToPlanQueryCall, profileDailyPlans.items, profileDailyPlans.total, setProfileDailyPlans]
+ );
+
+ const removeTaskFromPlan = useCallback(
+ async (data: IDailyPlanTasksUpdate, planId: string) => {
+ const res = await removeTAskFromPlanQueryCall(data, planId);
+ const updated = profileDailyPlans.items.filter((plan) => plan.id != planId);
+ setProfileDailyPlans({ total: profileDailyPlans.total, items: [...updated, res.data] });
+ return res;
+ },
+ [profileDailyPlans.items, profileDailyPlans.total, removeTAskFromPlanQueryCall, setProfileDailyPlans]
+ );
+
+ const deleteDailyPlan = useCallback(
+ async (planId: string) => {
+ const res = await deleteDailyPlanQueryCall(planId);
+ const updated = profileDailyPlans.items.filter((plan) => plan.id != planId);
+ setProfileDailyPlans({ total: updated.length, items: [...updated] });
+ return res;
+ },
+ [deleteDailyPlanQueryCall, profileDailyPlans.items, setProfileDailyPlans]
+ );
+
+ return {
+ dailyPlan,
+ profileDailyPlans,
+ setDailyPlan,
+ dailyPlanFetching,
+
+ taskPlanList,
+
+ firstLoadDailyPlanData,
+
+ getAllDayPlans,
+ getAllDayPlansLoading,
+
+ getEmployeeDayPlans,
+ loading,
+
+ getPlansByTask,
+ getPlansByTaskLoading,
+
+ createDailyPlan,
+ createDailyPlanLoading,
+
+ updateDailyPlan,
+ updateDailyPlanLoading,
+
+ addTaskToPlan,
+ addTaskToPlanLoading,
+
+ removeTaskFromPlan,
+ removeTaskFromPlanLoading,
+
+ deleteDailyPlan,
+ deleteDailyPlanLoading
+ };
+}
diff --git a/apps/web/app/hooks/features/useKanban.ts b/apps/web/app/hooks/features/useKanban.ts
index 852e1a348..42596ba2e 100644
--- a/apps/web/app/hooks/features/useKanban.ts
+++ b/apps/web/app/hooks/features/useKanban.ts
@@ -80,11 +80,11 @@ export function useKanban() {
};
const isColumnCollapse = (column: string) => {
- const columnData = taskStatusHook.taskStatus.filter((taskStatus: ITaskStatusItemList) => {
+ const columnData = taskStatusHook.taskStatus.find((taskStatus: ITaskStatusItemList) => {
return taskStatus.name === column;
});
- return columnData[0].isCollapsed;
+ return columnData?.isCollapsed;
};
const reorderStatus = (itemStatus: string, index: number) => {
diff --git a/apps/web/app/hooks/features/useLanguageSettings.ts b/apps/web/app/hooks/features/useLanguageSettings.ts
index 16bad43a6..f5bd7c535 100644
--- a/apps/web/app/hooks/features/useLanguageSettings.ts
+++ b/apps/web/app/hooks/features/useLanguageSettings.ts
@@ -39,14 +39,10 @@ export function useLanguageSettings() {
const loadLanguagesData = useCallback(() => {
setActiveLanguageId(getActiveLanguageIdCookie());
- if (user) {
- return queryCall(user.role.isSystem).then((res) => {
- setLanguages(
- res?.data?.items.filter((item: any) => APPLICATION_LANGUAGES_CODE.includes(item.code)) || []
- );
- return res;
- });
- }
+ return queryCall(user?.role?.isSystem ?? false).then((res) => {
+ setLanguages(res?.data?.items.filter((item: any) => APPLICATION_LANGUAGES_CODE.includes(item.code)) || []);
+ return res;
+ });
}, [queryCall, setActiveLanguageId, setLanguages, user]);
const setActiveLanguage = useCallback(
diff --git a/apps/web/app/hooks/features/useOrganizationTeams.ts b/apps/web/app/hooks/features/useOrganizationTeams.ts
index 8f2879c8f..fe9bc7406 100644
--- a/apps/web/app/hooks/features/useOrganizationTeams.ts
+++ b/apps/web/app/hooks/features/useOrganizationTeams.ts
@@ -21,7 +21,6 @@ import {
activeTeamManagersState,
activeTeamState,
isTeamMemberState,
- memberActiveTaskIdState,
organizationTeamsState,
teamsFetchingState,
timerStatusState
@@ -181,20 +180,22 @@ export function useOrganizationTeams() {
const { updateUserFromAPI, refreshToken, user } = useAuthenticateUser();
const timerStatus = useRecoilValue(timerStatusState);
- const setMemberActiveTaskId = useSetRecoilState(memberActiveTaskIdState);
+ // const setMemberActiveTaskId = useSetRecoilState(memberActiveTaskIdState);
const currentUser = activeTeam?.members?.find((member) => member.employee.userId === user?.id);
+
const memberActiveTaskId =
(timerStatus?.running && timerStatus?.lastLog?.taskId) || currentUser?.activeTaskId || null;
+
const isTrackingEnabled = activeTeam?.members?.find(
(member) => member.employee.userId === user?.id && member.isTrackingEnabled
)
? true
: false;
- useEffect(() => {
- setMemberActiveTaskId(memberActiveTaskId);
- }, [setMemberActiveTaskId, memberActiveTaskId]);
+ // useEffect(() => {
+ // setMemberActiveTaskId(memberActiveTaskId);
+ // }, [setMemberActiveTaskId, memberActiveTaskId]);
// Updaters
const { createOrganizationTeam, loading: createOTeamLoading } = useCreateOrganizationTeam();
diff --git a/apps/web/app/hooks/features/useTaskActivity.ts b/apps/web/app/hooks/features/useTaskActivity.ts
index 0645f8104..4764ec406 100644
--- a/apps/web/app/hooks/features/useTaskActivity.ts
+++ b/apps/web/app/hooks/features/useTaskActivity.ts
@@ -24,7 +24,6 @@ export function useTaskTimeSheets(id: string) {
organizationId: user?.employee.organizationId ?? '',
taskId: id
}).then((response) => {
- console.log(response);
if (response.data) {
console.log(response.data);
setTaskTimesheets(response.data);
diff --git a/apps/web/app/hooks/features/useTaskEstimation.ts b/apps/web/app/hooks/features/useTaskEstimation.ts
index cd203d45f..3e5c02724 100644
--- a/apps/web/app/hooks/features/useTaskEstimation.ts
+++ b/apps/web/app/hooks/features/useTaskEstimation.ts
@@ -18,7 +18,7 @@ export function useTaskEstimation(task?: Nullable) {
const { h, m } = secondsToTime($task?.estimate || 0);
setValue({
hours: h ? h.toString() : '',
- minutes: pad(m).toString()
+ minutes: m ? pad(m).toString() : ''
});
}, [$task?.estimate]);
@@ -124,6 +124,7 @@ export function useTaskEstimation(task?: Nullable) {
if (updateLoading || !editableMode) return;
handleSubmit();
}, [updateLoading, editableMode, handleSubmit]);
+
const { targetEl, ignoreElementRef } = useOutsideClick(handleOutsideClick);
return {
diff --git a/apps/web/app/hooks/features/useTaskInput.ts b/apps/web/app/hooks/features/useTaskInput.ts
index 134168405..241ca8f18 100644
--- a/apps/web/app/hooks/features/useTaskInput.ts
+++ b/apps/web/app/hooks/features/useTaskInput.ts
@@ -1,6 +1,6 @@
'use client';
-import { useAuthenticateUser, useModal, useSyncRef } from '@app/hooks';
+import { useAuthenticateUser, useModal, useSyncRef, useTaskStatus } from '@app/hooks';
import { useTeamTasks } from '@app/hooks/features/useTeamTasks';
import { ITaskLabelsItemList, Nullable } from '@app/interfaces';
import { ITaskStatus, ITeamTask } from '@app/interfaces/ITask';
@@ -36,7 +36,7 @@ export function useTaskInput({
} = {}) {
const { isOpen: isModalOpen, openModal, closeModal } = useModal();
const [closeableTask, setCloseableTaskTask] = useState(null);
-
+ const { taskStatus: taskStatusList } = useTaskStatus();
const {
tasks: teamTasks,
activeTeamTask,
@@ -50,8 +50,7 @@ export function useTaskInput({
const { user } = useAuthenticateUser();
const userRef = useSyncRef(user);
-
- const taskIssue = useRef(null);
+ const [taskIssue, setTaskIssue] = useState('');
const taskStatus = useRef(null);
const taskPriority = useRef(null);
const taskSize = useRef(null);
@@ -61,18 +60,12 @@ export function useTaskInput({
const tasks = customTasks || teamTasks;
const memberActiveTaskId = useRecoilValue(memberActiveTaskIdState);
+
const memberActiveTask = useMemo(() => {
return tasks.find((item) => item.id === memberActiveTaskId) || null;
}, [memberActiveTaskId, tasks]);
- /**
- * If task has null value then consider it as value 😄
- */
- const inputTask = initEditMode
- ? task !== undefined
- ? task
- : activeTeamTask
- : memberActiveTask || (task !== undefined ? task : activeTeamTask);
+ const inputTask = initEditMode ? task ?? activeTeamTask : memberActiveTask ?? task ?? activeTeamTask;
const [filter, setFilter] = useState<'closed' | 'open'>('open');
const [editMode, setEditMode] = useState(initEditMode || false);
@@ -125,7 +118,6 @@ export function useTaskInput({
.startsWith(query.toLowerCase().replace(/\s+/g, ''));
});
}, [query, tasks]);
-
const hasCreateForm = filteredTasks2.length === 0 && query !== '';
const handleTaskCreation = ({
@@ -140,11 +132,13 @@ export function useTaskInput({
}[];
} = {}) => {
if (query.trim().length < 2 || inputTask?.title === query.trim() || !userRef.current?.isEmailVerified) return;
-
+ const openId = taskStatusList.find((item) => item.value === 'open')?.id;
+ const statusId = taskStatusList.find((item) => item.name === taskStatus.current)?.id;
return createTask(
{
taskName: query.trim(),
- issueType: taskIssue.current || 'Bug',
+ issueType: taskIssue || 'Bug',
+ taskStatusId: statusId || (openId as string),
status: taskStatus.current || undefined,
priority: taskPriority.current || undefined,
size: taskSize.current || undefined,
@@ -154,6 +148,8 @@ export function useTaskInput({
!autoAssignTaskAuth ? assignToUsers : undefined
).then((res) => {
setQuery('');
+ localStorage.setItem('lastTaskIssue', taskIssue || 'Bug');
+ setTaskIssue('');
const items = res.data?.items || [];
const created = items.find((t) => t.title === query.trim());
if (created && autoActiveTask) setActiveTask(created);
@@ -183,7 +179,7 @@ export function useTaskInput({
}).length;
useEffect(() => {
- taskIssue.current = null;
+ setTaskIssue('');
}, [hasCreateForm]);
return {
@@ -209,6 +205,7 @@ export function useTaskInput({
filter,
updateTaskTitleHandler,
taskIssue,
+ setTaskIssue,
taskStatus,
taskPriority,
taskSize,
diff --git a/apps/web/app/hooks/features/useTaskVersion.ts b/apps/web/app/hooks/features/useTaskVersion.ts
index f96044907..f4e8694ca 100644
--- a/apps/web/app/hooks/features/useTaskVersion.ts
+++ b/apps/web/app/hooks/features/useTaskVersion.ts
@@ -14,10 +14,9 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { useFirstLoad } from '../useFirstLoad';
import { useQuery } from '../useQuery';
import isEqual from 'lodash/isEqual';
-import { useCallbackRef } from '../useCallbackRef';
import { getActiveTeamIdCookie } from '@app/helpers';
-export function useTaskVersion(onVersionCreated?: (version: ITaskVersionCreate) => void) {
+export function useTaskVersion() {
const [user] = useRecoilState(userState);
const activeTeamId = useRecoilValue(activeTeamIdState);
@@ -27,7 +26,6 @@ export function useTaskVersion(onVersionCreated?: (version: ITaskVersionCreate)
const { loading: editTaskVersionLoading, queryCall: editQueryCall } = useQuery(editTaskVersionAPI);
const [taskVersion, setTaskVersion] = useRecoilState(taskVersionListState);
- const $onVersionCreated = useCallbackRef(onVersionCreated);
const [taskVersionFetching, setTaskVersionFetching] = useRecoilState(taskVersionFetchingState);
const { firstLoadData: firstLoadTaskVersionData, firstLoad } = useFirstLoad();
@@ -68,7 +66,7 @@ export function useTaskVersion(onVersionCreated?: (version: ITaskVersionCreate)
}
},
- [$onVersionCreated, createQueryCall, createTaskVersionLoading, deleteTaskVersionLoading, activeTeamId]
+ [createQueryCall, createTaskVersionLoading, deleteTaskVersionLoading, activeTeamId]
);
const deleteTaskVersion = useCallback(
diff --git a/apps/web/app/hooks/features/useTeamMemberCard.ts b/apps/web/app/hooks/features/useTeamMemberCard.ts
index 77510197b..43bc27570 100644
--- a/apps/web/app/hooks/features/useTeamMemberCard.ts
+++ b/apps/web/app/hooks/features/useTeamMemberCard.ts
@@ -21,8 +21,9 @@ import cloneDeep from 'lodash/cloneDeep';
* IOrganizationTeamList['members'][number] | undefined
*/
export function useTeamMemberCard(member: IOrganizationTeamList['members'][number] | undefined) {
- const { updateTask, tasks, setActiveTask, deleteEmployeeFromTasks } = useTeamTasks();
-
+ const { updateTask, tasks, setActiveTask, deleteEmployeeFromTasks, unassignAuthActiveTask } = useTeamTasks();
+ const [assignTaskLoading, setAssignTaskLoading] = useState(false);
+ const [unAssignTaskLoading, setUnAssignTaskLoading] = useState(false);
const publicTeam = useRecoilValue(getPublicState);
const allTaskStatistics = useRecoilValue(allTaskStatisticsState);
@@ -167,7 +168,7 @@ export function useTeamMemberCard(member: IOrganizationTeamList['members'][numbe
if (!member?.employeeId) {
return Promise.resolve();
}
-
+ setAssignTaskLoading(true);
return updateTask({
...task,
members: [...task.members, (member?.employeeId ? { id: member?.employeeId } : {}) as any]
@@ -175,6 +176,7 @@ export function useTeamMemberCard(member: IOrganizationTeamList['members'][numbe
if (isAuthUser && !activeTeamTask) {
setActiveTask(task);
}
+ setAssignTaskLoading(false);
});
},
[updateTask, member, isAuthUser, setActiveTask, activeTeamTask]
@@ -185,15 +187,17 @@ export function useTeamMemberCard(member: IOrganizationTeamList['members'][numbe
if (!member?.employeeId) {
return Promise.resolve();
}
+ setUnAssignTaskLoading(true);
return updateTask({
...task,
members: task.members.filter((m) => m.id !== member.employeeId)
}).finally(() => {
- isAuthUser && setActiveTask(null);
+ isAuthUser && unassignAuthActiveTask();
+ setUnAssignTaskLoading(false);
});
},
- [updateTask, member, isAuthUser, setActiveTask]
+ [updateTask, member, isAuthUser, unassignAuthActiveTask]
);
return {
@@ -201,6 +205,8 @@ export function useTeamMemberCard(member: IOrganizationTeamList['members'][numbe
memberUnassignTasks,
isTeamManager,
memberUser,
+ assignTaskLoading,
+ unAssignTaskLoading,
member,
memberTask: memberTaskRef.current,
isAuthUser,
diff --git a/apps/web/app/hooks/features/useTeamTasks.ts b/apps/web/app/hooks/features/useTeamTasks.ts
index 8097779ab..a63926d31 100644
--- a/apps/web/app/hooks/features/useTeamTasks.ts
+++ b/apps/web/app/hooks/features/useTeamTasks.ts
@@ -19,6 +19,7 @@ import {
} from '@app/services/client/api';
import {
activeTeamState,
+ activeTeamTaskId,
detailedTaskState,
// employeeTasksState,
memberActiveTaskIdState,
@@ -48,6 +49,7 @@ export function useTeamTasks() {
const [tasksFetching, setTasksFetching] = useRecoilState(tasksFetchingState);
const authUser = useSyncRef(useRecoilValue(userState));
const memberActiveTaskId = useRecoilValue(memberActiveTaskIdState);
+ const $memberActiveTaskId = useSyncRef(memberActiveTaskId);
// const [employeeState, setEmployeeState] = useRecoilState(employeeTasksState);
const { taskStatus } = useTaskStatus();
const activeTeam = useRecoilValue(activeTeamState);
@@ -74,6 +76,12 @@ export function useTeamTasks() {
const getTaskById = useCallback(
(taskId: string) => {
+ tasksRef.current.forEach((task) => {
+ if (task.id === taskId) {
+ setDetailedTask(task);
+ }
+ });
+
return getTasksByIdQueryCall(taskId).then((res) => {
setDetailedTask(res?.data || null);
return res;
@@ -180,6 +188,7 @@ export function useTeamTasks() {
loadTeamTasksData();
}
}, [activeTeam?.id, firstLoad, loadTeamTasksData]);
+ const setActive = useSetRecoilState(activeTeamTaskId);
// Get the active task from cookie and put on global store
useEffect(() => {
@@ -215,6 +224,7 @@ export function useTeamTasks() {
{
taskName,
issueType,
+ taskStatusId,
status = taskStatus[0]?.name,
priority,
size,
@@ -224,6 +234,7 @@ export function useTeamTasks() {
taskName: string;
issueType?: string;
status?: string;
+ taskStatusId: string;
priority?: string;
size?: string;
tags?: ITaskLabelsItemList[];
@@ -231,13 +242,11 @@ export function useTeamTasks() {
},
members?: { id: string }[]
) => {
- const activeStatus = taskStatus.find((ts) => ts.name == status);
return createQueryCall(
{
title: taskName,
issueType,
status,
- taskStatusId: activeStatus?.id,
priority,
size,
tags,
@@ -246,10 +255,11 @@ export function useTeamTasks() {
...(activeTeam?.projects && activeTeam?.projects.length > 0
? {
projectId: activeTeam.projects[0].id
- }
+ }
: {}),
...(description ? { description: `${description}
` } : {}),
- ...(members ? { members } : {})
+ ...(members ? { members } : {}),
+ taskStatusId: taskStatusId
},
$user.current
).then((res) => {
@@ -264,6 +274,9 @@ export function useTeamTasks() {
const updateTask = useCallback(
(task: Partial & { id: string }) => {
return updateQueryCall(task.id, task).then((res) => {
+ setActive({
+ id: ''
+ });
const updatedTasks = res?.data?.items || [];
deepCheckAndUpdateTasks(updatedTasks, true);
@@ -274,7 +287,7 @@ export function useTeamTasks() {
return res;
});
},
- [updateQueryCall, deepCheckAndUpdateTasks, detailedTask, getTaskById]
+ [updateQueryCall, setActive, deepCheckAndUpdateTasks, detailedTask, getTaskById]
);
const updateTitle = useCallback(
@@ -371,6 +384,20 @@ export function useTeamTasks() {
*/
const setActiveTask = useCallback(
(task: ITeamTask | null) => {
+ /**
+ * Unassign previous active task
+ */
+ if ($memberActiveTaskId.current && $user.current) {
+ const _task = tasksRef.current.find((t) => t.id === $memberActiveTaskId.current);
+
+ if (_task) {
+ updateTask({
+ ..._task,
+ members: _task.members.filter((m) => m.id !== $user.current?.employee.id)
+ });
+ }
+ }
+
setActiveTaskIdCookie(task?.id || '');
setActiveTeamTask(task);
setActiveUserTaskCookieCb(task);
@@ -401,6 +428,11 @@ export function useTeamTasks() {
[deleteEmployeeFromTasksQueryCall]
);
+ const unassignAuthActiveTask = useCallback(() => {
+ setActiveTaskIdCookie('');
+ setActiveTeamTask(null);
+ }, [setActiveTeamTask]);
+
useEffect(() => {
const memberActiveTask = tasks.find((item) => item.id === memberActiveTaskId);
if (memberActiveTask) {
@@ -430,6 +462,7 @@ export function useTeamTasks() {
getTasksByEmployeeIdLoading,
activeTeam,
activeTeamId: activeTeam?.id,
+ unassignAuthActiveTask,
setAllTasks,
loadTeamTasksData,
deleteEmployeeFromTasks,
diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts
index 041a85cc2..35478eb04 100644
--- a/apps/web/app/hooks/features/useTimer.ts
+++ b/apps/web/app/hooks/features/useTimer.ts
@@ -30,6 +30,7 @@ import isEqual from 'lodash/isEqual';
import { useOrganizationEmployeeTeams } from './useOrganizatioTeamsEmployee';
import { useAuthenticateUser } from './useAuthenticateUser';
import moment from 'moment';
+import { usePathname } from 'next/navigation';
const LOCAL_TIMER_STORAGE_KEY = 'local-timer-ever-team';
@@ -151,7 +152,8 @@ function useLocalTimeCounter(timerStatus: ITimerStatus | null, activeTeamTask: I
* It returns a bunch of data and functions related to the timer
*/
export function useTimer() {
- const { updateTask, activeTeamId, activeTeam, activeTeamTask } = useTeamTasks();
+ const pathname = usePathname();
+ const { updateTask, setActiveTask, detailedTask, activeTeamId, activeTeam, activeTeamTask } = useTeamTasks();
const { updateOrganizationTeamEmployeeActiveTask } = useOrganizationEmployeeTeams();
const { user, $user } = useAuthenticateUser();
@@ -247,6 +249,7 @@ export function useTimer() {
// Start timer
const startTimer = useCallback(async () => {
+ if (pathname?.startsWith('/task/')) setActiveTask(detailedTask);
if (!taskId.current) return;
updateLocalTimerStatus({
lastTaskId: taskId.current,
@@ -289,17 +292,22 @@ export function useTimer() {
return promise;
}, [
+ pathname,
+ setActiveTask,
+ detailedTask,
+ taskId,
+ updateLocalTimerStatus,
+ setTimerStatusFetching,
+ startTimerQueryCall,
activeTeamTaskRef,
timerStatus,
- updateOrganizationTeamEmployeeActiveTask,
- user,
- activeTeam,
setTimerStatus,
- setTimerStatusFetching,
- startTimerQueryCall,
- taskId,
- updateLocalTimerStatus,
- updateTask
+ updateTask,
+ activeTeam?.members,
+ activeTeam?.id,
+ activeTeam?.tenantId,
+ user?.employee.id,
+ updateOrganizationTeamEmployeeActiveTask
]);
// Stop timer
diff --git a/apps/web/app/hooks/features/useUserProfilePage.ts b/apps/web/app/hooks/features/useUserProfilePage.ts
index 9a67841bd..037059780 100644
--- a/apps/web/app/hooks/features/useUserProfilePage.ts
+++ b/apps/web/app/hooks/features/useUserProfilePage.ts
@@ -8,19 +8,21 @@ import { useAuthTeamTasks } from './useAuthTeamTasks';
import { useOrganizationTeams } from './useOrganizationTeams';
import { useTaskStatistics } from './useTaskStatistics';
import { useTeamTasks } from './useTeamTasks';
+import { useRecoilValue } from 'recoil';
+import { userDetailAccordion } from '@app/stores';
export function useUserProfilePage() {
const { activeTeam } = useOrganizationTeams();
const { activeTeamTask, updateTask } = useTeamTasks();
+ const userMemberId = useRecoilValue(userDetailAccordion);
const { user: auth } = useAuthenticateUser();
const { getTasksStatsData } = useTaskStatistics();
-
const params = useParams();
const memberId: string = useMemo(() => {
- return (params?.memberId ?? '') as string;
+ return (params?.memberId ?? userMemberId) as string;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [params]);
+ }, [params, userMemberId]);
const members = activeTeam?.members || [];
diff --git a/apps/web/app/hooks/index.ts b/apps/web/app/hooks/index.ts
index 1ca8c9f67..8e1f27f7b 100644
--- a/apps/web/app/hooks/index.ts
+++ b/apps/web/app/hooks/index.ts
@@ -59,6 +59,9 @@ export * from './features/useTaskSizes';
export * from './features/useTaskStatus';
export * from './features/useTaskVersion';
+// Daily plan
+export * from './features/useDailyPlan';
+
export * from './features/useRefetchData';
export * from './features/useRolePermissions';
diff --git a/apps/web/app/hooks/useHotkeys.ts b/apps/web/app/hooks/useHotkeys.ts
index 92eea73e4..87089850f 100644
--- a/apps/web/app/hooks/useHotkeys.ts
+++ b/apps/web/app/hooks/useHotkeys.ts
@@ -25,10 +25,10 @@ export const HostKeys = {
export const HostKeysMapping = [
{
- heading: 'Help',
+ heading: 'HELP',
keySequence: [
{
- label: 'To Open Shortcut List',
+ label:'TO_OPEN_SHORTCUT_LIST',
sequence: {
MAC: ['H'],
OTHER: ['H']
@@ -37,17 +37,17 @@ export const HostKeysMapping = [
]
},
{
- heading: 'Timer',
+ heading: 'TIMER',
keySequence: [
{
- label: 'Start Timer',
+ label: 'START_TIMER',
sequence: {
MAC: ['Ctrl(⌃)', 'Opt(⌥)', ']'],
OTHER: ['Ctrl', 'Alt', ']']
}
},
{
- label: 'Stop Timer',
+ label: 'STOP_TIMER',
sequence: {
MAC: ['Ctrl(⌃)', 'Opt(⌥)', '['],
OTHER: ['Ctrl', 'Alt', '[']
@@ -56,17 +56,17 @@ export const HostKeysMapping = [
]
},
{
- heading: 'Task',
+ heading: 'TASK',
keySequence: [
{
- label: 'Assign Task',
+ label: 'ASSIGN_TASK',
sequence: {
MAC: ['A'],
OTHER: ['A']
}
},
{
- label: 'Create Task',
+ label: 'CREATE_TASK',
sequence: {
MAC: ['C'],
OTHER: ['C']
diff --git a/apps/web/app/hooks/useInfinityFetch.ts b/apps/web/app/hooks/useInfinityFetch.ts
index 99bd60f7b..506115430 100644
--- a/apps/web/app/hooks/useInfinityFetch.ts
+++ b/apps/web/app/hooks/useInfinityFetch.ts
@@ -15,17 +15,21 @@ export const useInfinityScrolling = (arr: T[], lim?: number) => {
const getSomeTasks = React.useCallback(
(offset: number) => {
- setData(() => {
- const newData = getPartData({ arr, limit, offset });
- return newData;
- });
+ if (arr.length > 0) {
+ setData(() => {
+ const newData = getPartData({ arr, limit, offset });
+ return newData;
+ });
+ }
},
[arr, limit]
);
const nextOffset = React.useCallback(() => {
- setOffset((prev) => prev + 1);
- }, []);
+ if (arr.length > 0) {
+ setOffset((prev) => prev + 1);
+ }
+ }, [arr.length]);
React.useEffect(() => {
getSomeTasks(offset);
diff --git a/apps/web/app/hooks/useLanguage.ts b/apps/web/app/hooks/useLanguage.ts
index 78277befd..691c9efa5 100644
--- a/apps/web/app/hooks/useLanguage.ts
+++ b/apps/web/app/hooks/useLanguage.ts
@@ -4,11 +4,9 @@
import { currentLanguageState } from '@app/stores';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
import { useRecoilState } from 'recoil';
export function useLanguage() {
- const { i18n } = useTranslation();
const router = useRouter();
const [currentLanguage, setCurrentLanguage] = useRecoilState(currentLanguageState);
@@ -30,7 +28,7 @@ export function useLanguage() {
}
// router.refresh();
},
- [router, i18n]
+ [router]
);
- return { currentLanguage, changeLanguage, i18n };
+ return { currentLanguage, changeLanguage,};
}
diff --git a/apps/web/app/hooks/useLeftSettingData.ts b/apps/web/app/hooks/useLeftSettingData.ts
index 7967576bf..60172af88 100644
--- a/apps/web/app/hooks/useLeftSettingData.ts
+++ b/apps/web/app/hooks/useLeftSettingData.ts
@@ -80,12 +80,12 @@ export const useLeftSettingData = () => {
href: '#labels',
managerOnly: false
},
- {
- title: t('pages.settingsTeam.RELATED_ISSUE_TYPE'),
- color: '#7E7991',
- href: '#related-issue-types',
- managerOnly: true
- },
+ // {
+ // title: t('pages.settingsTeam.RELATED_ISSUE_TYPE'),
+ // color: '#7E7991',
+ // href: '#related-issue-types',
+ // managerOnly: true
+ // },
// {
// title: t('pages.settingsTeam.NOTIFICATION_HEADING'),
// color: '#7E7991',
diff --git a/apps/web/app/interfaces/IBaseModel.ts b/apps/web/app/interfaces/IBaseModel.ts
new file mode 100644
index 000000000..5adf49218
--- /dev/null
+++ b/apps/web/app/interfaces/IBaseModel.ts
@@ -0,0 +1,34 @@
+import { IOrganization } from './IOrganization';
+import { ITenant } from './ITenant';
+
+export interface IBaseDelete {
+ deletedAt?: Date;
+}
+
+export interface IBaseEntity extends IBaseDelete {
+ id?: string;
+ readonly createdAt?: Date;
+ readonly updatedAt?: Date;
+ isActive?: boolean;
+ isArchived?: boolean;
+}
+
+export interface IBasePerTenant extends IBaseEntity {
+ tenantId?: ITenant['id'];
+ tenant?: ITenant;
+}
+
+export interface IBasePerTenantEntityMutationInput extends Pick, IBaseEntity {
+ tenant?: Pick;
+}
+
+export interface IBasePerTenantAndOrganizationEntity extends IBasePerTenant {
+ organizationId?: IOrganization['id'];
+ organization?: IOrganization;
+}
+
+export interface IBasePerTenantAndOrganizationEntityMutationInput
+ extends Pick,
+ Partial {
+ organization?: Pick;
+}
diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts
new file mode 100644
index 000000000..e5003ef02
--- /dev/null
+++ b/apps/web/app/interfaces/IDailyPlan.ts
@@ -0,0 +1,31 @@
+import { IBasePerTenantAndOrganizationEntity } from './IBaseModel';
+import { IRelationnalEmployee } from './IEmployee';
+import { ITeamTask } from './ITask';
+
+export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity {
+ date: Date;
+ workTimePlanned: number;
+ status: DailyPlanStatusEnum;
+}
+
+export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee {
+ tasks?: ITeamTask[];
+}
+
+export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee {
+ taskId?: ITeamTask['id'];
+}
+
+export interface IUpdateDailyPlan extends Partial, Pick {}
+
+export interface IDailyPlanTasksUpdate
+ extends Pick,
+ IBasePerTenantAndOrganizationEntity {}
+
+export enum DailyPlanStatusEnum {
+ OPEN = 'open',
+ IN_PROGRESS = 'in-progress',
+ COMPLETED = 'completed'
+}
+
+export type IDailyPlanMode = 'today' | 'tomorow' | 'custom';
diff --git a/apps/web/app/interfaces/IEmployee.ts b/apps/web/app/interfaces/IEmployee.ts
index dea78b974..3844c9ff9 100644
--- a/apps/web/app/interfaces/IEmployee.ts
+++ b/apps/web/app/interfaces/IEmployee.ts
@@ -11,6 +11,7 @@ export interface IEmployee {
short_description: any;
description: any;
startedWorkOn: any;
+ isTrackingTime?: boolean;
endWork: any;
payPeriod: string;
billRateValue: number;
@@ -82,3 +83,8 @@ export type IWorkingEmployee = Pick<
| 'user'
| 'fullName'
>;
+
+export interface IRelationnalEmployee {
+ readonly employee?: IEmployee;
+ readonly employeeId?: IEmployee['id'];
+}
diff --git a/apps/web/app/interfaces/IIssueTypes.ts b/apps/web/app/interfaces/IIssueTypes.ts
index dfa080056..9dd11e873 100644
--- a/apps/web/app/interfaces/IIssueTypes.ts
+++ b/apps/web/app/interfaces/IIssueTypes.ts
@@ -1,5 +1,7 @@
+import { ITaskIssue } from './ITask';
+
export interface IIssueTypesCreate {
- name: string;
+ name: ITaskIssue;
description?: string;
icon?: string;
color?: string;
@@ -7,6 +9,7 @@ export interface IIssueTypesCreate {
organizationId?: string;
tenantId?: string | undefined | null;
organizationTeamId?: string | undefined | null;
+ isDefault?: boolean;
}
export interface IIssueTypesItemList extends IIssueTypesCreate {
@@ -19,4 +22,5 @@ export interface IIssueTypesItemList extends IIssueTypesCreate {
fullIconUrl?: string;
is_system?: boolean;
isSystem?: boolean;
+ isDefault: boolean;
}
diff --git a/apps/web/app/interfaces/IOrganizationTeam.ts b/apps/web/app/interfaces/IOrganizationTeam.ts
index b609dd051..cb392b8c1 100644
--- a/apps/web/app/interfaces/IOrganizationTeam.ts
+++ b/apps/web/app/interfaces/IOrganizationTeam.ts
@@ -15,6 +15,7 @@ export interface IOrganizationTeamCreate {
tags?: any[];
organizationId: string;
tenantId: string;
+ shareProfileView?: boolean;
public?: boolean;
imageId?: string | null;
image?: IImageAssets | null;
@@ -42,6 +43,7 @@ export interface IOrganizationTeam {
id: string;
createdAt: string;
updatedAt: string;
+ shareProfileView?: boolean;
imageId?: string | null;
image?: IImageAssets | null;
}
@@ -59,6 +61,7 @@ export interface IOrganizationTeamList {
updated?: boolean;
prefix: string;
members: OT_Member[];
+ shareProfileView?: boolean;
public?: boolean;
createdById: string;
createdBy: IUser;
diff --git a/apps/web/app/interfaces/ITaskStatus.ts b/apps/web/app/interfaces/ITaskStatus.ts
index b2a8e9350..3769e6163 100644
--- a/apps/web/app/interfaces/ITaskStatus.ts
+++ b/apps/web/app/interfaces/ITaskStatus.ts
@@ -21,6 +21,7 @@ export interface ITaskStatusCreate {
name?: string;
description?: string;
icon?: string;
+ value?: string;
color?: string;
projectId?: string;
organizationId?: string;
diff --git a/apps/web/app/interfaces/index.ts b/apps/web/app/interfaces/index.ts
index f6ba725be..187fbd13a 100644
--- a/apps/web/app/interfaces/index.ts
+++ b/apps/web/app/interfaces/index.ts
@@ -16,6 +16,7 @@ export * from './ITaskSizes';
export * from './ITaskTimesheet';
export * from './ITaskLabels';
export * from './ITaskRelatedIssueType';
+export * from './IDailyPlan';
export * from './IColor';
export * from './hooks';
export * from './IIcon';
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index d48047682..00063f410 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -9,5 +9,9 @@ type Props = {
// Since we have a `not-found.tsx` page on the root, a layout file
// is required, even if it's just passing children through.
export default function RootLayout({ children }: Props) {
- return children;
+ return (
+
+ {children}
+
+ );
}
diff --git a/apps/web/app/not-found.tsx b/apps/web/app/not-found.tsx
new file mode 100644
index 000000000..b95b93927
--- /dev/null
+++ b/apps/web/app/not-found.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+import NotFound from '@components/pages/404';
+
+const NotFoundPage = () => {
+ return ;
+};
+
+export default NotFoundPage;
diff --git a/apps/web/app/services/client/api/auth.ts b/apps/web/app/services/client/api/auth.ts
index 22d6d6a44..fe53873f5 100644
--- a/apps/web/app/services/client/api/auth.ts
+++ b/apps/web/app/services/client/api/auth.ts
@@ -13,17 +13,24 @@ import {
} from '@app/constants';
import qs from 'qs';
import { signInEmailConfirmGauzy, signInWorkspaceGauzy } from './auth/invite-accept';
+import { ProviderEnum } from '@app/services/server/requests/OAuth';
+/**
+ * Fetches data of the authenticated user with specified relations and the option to include employee details.
+ *
+ * @returns A Promise resolving to the IUser object.
+ */
export const getAuthenticatedUserDataAPI = () => {
- const params = {} as { [x: string]: string };
- const relations = ['employee', 'role', 'tenant'];
+ // Define the relations to be included in the request
+ const relations = ['role', 'tenant'];
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
+ // Construct the query string with 'qs', including the includeEmployee parameter
+ const query = qs.stringify({
+ relations: relations,
+ includeEmployee: true // Append includeEmployee parameter set to true
});
- const query = qs.stringify(params);
-
+ // Execute the GET request to fetch the user data
return get(`/user/me?${query}`);
};
@@ -100,11 +107,14 @@ export const signInEmailAPI = (email: string) => {
};
export function signInEmailPasswordAPI(email: string, password: string) {
- const endpoint = GAUZY_API_BASE_SERVER_URL.value
- ? '/auth/signin.email.password?includeTeams=true'
- : `/auth/signin-email-password`;
+ const endpoint = GAUZY_API_BASE_SERVER_URL.value ? '/auth/signin.email.password' : `/auth/signin-email-password`;
+ return post(endpoint, { email, password, includeTeams: true });
+}
+
+export function signInEmailSocialLoginAPI(provider: ProviderEnum, access_token: string) {
+ const endpoint = GAUZY_API_BASE_SERVER_URL.value ? '/auth/signin.provider.social' : `/auth/signin-email-social`;
- return post(endpoint, { email, password });
+ return post(endpoint, { provider, access_token, includeTeams: true });
}
export const verifyUserEmailByTokenAPI = (email: string, token: string) => {
@@ -124,15 +134,20 @@ export async function signInEmailConfirmAPI(email: string, code: string) {
});
}
-export const signInWorkspaceAPI = (email: string, token: string, selectedTeam: string) => {
+export const signInWorkspaceAPI = (params: { email: string; token: string; selectedTeam: string; code?: string }) => {
if (GAUZY_API_BASE_SERVER_URL.value) {
- return signInWorkspaceGauzy({ email, token, teamId: selectedTeam, code: 'sign-in-workspace' });
+ return signInWorkspaceGauzy({
+ email: params.email,
+ token: params.token,
+ teamId: params.selectedTeam,
+ code: params.code
+ });
}
return api.post(`/auth/signin-workspace`, {
- email,
- token,
- teamId: selectedTeam
+ email: params.email,
+ token: params.token,
+ teamId: params.selectedTeam
});
};
diff --git a/apps/web/app/services/client/api/auth/invite-accept.ts b/apps/web/app/services/client/api/auth/invite-accept.ts
index 05f1aab58..e97106baa 100644
--- a/apps/web/app/services/client/api/auth/invite-accept.ts
+++ b/apps/web/app/services/client/api/auth/invite-accept.ts
@@ -25,16 +25,28 @@ export function verifyInviteCodeAPI(params: IInviteVerifyCode) {
return post('/invite/validate-by-code', params).then((res) => res.data);
}
+/**
+ * Constructs a request to fetch user organizations with tenant and user ID.
+ *
+ * @param params - Parameters including tenantId, userId, and token for authentication.
+ * @returns A promise that resolves to a pagination response of user organizations.
+ */
export function getUserOrganizationsRequest(params: { tenantId: string; userId: string; token: string }) {
- const query = JSON.stringify({
- relations: [],
- findInput: {
- userId: params.userId,
- tenantId: params.tenantId
- }
+ // Create a new instance of URLSearchParams for query string construction
+ const query = new URLSearchParams();
+
+ // Add user and tenant IDs to the query
+ query.append('where[userId]', params.userId);
+ query.append('where[tenantId]', params.tenantId);
+
+ // If there are relations, add them to the query
+ const relations: string[] = [];
+ // Append each relation to the query string
+ relations.forEach((relation, index) => {
+ query.append(`relations[${index}]`, relation);
});
- return get>(`/user-organization?data=${encodeURIComponent(query)}`, {
+ return get>(`/user-organization?${query.toString()}`, {
tenantId: params.tenantId,
headers: {
Authorization: `Bearer ${params.token}`
@@ -42,6 +54,13 @@ export function getUserOrganizationsRequest(params: { tenantId: string; userId:
});
}
+/**
+ * Fetches a list of all teams within an organization, including specified relation data.
+ *
+ * @param {ITeamRequestParams} params Parameters for the team request, including organization and tenant IDs, and optional relations.
+ * @param {string} bearer_token The bearer token for authentication.
+ * @returns A Promise resolving to the pagination response of organization teams.
+ */
export function getAllOrganizationTeamAPI(params: ITeamRequestParams, bearer_token: string) {
const relations = params.relations || [
'members',
@@ -49,25 +68,24 @@ export function getAllOrganizationTeamAPI(params: ITeamRequestParams, bearer_tok
'members.employee',
'members.employee.user',
'createdBy',
- 'createdBy.employee',
'projects',
'projects.repository'
];
- const searchQueries = {
+ // Construct search queries
+ const queryParams = {
'where[organizationId]': params.organizationId,
'where[tenantId]': params.tenantId,
source: TimerSource.TEAMS,
- withLaskWorkedTask: 'true'
- } as { [x: string]: string };
-
- relations.forEach((rl, i) => {
- searchQueries[`relations[${i}]`] = rl;
- });
+ withLastWorkedTask: 'true', // Corrected the typo here
+ ...Object.fromEntries(relations.map((relation, index) => [`relations[${index}]`, relation]))
+ };
- const query = qs.stringify(params);
+ // Serialize search queries into a query string
+ const queryString = qs.stringify(queryParams, { arrayFormat: 'brackets' });
- return get>(`/organization-team?${query}`, {
+ // Construct and execute the request
+ return get>(`/organization-team?${queryString}`, {
tenantId: params.tenantId,
headers: {
Authorization: `Bearer ${bearer_token}`
@@ -77,7 +95,7 @@ export function getAllOrganizationTeamAPI(params: ITeamRequestParams, bearer_tok
export const signInEmailConfirmAPI = (data: { code: string; email: string }) => {
const { code, email } = data;
- return post('/auth/signin.email/confirm?includeTeams=true', { code, email });
+ return post('/auth/signin.email/confirm', { code, email, includeTeams: true });
};
export async function signInEmailCodeConfirmGauzy(email: string, code: string) {
@@ -199,11 +217,13 @@ export async function signInEmailConfirmGauzy(email: string, code: string) {
/**
* @param params
*/
-export async function signInWorkspaceGauzy(params: { email: string; token: string; teamId: string; code: string }) {
- const loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code);
+export async function signInWorkspaceGauzy(params: { email: string; token: string; teamId: string; code?: string }) {
+ if (params.code) {
+ const loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code);
- if (loginResponse) {
- return loginResponse;
+ if (loginResponse) {
+ return loginResponse;
+ }
}
const data = await signInWorkspaceAPI(params.email, params.token);
diff --git a/apps/web/app/services/client/api/daily-plan.ts b/apps/web/app/services/client/api/daily-plan.ts
new file mode 100644
index 000000000..463a2d936
--- /dev/null
+++ b/apps/web/app/services/client/api/daily-plan.ts
@@ -0,0 +1,91 @@
+import qs from 'qs';
+import { deleteApi, get, post, put } from '../axios';
+import {
+ DeleteResponse,
+ ICreateDailyPlan,
+ IDailyPlan,
+ IDailyPlanTasksUpdate,
+ IUpdateDailyPlan,
+ PaginationResponse
+} from '@app/interfaces';
+import { getOrganizationIdCookie, getTenantIdCookie } from '@app/helpers';
+
+export function getAllDayPlansAPI() {
+ const organizationId = getOrganizationIdCookie();
+ const tenantId = getTenantIdCookie();
+
+ const relations = ['employee', 'tasks'];
+
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ relations.forEach((relation, i) => {
+ obj[`relations[${i}]`] = relation;
+ });
+
+ const query = qs.stringify(obj);
+ return get>(`/daily-plan?${query}`, { tenantId });
+}
+
+export function getDayPlansByEmployeeAPI(employeeId?: string) {
+ const organizationId = getOrganizationIdCookie();
+ const tenantId = getTenantIdCookie();
+
+ const relations = ['employee', 'tasks'];
+
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ relations.forEach((relation, i) => {
+ obj[`relations[${i}]`] = relation;
+ });
+
+ const query = qs.stringify(obj);
+ return get>(`/daily-plan/employee/${employeeId}?${query}`, { tenantId });
+}
+
+export function getPlansByTaskAPI(taskId?: string) {
+ const organizationId = getOrganizationIdCookie();
+ const tenantId = getTenantIdCookie();
+
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ const query = qs.stringify(obj);
+
+ return get>(`/daily-plan/task/${taskId}?${query}`, { tenantId });
+}
+
+export function createDailyPlanAPI(data: ICreateDailyPlan, tenantId?: string) {
+ return post('/daily-plan', data, {
+ tenantId
+ });
+}
+
+export function updateDailyPlanAPI(data: IUpdateDailyPlan, planId: string) {
+ return put(`/daily-plan/${planId}`, data, {});
+}
+
+export function addTaskToPlanAPI(data: IDailyPlanTasksUpdate, planId: string) {
+ const organizationId = getOrganizationIdCookie();
+ const tenantId = getTenantIdCookie();
+
+ return post(`/daily-plan/${planId}/task`, { ...data, organizationId }, { tenantId });
+}
+
+export function removeTaskFromPlanAPI(data: IDailyPlanTasksUpdate, planId: string) {
+ const organizationId = getOrganizationIdCookie();
+ const tenantId = getTenantIdCookie();
+
+ return put(`/daily-plan/${planId}/task`, { ...data, organizationId }, { tenantId });
+}
+
+export function deleteDailyPlanAPI(planId: string) {
+ return deleteApi(`/daily-plan/${planId}`);
+}
diff --git a/apps/web/app/services/client/api/index.ts b/apps/web/app/services/client/api/index.ts
index 7b8f36e10..a3fc188f9 100644
--- a/apps/web/app/services/client/api/index.ts
+++ b/apps/web/app/services/client/api/index.ts
@@ -14,6 +14,7 @@ export * from './task-sizes';
export * from './task-labels';
export * from './issue-type';
export * from './task-related-issue-type';
+export * from './daily-plan';
export * from './user';
export * from './request-to-join-team';
diff --git a/apps/web/app/services/client/api/invite.ts b/apps/web/app/services/client/api/invite.ts
index e23da8111..c8c377cbd 100644
--- a/apps/web/app/services/client/api/invite.ts
+++ b/apps/web/app/services/client/api/invite.ts
@@ -15,6 +15,10 @@ interface IIInviteRequest {
export async function inviteByEmailsAPI(data: IIInviteRequest, tenantId: string) {
const endpoint = '/invite/emails';
+ if (!GAUZY_API_BASE_SERVER_URL.value) {
+ return post>(endpoint, data, { tenantId });
+ }
+
const date = new Date();
date.setDate(date.getDate() - 1);
diff --git a/apps/web/app/services/client/api/organization-team.ts b/apps/web/app/services/client/api/organization-team.ts
index 2769cab13..75e737fd9 100644
--- a/apps/web/app/services/client/api/organization-team.ts
+++ b/apps/web/app/services/client/api/organization-team.ts
@@ -16,6 +16,13 @@ import { getAccessTokenCookie, getOrganizationIdCookie, getTenantIdCookie } from
import { createOrganizationProjectAPI } from './projects';
import qs from 'qs';
+/**
+ * Fetches a list of teams for a specified organization.
+ *
+ * @param {string} organizationId The unique identifier for the organization.
+ * @param {string} tenantId The tenant identifier.
+ * @returns A Promise resolving to a paginated response containing the list of organization teams.
+ */
export async function getOrganizationTeamsAPI(organizationId: string, tenantId: string) {
const relations = [
'members',
@@ -23,22 +30,21 @@ export async function getOrganizationTeamsAPI(organizationId: string, tenantId:
'members.employee',
'members.employee.user',
'createdBy',
- 'createdBy.employee',
'projects',
'projects.repository'
];
-
- const params = {
+ // Construct the query parameters including relations
+ const queryParameters = {
'where[organizationId]': organizationId,
'where[tenantId]': tenantId,
source: TimerSource.TEAMS,
- withLaskWorkedTask: 'true'
- } as { [x: string]: string };
+ withLastWorkedTask: 'true', // Corrected the typo here
+ relations
+ };
+
+ // Serialize parameters into a query string
+ const query = qs.stringify(queryParameters, { arrayFormat: 'brackets' });
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
- });
- const query = qs.stringify(params);
const endpoint = `/organization-team?${query}`;
return get>(endpoint, { tenantId });
@@ -84,36 +90,43 @@ export async function createOrganizationTeamAPI(name: string, user: IUser) {
return api.post>('/organization-team', { name });
}
+/**
+ * Fetches details of a specific team within an organization.
+ *
+ * @param {string} teamId The unique identifier of the team.
+ * @param {string} organizationId The unique identifier of the organization.
+ * @param {string} tenantId The tenant identifier.
+ * @returns A Promise resolving to the details of the specified organization team.
+ */
export async function getOrganizationTeamAPI(teamId: string, organizationId: string, tenantId: string) {
- const params = {
- organizationId: organizationId,
- tenantId: tenantId,
- // source: TimerSource.TEAMS,
- withLaskWorkedTask: 'true',
- startDate: moment().startOf('day').toISOString(),
- endDate: moment().endOf('day').toISOString(),
- includeOrganizationTeamId: 'false'
- } as { [x: string]: string };
-
const relations = [
'members',
'members.role',
'members.employee',
'members.employee.user',
'createdBy',
- 'createdBy.employee',
'projects',
'projects.repository'
];
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
- });
+ // Define base parameters including organization and tenant IDs, and date range
+ const queryParams = {
+ organizationId,
+ tenantId,
+ withLastWorkedTask: 'true', // Corrected the typo here
+ startDate: moment().startOf('day').toISOString(),
+ endDate: moment().endOf('day').toISOString(),
+ includeOrganizationTeamId: 'false',
+ relations
+ };
- const queries = qs.stringify(params);
+ // Serialize parameters into a query string using 'qs'
+ const queryString = qs.stringify(queryParams, { arrayFormat: 'brackets' });
- const endpoint = `/organization-team/${teamId}?${queries}`;
+ // Construct the endpoint URL
+ const endpoint = `/organization-team/${teamId}?${queryString}`;
+ // Fetch and return the team details
return get(endpoint);
}
diff --git a/apps/web/app/services/client/api/public-organization-team.ts b/apps/web/app/services/client/api/public-organization-team.ts
index 9c9707ac8..59026e546 100644
--- a/apps/web/app/services/client/api/public-organization-team.ts
+++ b/apps/web/app/services/client/api/public-organization-team.ts
@@ -11,13 +11,12 @@ export function getPublicOrganizationTeamsAPI(profile_link: string, team_id: str
'tasks.teams',
'tasks.tags',
'members',
- // 'members.role',
'members.employee',
'members.employee.user'
];
const params = {
- withLaskWorkedTask: 'true',
+ withLastWorkedTask: 'true',
startDate: moment().startOf('day').toISOString(),
endDate: moment().endOf('day').toISOString()
} as { [x: string]: string };
@@ -39,7 +38,7 @@ export function getPublicOrganizationTeamsMiscDataAPI(profile_link: string, team
const relations = ['statuses', 'priorities', 'sizes', 'labels', 'issueTypes'];
const params = {
- withLaskWorkedTask: 'true',
+ withLastWorkedTask: 'true',
startDate: moment().startOf('day').toISOString(),
endDate: moment().endOf('day').toISOString()
} as { [x: string]: string };
diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts
index a940e724f..5b4af5a0e 100644
--- a/apps/web/app/services/client/axios.ts
+++ b/apps/web/app/services/client/axios.ts
@@ -1,11 +1,6 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import { API_BASE_URL, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL } from '@app/constants';
-import {
- getAccessTokenCookie,
- getActiveTeamIdCookie,
- getOrganizationIdCookie,
- getTenantIdCookie
-} from '@app/helpers/cookies';
+import { getAccessTokenCookie, getOrganizationIdCookie, getTenantIdCookie } from '@app/helpers/cookies';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
const api = axios.create({
@@ -13,9 +8,10 @@ const api = axios.create({
withCredentials: true,
timeout: 60 * 1000
});
+
api.interceptors.request.use(
async (config: any) => {
- const cookie = getActiveTeamIdCookie();
+ const cookie = getAccessTokenCookie();
if (cookie) {
config.headers['Authorization'] = `Bearer ${cookie}`;
@@ -27,6 +23,7 @@ api.interceptors.request.use(
Promise.reject(error);
}
);
+
api.interceptors.response.use(
(response: AxiosResponse) => response,
async (error: { response: AxiosResponse }) => {
diff --git a/apps/web/app/services/server/requests/OAuth.ts b/apps/web/app/services/server/requests/OAuth.ts
new file mode 100644
index 000000000..d2743e2d0
--- /dev/null
+++ b/apps/web/app/services/server/requests/OAuth.ts
@@ -0,0 +1,83 @@
+import { type Adapter } from '@auth/core/adapters';
+import { signWithSocialLoginsRequest } from '@app/services/server/requests';
+import { getUserOrganizationsRequest, signInWorkspaceAPI } from '@app/services/client/api/auth/invite-accept';
+
+export enum ProviderEnum {
+ GITHUB = 'github',
+ GOOGLE = 'google',
+ FACEBOOK = 'facebook',
+ TWITTER = 'twitter'
+}
+
+export const GauzyAdapter: Adapter = {
+ // Provide createUser and other related functions that will call Gauzy APIs when implementing social signup
+};
+
+async function signIn(provider: ProviderEnum, access_token: string) {
+ try {
+ const gauzyUser = await signWithSocialLoginsRequest(provider, access_token);
+
+ if (!gauzyUser) {
+ return Promise.reject({
+ errors: {
+ email: 'Your account is not yet ready to be used on the Ever Teams Platform'
+ }
+ });
+ }
+
+ const data = await signInWorkspaceAPI(gauzyUser?.data.confirmed_email, gauzyUser?.data.workspaces[0].token);
+ const tenantId = data.user?.tenantId || '';
+ const token = data.token;
+ const userId = data.user?.id;
+
+ const { data: organizations } = await getUserOrganizationsRequest({
+ tenantId,
+ userId,
+ token
+ });
+
+ const organization = organizations?.items[0];
+
+ if (!organization) {
+ return Promise.reject({
+ errors: {
+ email: 'Your account is not yet ready to be used on the Ever Teams Platform'
+ }
+ });
+ }
+ return { data, gauzyUser, organization, tenantId, userId };
+ } catch (error) {
+ throw new Error('Signin error', { cause: error });
+ }
+}
+
+export async function signInCallback(provider: ProviderEnum, access_token: string): Promise {
+ try {
+ const { gauzyUser, organization } = await signIn(provider, access_token);
+ return !!gauzyUser && !!organization;
+ } catch (error) {
+ return false;
+ }
+}
+
+export async function jwtCallback(provider: ProviderEnum, access_token: string) {
+ try {
+ const { data, gauzyUser, organization, tenantId, userId } = await signIn(provider, access_token);
+ return {
+ access_token,
+ refresh_token: {
+ token: data.refresh_token
+ },
+ teamId: gauzyUser?.data.workspaces[0].current_teams[0].team_id,
+ tenantId,
+ organizationId: organization?.organizationId,
+ languageId: 'en',
+ noTeamPopup: true,
+ userId,
+ workspaces: gauzyUser?.data.workspaces,
+ confirmed_mail: gauzyUser?.data.confirmed_email
+ };
+ } catch (error) {
+ throw new Error('Signin error', { cause: error });
+ }
+}
diff --git a/apps/web/app/services/server/requests/auth.ts b/apps/web/app/services/server/requests/auth.ts
index de2abfa06..7364730ed 100644
--- a/apps/web/app/services/server/requests/auth.ts
+++ b/apps/web/app/services/server/requests/auth.ts
@@ -4,6 +4,7 @@ import { ILoginResponse, IRegisterDataRequest, ISigninEmailConfirmResponse } fro
import { IUser } from '@app/interfaces/IUserData';
import { serverFetch } from '../fetch';
import qs from 'qs';
+import { ProviderEnum } from './OAuth';
const registerDefaultValue = {
appName: APP_NAME,
@@ -43,9 +44,17 @@ export function signInEmailRequest(email: string, callbackUrl: string) {
export function signInEmailPasswordRequest(email: string, password: string) {
return serverFetch({
- path: '/auth/signin.email.password?includeTeams=true',
+ path: '/auth/signin.email.password',
method: 'POST',
- body: { email, password }
+ body: { email, password, includeTeams: true }
+ });
+}
+
+export function signWithSocialLoginsRequest(provider: ProviderEnum, token: string) {
+ return serverFetch({
+ path: '/auth/signin.email.social',
+ method: 'POST',
+ body: { provider, token, includeTeams: true }
});
}
@@ -53,9 +62,9 @@ export const signInEmailConfirmRequest = (data: { code: string; email: string })
const { code, email } = data;
return serverFetch({
- path: '/auth/signin.email/confirm?includeTeams=true',
+ path: '/auth/signin.email/confirm',
method: 'POST',
- body: { code, email }
+ body: { code, email, includeTeams: true }
});
};
@@ -100,18 +109,20 @@ type IUEmployeeParam = {
relations?: string[];
};
-export const currentAuthenticatedUserRequest = ({
- bearer_token,
- relations = ['employee', 'role', 'tenant']
-}: IUEmployeeParam) => {
- const params = {} as { [x: string]: string };
-
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
+/**
+ * Fetches details of the currently authenticated user, including specified relations.
+ *
+ * @param {IUEmployeeParam} employeeParams - The employee parameters, including bearer token and optional relations.
+ * @returns A Promise resolving to the IUser object with the desired relations.
+ */
+export const currentAuthenticatedUserRequest = ({ bearer_token, relations = ['role', 'tenant'] }: IUEmployeeParam) => {
+ // Construct the query string with 'qs', including the includeEmployee parameter
+ const query = qs.stringify({
+ relations: relations,
+ includeEmployee: true // Append includeEmployee parameter set to true
});
- const query = qs.stringify(params);
-
+ // Construct and return the server fetch request
return serverFetch({
path: `/user/me?${query}`,
method: 'GET',
diff --git a/apps/web/app/services/server/requests/daily-plan.ts b/apps/web/app/services/server/requests/daily-plan.ts
new file mode 100644
index 000000000..20c8ba85d
--- /dev/null
+++ b/apps/web/app/services/server/requests/daily-plan.ts
@@ -0,0 +1,175 @@
+import qs from 'qs';
+import { ICreateDailyPlan, IDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces/IDailyPlan';
+import { serverFetch } from '../fetch';
+import { DeleteResponse } from '@app/interfaces';
+
+export function getAllDayPlans({
+ organizationId,
+ tenantId,
+ bearer_token,
+ relations = ['employee', 'tasks']
+}: {
+ organizationId: string;
+ tenantId: string;
+ bearer_token: string;
+ relations?: string[];
+}) {
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ relations.forEach((relation, i) => {
+ obj[`relations[${i}]`] = relation;
+ });
+
+ const query = qs.stringify(obj);
+
+ return serverFetch({
+ path: `/daily-plan?${query}`,
+ method: 'GET',
+ bearer_token
+ });
+}
+
+export function getDayPlansByEmployee({
+ employeeId,
+ organizationId,
+ tenantId,
+ bearer_token,
+ relations = ['employee', 'tasks']
+}: {
+ employeeId: string;
+ organizationId: string;
+ tenantId: string;
+ bearer_token: string;
+ relations?: string[];
+}) {
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ relations.forEach((relation, i) => {
+ obj[`relations[${i}]`] = relation;
+ });
+
+ const query = qs.stringify(obj);
+
+ return serverFetch({
+ path: `/daily-plan/employee/${employeeId}?${query}`,
+ method: 'GET',
+ bearer_token
+ });
+}
+
+export function getPlansByTask({
+ taskId,
+ organizationId,
+ tenantId,
+ bearer_token
+}: {
+ taskId: string;
+ organizationId: string;
+ tenantId: string;
+ bearer_token: string;
+}) {
+ const obj = {
+ 'where[organizationId]': organizationId,
+ 'where[tenantId]': tenantId
+ } as Record;
+
+ const query = qs.stringify(obj);
+
+ return serverFetch({
+ path: `/daily-plan/task/${taskId}?${query}`,
+ method: 'GET',
+ bearer_token
+ });
+}
+
+export function createPlanRequest({
+ data,
+ bearer_token,
+ tenantId
+}: {
+ data: ICreateDailyPlan;
+ bearer_token: string;
+ tenantId?: any;
+}) {
+ return serverFetch({
+ method: 'POST',
+ path: '/daily-plan',
+ body: data,
+ bearer_token,
+ tenantId
+ });
+}
+
+export function updatePlanRequest({
+ planId,
+ data,
+ bearer_token,
+ tenantId
+}: {
+ planId: string;
+ data: IUpdateDailyPlan;
+ bearer_token?: string;
+ tenantId?: any;
+}) {
+ return serverFetch({
+ method: 'PUT',
+ path: `/daily-plan/${planId}`,
+ body: data,
+ bearer_token,
+ tenantId
+ });
+}
+
+export function addTaskToDailyPlanRequest({
+ planId,
+ data,
+ bearer_token,
+ tenantId
+}: {
+ planId: string;
+ data: IDailyPlanTasksUpdate;
+ bearer_token?: string;
+ tenantId: any;
+}) {
+ return serverFetch({
+ method: 'POST',
+ path: `/daily-plan/${planId}/task`,
+ body: data,
+ bearer_token,
+ tenantId
+ });
+}
+
+export function removeTaskFromPlanRequest({
+ planId,
+ data,
+ bearer_token,
+ tenantId
+}: {
+ planId: string;
+ data: IDailyPlanTasksUpdate;
+ bearer_token?: string;
+ tenantId: any;
+}) {
+ return serverFetch({
+ method: 'PUT',
+ path: `/daily-plan/${planId}/task`,
+ body: data,
+ bearer_token,
+ tenantId
+ });
+}
+
+export function deleteDailyPlanRequest({ planId, bearer_token }: { planId: string; bearer_token?: string }) {
+ return serverFetch({
+ method: 'DELETE',
+ path: `/daily-plan/${planId}`,
+ bearer_token
+ });
+}
diff --git a/apps/web/app/services/server/requests/index.ts b/apps/web/app/services/server/requests/index.ts
index 3226b849d..8915f0553 100644
--- a/apps/web/app/services/server/requests/index.ts
+++ b/apps/web/app/services/server/requests/index.ts
@@ -5,6 +5,7 @@ export * from './organization-team-employee';
export * from './tenant';
export * from './timer';
export * from './tasks';
+export * from './daily-plan';
export * from './employee';
export * from './invite';
export * from './timesheet';
diff --git a/apps/web/app/services/server/requests/organization-team.ts b/apps/web/app/services/server/requests/organization-team.ts
index b4040e9ff..a62c2c6d9 100644
--- a/apps/web/app/services/server/requests/organization-team.ts
+++ b/apps/web/app/services/server/requests/organization-team.ts
@@ -73,6 +73,13 @@ export function deleteOrganizationTeamRequest({
});
}
+/**
+ * Fetches detailed information for a specific team within an organization.
+ *
+ * @param {ITeamRequestParams & { teamId: string }} params Contains team, organization, tenant IDs, and optional relations.
+ * @param {string} bearer_token Token for authenticating the request.
+ * @returns A Promise resolving to the detailed information of the organization team with additional status.
+ */
export function getOrganizationTeamRequest(
{
organizationId,
@@ -84,37 +91,43 @@ export function getOrganizationTeamRequest(
'members.employee',
'members.employee.user',
'createdBy',
- 'createdBy.employee',
'projects',
'projects.repository'
]
}: ITeamRequestParams & { teamId: string },
bearer_token: string
) {
- const params = {
- organizationId: organizationId,
- tenantId: tenantId,
+ // Define base query parameters
+ const queryParams = {
+ organizationId,
+ tenantId,
// source: TimerSource.TEAMS,
- withLaskWorkedTask: 'true',
+ withLastWorkedTask: 'true', // Corrected typo
startDate: moment().startOf('day').toISOString(),
endDate: moment().endOf('day').toISOString(),
- includeOrganizationTeamId: 'false'
- } as { [x: string]: string };
+ includeOrganizationTeamId: 'false',
+ ...Object.fromEntries(relations.map((relation, index) => [`relations[${index}]`, relation]))
+ };
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
- });
-
- const queries = qs.stringify(params);
+ // Serialize parameters into a query string
+ const queryString = qs.stringify(queryParams, { arrayFormat: 'brackets' });
+ // Fetch and return team details
return serverFetch({
- path: `/organization-team/${teamId}?${queries.toString()}`,
+ path: `/organization-team/${teamId}?${queryString}`,
method: 'GET',
bearer_token,
tenantId
});
}
+/**
+ * Fetches team details for a specified organization from the server.
+ *
+ * @param {TeamRequestParams} params Contains organizationId, tenantId, and optional relationship specifications.
+ * @param {string} bearer_token Token for request authentication.
+ * @returns A Promise resolving to a paginated list of organization team data.
+ */
export function getAllOrganizationTeamRequest(
{
organizationId,
@@ -125,26 +138,25 @@ export function getAllOrganizationTeamRequest(
'members.employee',
'members.employee.user',
'createdBy',
- 'createdBy.employee',
'projects',
'projects.repository'
]
}: ITeamRequestParams,
bearer_token: string
) {
+ // Consolidate all parameters into a single object
const params = {
'where[organizationId]': organizationId,
'where[tenantId]': tenantId,
source: TimerSource.TEAMS,
- withLaskWorkedTask: 'true'
- } as { [x: string]: string };
-
- relations.forEach((rl, i) => {
- params[`relations[${i}]`] = rl;
- });
+ withLastWorkedTask: 'true',
+ relations
+ };
- const query = qs.stringify(params);
+ // Serialize parameters into a query string
+ const query = qs.stringify(params, { arrayFormat: 'brackets' });
+ // Construct and return the server fetch request
return serverFetch>({
path: `/organization-team?${query}`,
method: 'GET',
diff --git a/apps/web/app/services/server/requests/organization.ts b/apps/web/app/services/server/requests/organization.ts
index 4ff22b50a..a30934f46 100644
--- a/apps/web/app/services/server/requests/organization.ts
+++ b/apps/web/app/services/server/requests/organization.ts
@@ -2,31 +2,48 @@ import { PaginationResponse } from '@app/interfaces/IDataResponse';
import { IOrganization, IOrganizationCreate, IUserOrganization } from '@app/interfaces/IOrganization';
import { serverFetch } from '../fetch';
-export function createOrganizationRequest(datas: IOrganizationCreate, bearer_token: string) {
+export function createOrganizationRequest(datas: IOrganizationCreate, bearerToken: string) {
return serverFetch({
path: '/organization',
method: 'POST',
body: datas,
- bearer_token
+ bearer_token: bearerToken
});
}
-export function getUserOrganizationsRequest(
- { tenantId, userId }: { tenantId: string; userId: string },
- bearer_token: string
-) {
- const query = JSON.stringify({
- relations: [],
- findInput: {
- userId,
- tenantId
- }
+/**
+ * Constructs a GET request to fetch user organizations based on tenant and user IDs.
+ *
+ * @param param0 - Contains the tenantId and userId.
+ * @param bearerToken - The bearer token for authorization.
+ * @returns A promise resolving to a pagination response of user organizations.
+ * @throws Error if required parameters are missing or invalid.
+ */
+export function getUserOrganizationsRequest({ tenantId, userId }: {
+ tenantId: string;
+ userId: string
+}, bearerToken: string) {
+ if (!tenantId || !userId || !bearerToken) {
+ throw new Error('Tenant ID, User ID, and Bearer token are required'); // Validate required parameters
+ }
+ // Create a new instance of URLSearchParams for query string construction
+ const query = new URLSearchParams();
+
+ // Add user and tenant IDs to the query
+ query.append('where[userId]', userId);
+ query.append('where[tenantId]', tenantId);
+
+ // If there are relations, add them to the query
+ const relations: string[] = [];
+ // Append each relation to the query string
+ relations.forEach((relation, index) => {
+ query.append(`relations[${index}]`, relation);
});
return serverFetch>({
- path: `/user-organization?data=${encodeURIComponent(query)}`,
- method: 'GET',
- bearer_token,
+ path: `/user-organization?${query.toString()}`, // Build query string
+ method: 'GET', // GET request
+ bearer_token: bearerToken, // Include bearer token in headers
tenantId
});
}
diff --git a/apps/web/app/services/server/requests/public-organization-team.ts b/apps/web/app/services/server/requests/public-organization-team.ts
index 54347c45d..30db4d934 100644
--- a/apps/web/app/services/server/requests/public-organization-team.ts
+++ b/apps/web/app/services/server/requests/public-organization-team.ts
@@ -22,7 +22,7 @@ export function getPublicOrganizationTeamRequest({
relations?: string[];
}) {
const params = {
- withLaskWorkedTask: 'true',
+ withLastWorkedTask: 'true',
startDate: moment().startOf('day').toISOString(),
endDate: moment().endOf('day').toISOString()
} as { [x: string]: string };
@@ -49,7 +49,7 @@ export function getPublicOrganizationTeamMiscDataRequest({
relations?: string[];
}) {
const params = {
- withLaskWorkedTask: 'true',
+ withLastWorkedTask: 'true',
startDate: moment().startOf('day').toISOString(),
endDate: moment().endOf('day').toISOString()
} as { [x: string]: string };
diff --git a/apps/web/app/stores/daily-plan.ts b/apps/web/app/stores/daily-plan.ts
new file mode 100644
index 000000000..16a399ed2
--- /dev/null
+++ b/apps/web/app/stores/daily-plan.ts
@@ -0,0 +1,36 @@
+import { atom, selector } from 'recoil';
+import { IDailyPlan, PaginationResponse } from '@app/interfaces';
+
+export const dailyPlanListState = atom>({
+ key: 'dailyPlanListState',
+ default: { items: [], total: 0 }
+});
+
+export const profileDailyPlanListState = atom>({
+ key: 'profileDailyPlanListState',
+ default: { items: [], total: 0 }
+});
+
+export const taskPlans = atom({
+ key: 'taskPlansList',
+ default: []
+});
+
+export const activeDailyPlanIdState = atom({
+ key: 'activeDailyPlanIdService',
+ default: null
+});
+
+export const dailyPlanFetchingState = atom({
+ key: 'dailyPlanFetchingState',
+ default: false
+});
+
+export const activeDailyPlanState = selector({
+ key: 'activeDailyPlanState',
+ get: ({ get }) => {
+ const dailyPlans = get(dailyPlanListState);
+ const activeId = get(activeDailyPlanIdState);
+ return dailyPlans.items.find((plan) => plan.id === activeId) || dailyPlans.items[0] || null;
+ }
+});
diff --git a/apps/web/app/stores/fullWidth.ts b/apps/web/app/stores/fullWidth.ts
index 85485c666..ace0453fd 100644
--- a/apps/web/app/stores/fullWidth.ts
+++ b/apps/web/app/stores/fullWidth.ts
@@ -1,6 +1,6 @@
import { atom } from 'recoil';
-export const fullWidthState = atom({
+export const fullWidthState = atom({
key: 'fullWidth',
default: true
});
diff --git a/apps/web/app/stores/index.ts b/apps/web/app/stores/index.ts
index 02fab4487..35f126481 100644
--- a/apps/web/app/stores/index.ts
+++ b/apps/web/app/stores/index.ts
@@ -16,6 +16,7 @@ export * from './task-sizes';
export * from './task-labels';
export * from './issue-type';
export * from './task-related-issue-type';
+export * from './daily-plan';
export * from './roles';
export * from './role-permissions';
diff --git a/apps/web/app/stores/setting.ts b/apps/web/app/stores/setting.ts
new file mode 100644
index 000000000..28b408b70
--- /dev/null
+++ b/apps/web/app/stores/setting.ts
@@ -0,0 +1,6 @@
+import { atom } from "recoil";
+
+export const activeSettingTeamTab = atom({
+ key: 'activeSettingTeamTab',
+ default: ''
+});
diff --git a/apps/web/app/stores/team-tasks.ts b/apps/web/app/stores/team-tasks.ts
index 27161efe4..78783383e 100644
--- a/apps/web/app/stores/team-tasks.ts
+++ b/apps/web/app/stores/team-tasks.ts
@@ -12,7 +12,12 @@ export const activeTeamTaskState = atom({
key: 'activeTeamTaskState',
default: null
});
-
+export const activeTeamTaskId = atom<{ id: string }>({
+ key: 'activeTeamTaskId',
+ default: {
+ id: ''
+ }
+});
export const tasksFetchingState = atom({
key: 'tasksFetchingState',
default: false
diff --git a/apps/web/app/stores/user.ts b/apps/web/app/stores/user.ts
index c5fcb7c58..17396c06d 100644
--- a/apps/web/app/stores/user.ts
+++ b/apps/web/app/stores/user.ts
@@ -5,3 +5,7 @@ export const userState = atom({
key: 'userState',
default: null
});
+export const userDetailAccordion = atom({
+ key: 'userDetailAccordion',
+ default: ''
+});
diff --git a/apps/web/app/utils/check-provider-env-vars.ts b/apps/web/app/utils/check-provider-env-vars.ts
new file mode 100644
index 000000000..848ac64e4
--- /dev/null
+++ b/apps/web/app/utils/check-provider-env-vars.ts
@@ -0,0 +1,50 @@
+import Apple from 'next-auth/providers/apple';
+import Discord from 'next-auth/providers/discord';
+import Facebook from 'next-auth/providers/facebook';
+import Google from 'next-auth/providers/google';
+import Github from 'next-auth/providers/github';
+import Linkedin from 'next-auth/providers/linkedin';
+import MicrosoftEntraID from 'next-auth/providers/microsoft-entra-id';
+import Slack from 'next-auth/providers/slack';
+import Twitter from 'next-auth/providers/twitter';
+import type { Provider } from 'next-auth/providers';
+
+type ProviderNames = {
+ [key: string]: string | undefined;
+};
+
+export const providerNames: ProviderNames = {
+ apple: process.env.NEXT_PUBLIC_APPLE_APP_NAME,
+ discord: process.env.NEXT_PUBLIC_DISCORD_APP_NAME,
+ facebook: process.env.NEXT_PUBLIC_FACEBOOK_APP_NAME,
+ google: process.env.NEXT_PUBLIC_GOOGLE_APP_NAME,
+ github: process.env.NEXT_PUBLIC_GITHUB_APP_NAME,
+ linkedin: process.env.NEXT_PUBLIC_LINKEDIN_APP_NAME,
+ microsoftEntraId: process.env.NEXT_PUBLIC_MICROSOFTENTRAID_APP_NAME,
+ slack: process.env.NEXT_PUBLIC_SLACK_APP_NAME,
+ twitter: process.env.NEXT_PUBLIC_TWITTER_APP_NAME
+};
+
+export const providers: Provider[] = [
+ Apple,
+ Discord,
+ Facebook,
+ Google,
+ Github,
+ Linkedin,
+ MicrosoftEntraID,
+ Slack,
+ Twitter
+];
+
+export const filteredProviders = providers.filter((provider) => {
+ const providerName = provider.name.toLowerCase();
+ return providerNames[providerName] !== undefined;
+});
+
+export const mappedProviders = filteredProviders.map((provider) => {
+ if (typeof provider === 'function') {
+ const providerData = provider();
+ return { id: providerData.id, name: providerData.name };
+ } else return { id: provider.id, name: provider.name };
+});
diff --git a/apps/web/auth.ts b/apps/web/auth.ts
new file mode 100644
index 000000000..c18f055fc
--- /dev/null
+++ b/apps/web/auth.ts
@@ -0,0 +1,43 @@
+import NextAuth from 'next-auth';
+import { filteredProviders } from '@app/utils/check-provider-env-vars';
+import { GauzyAdapter, jwtCallback, ProviderEnum, signInCallback } from '@app/services/server/requests/OAuth';
+
+export const { handlers, signIn, signOut, auth } = NextAuth({
+ providers: filteredProviders,
+ adapter: GauzyAdapter,
+ callbacks: {
+ async signIn({ account }) {
+ if (account) {
+ const { provider, access_token } = account;
+ if (access_token) {
+ return await signInCallback(provider as ProviderEnum, access_token);
+ }
+ }
+ return true;
+ },
+
+ async jwt({ token, user, trigger, session, account }) {
+ if (user) {
+ if (account) {
+ const { access_token, provider } = account;
+ if (access_token) {
+ token.authCookie = await jwtCallback(provider as ProviderEnum, access_token);
+ }
+ }
+ }
+
+ if (trigger === 'update' && session) {
+ token = { ...token, authCookie: session };
+ }
+
+ return token;
+ },
+ session({ session, token }) {
+ session.user = token.authCookie as any;
+ return session;
+ }
+ },
+ pages: {
+ error: '/auth/error'
+ }
+});
diff --git a/apps/web/components/pages/404/index.tsx b/apps/web/components/pages/404/index.tsx
index a26d8643a..5e8db526d 100644
--- a/apps/web/components/pages/404/index.tsx
+++ b/apps/web/components/pages/404/index.tsx
@@ -1,26 +1,32 @@
'use client';
-import SadCry from '@components/ui/svgs/sad-cry';
+// import SadCry from '@components/ui/svgs/sad-cry';
import { Button, Text } from 'lib/components';
import Link from 'next/link';
+// import { useTranslations } from 'next-intl';
function NotFound() {
+ // const t = useTranslations();
return (
-
-
-
- 404!
-
+
+
+
+ {/* */}
+ 404
+
+
+
+
+ Page Not found !{/* {t('pages.notFound.TITLE')} */}
+
+
+
+ Resource you are looking for is not found !
+
-
- Page not found !
-
-
-
- {`We looked, but can't find it ....`}
-
-
-
-
+
+
);
diff --git a/apps/web/components/pages/kanban/create-task-modal.tsx b/apps/web/components/pages/kanban/create-task-modal.tsx
index a7333efb1..a511869fa 100644
--- a/apps/web/components/pages/kanban/create-task-modal.tsx
+++ b/apps/web/components/pages/kanban/create-task-modal.tsx
@@ -1,10 +1,11 @@
import { TaskInputKanban } from 'lib/features/task/task-input-kanban';
import React from 'react';
-const CreateTaskModal = (props: { task: any; initEditMode: boolean; tasks: any; title: string }) => {
+const CreateTaskModal = (props: { onClose: any; task: any; initEditMode: boolean; tasks: any; title: string }) => {
return (
{
+ const { editTaskStatus, loading,editTaskStatusLoading } = useTaskStatus();
+ const editStatus: any = editTaskStatus;
+ const [createNew] = useState(status);
+ const t = useTranslations();
+ const { register, handleSubmit, setValue, getValues } = useForm({
+ defaultValues: {
+ name: status.name || '',
+ color: status.color || '',
+ icon: status.icon || ''
+ }
+ });
+ const renameProperty = (newProp: string, icon: string) => {
+ setColumn((prev: any) => {
+ const newColumn = prev.map((column: any) => {
+ if (column.id === status.id) {
+ return {
+ ...column,
+ name: newProp,
+ icon: !icon.includes('https') ? `https://api.ever.team/public/${icon}` : icon
+ };
+ }
+ return column;
+ });
+ return newColumn;
+ });
+ };
+
+ const onSubmit = async (values: EditSet) => {
+ if (status && values) {
+ await editStatus(status.id, { ...values, color: !values.color ? status.color : values.color }).then(() => {
+ renameProperty(values.name, values.icon);
+
+ // Call this function with 'Open1' and 'Open2' when you need to change the property name.
+ onClose();
+ });
+ }
+ };
+ const taskStatusIconList: IIcon[] = generateIconList('task-statuses', [
+ 'open',
+ 'in-progress',
+ 'ready',
+ 'in-review',
+ 'blocked',
+ 'completed'
+ ]);
+ const taskSizesIconList: IIcon[] = generateIconList('task-sizes', ['x-large', 'large', 'medium', 'small', 'tiny']);
+ const taskPrioritiesIconList: IIcon[] = generateIconList('task-priorities', ['urgent', 'high', 'medium', 'low']);
+
+ const iconList: IIcon[] = [...taskStatusIconList, ...taskSizesIconList, ...taskPrioritiesIconList];
+
+ return (
+
+ );
+};
+
+export default EditStatusModal;
diff --git a/apps/web/components/pages/kanban/menu-kanban-card.tsx b/apps/web/components/pages/kanban/menu-kanban-card.tsx
new file mode 100644
index 000000000..85e8bbbd5
--- /dev/null
+++ b/apps/web/components/pages/kanban/menu-kanban-card.tsx
@@ -0,0 +1,101 @@
+import { useTeamMemberCard } from '@app/hooks';
+import { activeTeamTaskId } from '@app/stores';
+import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover';
+import { ThreeCircleOutlineVerticalIcon } from 'assets/svg';
+import { HorizontalSeparator, SpinnerLoader } from 'lib/components';
+import { PlanTask } from 'lib/features/task/task-card';
+import { useTranslations } from 'next-intl';
+import { useState } from 'react';
+import { useSetRecoilState } from 'recoil';
+
+export default function MenuKanbanCard({ member, item }: { item: any; member: any }) {
+ const t = useTranslations();
+ const { assignTask, unassignTask, assignTaskLoading, unAssignTaskLoading } = useTeamMemberCard(member);
+ const setActiveTask = useSetRecoilState(activeTeamTaskId);
+ const [load, setLoad] = useState<'' | 'assign' | 'unassign'>('');
+ const menu = [
+ {
+ name: t('common.EDIT_TASK'),
+ closable: true,
+ action: 'edit',
+ active: true,
+ onClick: () => {
+ setActiveTask({
+ id: item.id
+ });
+ }
+ },
+ {
+ name: t('common.ESTIMATE'),
+ closable: true,
+ action: 'estimate',
+ onClick: () => {
+ // TODO: Implement estimate task after fixing the time estimate issue
+ },
+ active: true
+ },
+ {
+ name: t('common.ASSIGN_TASK'),
+ action: 'assign',
+ active: true,
+ onClick: () => {
+ setLoad('assign');
+ assignTask(item);
+ }
+ },
+ {
+ name: t('common.UNASSIGN_TASK'),
+ action: 'unassign',
+ closable: true,
+ active: true,
+ onClick: () => {
+ setLoad('unassign');
+ unassignTask(item);
+ }
+ }
+ ].filter((item) => item.active || item.active === undefined);
+
+ return (
+
+
+
+
+
+
+ {menu.map((item) => {
+ return (
+ - item?.onClick()}>
+
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/components/pages/main/header-tabs.tsx b/apps/web/components/pages/main/header-tabs.tsx
index 942b8783b..00b69117d 100644
--- a/apps/web/components/pages/main/header-tabs.tsx
+++ b/apps/web/components/pages/main/header-tabs.tsx
@@ -7,13 +7,15 @@ import KanbanIcon from '@components/ui/svgs/kanban';
import { IssuesView } from '@app/constants';
import { useRecoilState } from 'recoil';
import { headerTabs } from '@app/stores/header-tabs';
+import { DottedLanguageObjectStringPaths, useTranslations } from 'next-intl';
const HeaderTabs = ({ linkAll, kanban = false }: { linkAll: boolean; kanban?: boolean }) => {
+ const t = useTranslations();
const options = [
- { label: 'Cards', icon: QueueListIcon, view: IssuesView.CARDS },
- { label: 'Table', icon: TableCellsIcon, view: IssuesView.TABLE },
- { label: 'Blocks', icon: Squares2X2Icon, view: IssuesView.BLOCKS },
- { label: 'Kanban', icon: KanbanIcon, view: IssuesView.KANBAN }
+ { label: 'CARDS', icon: QueueListIcon, view: IssuesView.CARDS },
+ { label: 'TABLE', icon: TableCellsIcon, view: IssuesView.TABLE },
+ { label: 'BLOCKS', icon: Squares2X2Icon, view: IssuesView.BLOCKS },
+ { label: 'KANBAN', icon: KanbanIcon, view: IssuesView.KANBAN }
];
const links = linkAll ? ['/', '/', '/', '/kanban'] : [undefined, undefined, undefined, '/kanban'];
const [view, setView] = useRecoilState(headerTabs);
@@ -21,7 +23,7 @@ const HeaderTabs = ({ linkAll, kanban = false }: { linkAll: boolean; kanban?: bo
return (
<>
{options.map(({ label, icon: Icon, view: optionView }, index) => (
-
+
+ />
diff --git a/apps/web/components/pages/task/description-block/editor-toolbar.tsx b/apps/web/components/pages/task/description-block/editor-toolbar.tsx
index b21aa931a..906cfe567 100644
--- a/apps/web/components/pages/task/description-block/editor-toolbar.tsx
+++ b/apps/web/components/pages/task/description-block/editor-toolbar.tsx
@@ -27,12 +27,20 @@ import {
AlignFullIcon,
ChevronDownIcon
} from 'assets/svg';
+import { BsEmojiSmile } from 'react-icons/bs';
+import { clsxm } from '@app/utils';
+import data from '@emoji-mart/data';
+import Picker from '@emoji-mart/react';
+import { MdOutlineClose } from "react-icons/md";
+
interface IToolbarProps {
isMarkActive?: (editor: any, format: string) => boolean;
isBlockActive?: (editor: any, format: any, blockType?: string) => boolean;
+ selectEmoji?: (emoji: { native: string }) => void;
+ showEmojiIcon?: boolean;
}
-const Toolbar = ({ isMarkActive, isBlockActive }: IToolbarProps) => {
+const Toolbar = ({ isMarkActive, isBlockActive, selectEmoji, showEmojiIcon }: IToolbarProps) => {
const t = useTranslations();
const editor = useSlateStatic();
const [showLinkPopup, setShowLinkPopup] = useState(false);
@@ -43,9 +51,11 @@ const Toolbar = ({ isMarkActive, isBlockActive }: IToolbarProps) => {
top: 0
});
const [showDropdown, setShowDropdown] = useState(false);
+ const [showEmoji, setShowEmoji] = useState(false);
const popupRef = useRef
(null);
const inputRef = useRef(null);
const dropdownRef = useRef(null);
+ const emojiRef = useRef(null);
// const handleLinkIconClick = () => {
// const selection = editor.selection;
@@ -147,6 +157,27 @@ const Toolbar = ({ isMarkActive, isBlockActive }: IToolbarProps) => {
};
}, [onClickOutsideOfDropdown]);
+ useEffect(() => {
+ const handleClickOutsideOfEmoji = (event: MouseEvent) => {
+ if (emojiRef.current && !emojiRef.current.contains(event.target as unknown as Node)) {
+ setShowEmoji(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutsideOfEmoji);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutsideOfEmoji);
+ };
+ }, [setShowEmoji]);
+
+
+ const addEmoji = (emoji: { native: string }) => {
+
+ if (showEmojiIcon) {
+ selectEmoji?.(emoji);
+ }
+ };
+
// const isBlockActiveMemo = useMemo(() => {
// return (
// isBlockActive &&
@@ -161,10 +192,11 @@ const Toolbar = ({ isMarkActive, isBlockActive }: IToolbarProps) => {
// }, [editor, isBlockActive]);
return (
-
+
{t('pages.taskDetails.DESCRIPTION')}
+
{
icon={AlignFullIcon}
isBlockActive={isBlockActive as (editor: any, format: any, blockType?: string | undefined) => boolean}
/>
+
+ setShowEmoji(true)} className={clsxm('mr-3')} />
+ {
+ showEmoji &&
+
+ setShowEmoji(false)}
+ className="absolute right-5 cursor-pointer"
+ />
+
+
+
+ }
+
);
};
diff --git a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx
index d2500a643..260d9f22e 100644
--- a/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx
+++ b/apps/web/components/pages/task/details-section/blocks/task-secondary-info.tsx
@@ -50,9 +50,7 @@ const TaskSecondaryInfo = () => {
const onVersionCreated = useCallback(
(version: ITaskVersionCreate) => {
- if ($taskVersion.current.length === 0) {
- handleStatusUpdate(version.value || version.name, 'version', task);
- }
+ handleStatusUpdate(version.value || version.name, 'version', task);
},
[$taskVersion, task, handleStatusUpdate]
);
@@ -72,6 +70,7 @@ const TaskSecondaryInfo = () => {
);
const taskLabels = useTaskLabelsValue();
+
const tags = useMemo(() => {
return (
task?.tags
@@ -120,6 +119,7 @@ const TaskSecondaryInfo = () => {
/>
)}
+
{task && }
{/* Task Status */}
@@ -235,7 +235,7 @@ const EpicParent = ({ task }: { task: ITeamTask }) => {
- ,
+
{`#${task?.rootEpic?.number} ${task?.rootEpic?.title}`}
diff --git a/apps/web/components/pages/task/details-section/components/profile-info-with-time.tsx b/apps/web/components/pages/task/details-section/components/profile-info-with-time.tsx
index fb2cbd0cf..44137a048 100644
--- a/apps/web/components/pages/task/details-section/components/profile-info-with-time.tsx
+++ b/apps/web/components/pages/task/details-section/components/profile-info-with-time.tsx
@@ -15,6 +15,7 @@ const ProfileInfoWithTime = ({ profilePicSrc, names, profileInfoWrapperClassName
diff --git a/apps/web/components/pages/task/details-section/components/profile-info.tsx b/apps/web/components/pages/task/details-section/components/profile-info.tsx
index 3a8aba4be..e6fc77302 100644
--- a/apps/web/components/pages/task/details-section/components/profile-info.tsx
+++ b/apps/web/components/pages/task/details-section/components/profile-info.tsx
@@ -8,15 +8,16 @@ import stc from 'string-to-color';
type Props = {
profilePicSrc?: string;
names?: string;
+ fullName?: string;
wrapperClassName?: string;
profilePicSize?: number;
};
-const ProfileInfo = ({ profilePicSrc, names, wrapperClassName, profilePicSize }: Props) => {
+const ProfileInfo = ({ profilePicSrc, fullName, names, wrapperClassName, profilePicSize }: Props) => {
const size = profilePicSize || 20;
return (
-
+
= ({ labelIconPath, afterIconPath, labelTitle, alignWithIconLabel }) => {
return (
{labelIconPath ? (
{
const t = useTranslations();
@@ -28,6 +29,11 @@ const TaskDetailsAside = () => {
{/* Divider */}
+
+
+ {/* Divider */}
+
+
{/* Divider */}
diff --git a/apps/web/components/pages/task/title-block/task-title-block.tsx b/apps/web/components/pages/task/title-block/task-title-block.tsx
index 5628b8024..b19256525 100644
--- a/apps/web/components/pages/task/title-block/task-title-block.tsx
+++ b/apps/web/components/pages/task/title-block/task-title-block.tsx
@@ -3,7 +3,7 @@ import { ITeamTask } from '@app/interfaces';
import { detailedTaskState } from '@app/stores';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@components/ui/hover-card';
import { useToast } from '@components/ui/use-toast';
-import { Button, Tooltip } from 'lib/components';
+import { Button, CopyTooltip } from 'lib/components';
import { ActiveTaskIssuesDropdown } from 'lib/features';
import Image from 'next/image';
import { CheckSimpleIcon, CopyRoundIcon } from 'assets/svg';
@@ -15,6 +15,7 @@ import CreateParentTask from '../ParentTask';
import TitleLoader from './title-loader';
import { useTranslations } from 'next-intl';
import { XMarkIcon } from '@heroicons/react/20/solid';
+import { clsxm } from '@app/utils';
const TaskTitleBlock = () => {
const { updateTitle, updateLoading } = useTeamTasks();
@@ -30,7 +31,6 @@ const TaskTitleBlock = () => {
//States
const [edit, setEdit] = useState(false);
- const [copied, setCopied] = useState(false);
const [task] = useRecoilState(detailedTaskState);
const [title, setTitle] = useState('');
@@ -97,16 +97,6 @@ const TaskTitleBlock = () => {
titleDOM.current?.style.setProperty('height', titleDOM.current.scrollHeight + 'px');
};
- const copyTitle = () => {
- navigator.clipboard.writeText(title);
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- };
-
- const copyTaskNumber = () => {
- task && navigator.clipboard.writeText(task?.taskNumber);
- };
-
const handleTaskTitleChange = (event: ChangeEvent) => {
setTitle(event.target.value);
};
@@ -116,15 +106,18 @@ const TaskTitleBlock = () => {
{task ? (
+ />
{edit ? (
@@ -133,18 +126,18 @@ const TaskTitleBlock = () => {
onClick={() => saveTitle(title)}
className="border-2 dark:border-[#464242] rounded-md"
>
-
+
) : (
-
+
-
+
+
)}
@@ -223,13 +216,12 @@ const TaskTitleBlock = () => {
-
-
- {t('pages.settingsTeam.COPY_NUMBER')}
-
+
+
+
+ {t('pages.settingsTeam.COPY_NUMBER')}
+
+
);
@@ -244,33 +236,30 @@ const ParentTaskBadge = ({ task }: { task: ITeamTask | null }) => {
{`#${task.parent.taskNumber || task.parent.number}`}
{` - ${task.parent.title}`}
diff --git a/apps/web/components/shared/collaborate/index.tsx b/apps/web/components/shared/collaborate/index.tsx
index 8c4e08ee7..e760935ba 100644
--- a/apps/web/components/shared/collaborate/index.tsx
+++ b/apps/web/components/shared/collaborate/index.tsx
@@ -21,6 +21,7 @@ import stc from 'string-to-color';
import { JitsuAnalytics } from '../../../lib/components/services/jitsu-analytics';
import { useTranslations } from 'next-intl';
import { BrushSquareIcon, PhoneUpArrowIcon, UserLinearIcon } from 'assets/svg';
+import { ScrollArea } from '@components/ui/scroll-bar';
const Collaborate = () => {
const { onMeetClick, onBoardClick, collaborativeMembers, setCollaborativeMembers } = useCollaborative();
@@ -89,58 +90,64 @@ const Collaborate = () => {
{t('common.USER_NOT_FOUND')}
-
- {members.map((member) => (
- {
- handleMemberClick(member);
- }}
- >
-
+
+ {members.map((member) => (
+ {
+ handleMemberClick(member);
}}
>
- {(member?.image?.thumbUrl || member?.image?.fullUrl || member?.imageUrl) &&
- isValidUrl(
- member?.image?.thumbUrl || member?.image?.fullUrl || member?.imageUrl
- ) ? (
-
+ {(member?.image?.thumbUrl ||
+ member?.image?.fullUrl ||
+ member?.imageUrl) &&
+ isValidUrl(
+ member?.image?.thumbUrl ||
member?.image?.fullUrl ||
member?.imageUrl
- }
- alt="Team Avatar"
- imageTitle={member?.name || ''}
- >
- ) : member?.name ? (
- imgTitle(member?.name || ' ').charAt(0)
- ) : (
- ''
- )}
-
+ ) ? (
+
+ ) : member?.name ? (
+ imgTitle(member?.name || ' ').charAt(0)
+ ) : (
+ ''
+ )}
+
-
-
{member?.name}
-
{member?.email}
-
- {selectedMemberIds.includes(member?.id) ? (
-
- ) : null}
-
- ))}
-
+
+
{member?.name}
+
{member?.email}
+
+ {selectedMemberIds.includes(member?.id) ? (
+
+ ) : null}
+
+ ))}
+
+
diff --git a/apps/web/components/ui/accordion.tsx b/apps/web/components/ui/accordion.tsx
new file mode 100644
index 000000000..c60b2759b
--- /dev/null
+++ b/apps/web/components/ui/accordion.tsx
@@ -0,0 +1,54 @@
+import * as React from 'react';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import { ChevronDown } from 'lucide-react';
+
+import { cn } from 'lib/utils';
+
+const Accordion = AccordionPrimitive.Root;
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AccordionItem.displayName = 'AccordionItem';
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180',
+ className
+ )}
+ {...props}
+ >
+ <>
+ {children}
+
+ >
+
+
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/apps/web/components/ui/data-table.tsx b/apps/web/components/ui/data-table.tsx
index b0a2b3e9e..afeb6631e 100644
--- a/apps/web/components/ui/data-table.tsx
+++ b/apps/web/components/ui/data-table.tsx
@@ -16,6 +16,7 @@ import {
import { Table, TableHeader, TableRow, TableHead, TableCell, TableBody, TableFooter } from './table';
import { Tooltip } from 'lib/components';
+import { clsxm } from '@app/utils';
interface DataTableProps {
columns: ColumnDef[];
@@ -48,6 +49,7 @@ function DataTable({ columns, data, footerRows, isHeader }: DataT
// Let's set up our default column filter UI
size: 20
},
+
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
@@ -60,74 +62,85 @@ function DataTable({ columns, data, footerRows, isHeader }: DataT
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues()
});
+
return (
-
- {isHeader && (
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header, index) => {
- const tooltip: any = header.column.columnDef;
- const isTooltip: any = flexRender(tooltip.tooltip, header.getContext());
- return (
-
+
+ {isHeader && (
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header, index) => {
+ const tooltip: any = header.column.columnDef;
+ const isTooltip: any = flexRender(tooltip.tooltip, header.getContext());
+ return (
+
+
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+
+
+ );
+ })}
+
+ ))}
+
+ )}
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row, i) => (
+
+ {row.getVisibleCells().map((cell, index) => (
+
-
- {header.isPlaceholder
- ? null
- : flexRender(header.column.columnDef.header, header.getContext())}
-
-
- );
- })}
-
- ))}
-
- )}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
-
- {row.getVisibleCells().map((cell, index) => (
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
-
- ))}
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ No results.
+
- ))
- ) : (
-
-
- No results.
-
-
+ )}
+
+ {footerRows && footerRows?.length > 0 && (
+
+ {footerRows.map((row, index) => (
+ {row}
+ ))}
+
)}
-
- {footerRows && footerRows?.length > 0 && (
-
- {footerRows.map((row, index) => (
- {row}
- ))}
-
- )}
-
+
+ >
);
}
diff --git a/apps/web/components/ui/dropdown-menu.tsx b/apps/web/components/ui/dropdown-menu.tsx
new file mode 100644
index 000000000..eeba02e07
--- /dev/null
+++ b/apps/web/components/ui/dropdown-menu.tsx
@@ -0,0 +1,179 @@
+import * as React from 'react';
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+
+import { cn } from 'lib/utils';
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
+ return ;
+};
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup
+};
diff --git a/apps/web/components/ui/popover.tsx b/apps/web/components/ui/popover.tsx
index 6f3f1ba18..ccbdc3081 100644
--- a/apps/web/components/ui/popover.tsx
+++ b/apps/web/components/ui/popover.tsx
@@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
- 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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',
+ 'z-[9999] w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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',
className
)}
{...props}
diff --git a/apps/web/components/ui/scroll-bar.tsx b/apps/web/components/ui/scroll-bar.tsx
new file mode 100644
index 000000000..dfddf4fae
--- /dev/null
+++ b/apps/web/components/ui/scroll-bar.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import * as React from 'react';
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
+import { clsxm } from '@app/utils';
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+
+
+));
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
diff --git a/apps/web/components/ui/select.tsx b/apps/web/components/ui/select.tsx
index 85815e7fd..66bfff203 100644
--- a/apps/web/components/ui/select.tsx
+++ b/apps/web/components/ui/select.tsx
@@ -3,6 +3,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown } from 'lucide-react';
import { cn } from 'lib/utils';
+import { clsxm } from '@app/utils';
const Select = SelectPrimitive.Root;
@@ -70,8 +71,10 @@ SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
+ {
+ chevronClass?: string;
+ } & React.ComponentPropsWithoutRef
+>(({ className, chevronClass, children, ...props }, ref) => (