Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/app/services/tasksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import task, {
TasksResponseType,
GetAllTaskParamType,
ExtensionRequestsResponse,
ExtensionRequestCreatePayload,
ExtensionRequestCreateResponse,
} from '@/interfaces/task.type';
import { api } from './api';
import { MINE_TASKS_URL, TASKS_URL } from '@/constants/url';
Expand Down Expand Up @@ -114,6 +116,21 @@ export const tasksApi = api.injectEndpoints({
}),
providesTags: ['Extension_Requests'],
}),
createExtensionRequest: builder.mutation<
ExtensionRequestCreateResponse,
ExtensionRequestCreatePayload & { dev?: boolean }
>({
query: (payload) => ({
url: '/extension-requests',
method: 'POST',
params: { dev: payload.dev ?? true },
body: (() => {
const { dev, ...requestPayload } = payload;
return requestPayload;
})(),
}),
invalidatesTags: ['Extension_Requests'],
}),
}),
overrideExisting: true,
});
Expand All @@ -125,4 +142,5 @@ export const {
useUpdateTaskMutation,
useUpdateSelfTaskMutation,
useGetSelfExtensionRequestsQuery,
useCreateExtensionRequestMutation,
} = tasksApi;
216 changes: 216 additions & 0 deletions src/components/ExtensionRequest/ExtensionRequestForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import React, { useState, useEffect } from 'react';
import styles from './Form.module.scss';
import { useCreateExtensionRequestMutation } from '@/app/services/tasksApi';
import { toast, ToastTypes } from '@/helperFunctions/toast';
import {
EXTENSION_REQUEST_SUCCESS_MESSAGE,
EXTENSION_REQUEST_ERROR_MESSAGE,
} from '@/constants/constants';

interface ExtensionRequestFormProps {
isOpen: boolean;
onClose: () => void;
taskId: string;
assignee: string;
oldEndsOn: number | null;
}

interface FormData {
reason: string;
newEndsOn: number;
title: string;
}

const { SUCCESS, ERROR } = ToastTypes;

const formatDateForInput = (timestamp: number) =>
new Date(timestamp).toISOString().slice(0, 16);

const getOldEndsOnDate = (oldEndsOn: number | null) =>
oldEndsOn
? new Date(oldEndsOn).toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: true,
})
: 'Not available';

export function ExtensionRequestForm({
isOpen,
onClose,
taskId,
assignee,
oldEndsOn,
}: ExtensionRequestFormProps) {
const [formData, setFormData] = useState<FormData>({
reason: '',
newEndsOn: new Date().getTime(),
title: '',
});

const [etaError, setEtaError] = useState(false);
const [createExtensionRequest, { isLoading }] =
useCreateExtensionRequestMutation();

useEffect(() => {
if (isOpen) {
const defaultNewEndsOn = oldEndsOn
? oldEndsOn + 3 * 24 * 60 * 60 * 1000
: new Date().getTime();
setFormData((prev) => ({ ...prev, newEndsOn: defaultNewEndsOn }));
setEtaError(false);
}
}, [isOpen, oldEndsOn]);

const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;

if (name === 'newEndsOn') {
const timestamp = new Date(value).getTime();
setFormData((prev) => ({ ...prev, [name]: timestamp }));

if (oldEndsOn && timestamp <= oldEndsOn) {
setEtaError(true);
} else {
setEtaError(false);
}
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
}
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

const submissionOldEndsOn = oldEndsOn || new Date().getTime();

try {
await createExtensionRequest({
assignee: assignee,
newEndsOn: Math.floor(formData.newEndsOn / 1000),
oldEndsOn: submissionOldEndsOn,
reason: formData.reason,
status: 'PENDING',
taskId,
title: formData.title,
dev: true,
}).unwrap();

toast(SUCCESS, EXTENSION_REQUEST_SUCCESS_MESSAGE);
onClose();
} catch (error: any) {
toast(
ERROR,
error?.data?.message ?? EXTENSION_REQUEST_ERROR_MESSAGE
);
}
};

if (!isOpen) return null;

const oldEndsOnDate = getOldEndsOnDate(oldEndsOn);

return (
<div className={styles.modalOverlay} data-testid="extension-form-modal">
<div className={styles.modalContent}>
<h2 className={styles.heading} data-testid="form-heading">
Extension Request Form
</h2>
<form onSubmit={handleSubmit} data-testid="extension-form">
<div className={styles.formGroup}>
<label className={styles.label} htmlFor="reason">
Reason
</label>
<textarea
id="reason"
name="reason"
value={formData.reason}
onChange={handleChange}
rows={4}
className={styles.textArea}
required
disabled={isLoading}
data-testid="reason-input"
/>
</div>

<div className={styles.formGroup}>
<div className={styles.oldEta} data-testid="old-eta">
Old ETA - {oldEndsOnDate}
{etaError && (
<p
className={styles.errorText}
data-testid="eta-error"
>
Please choose ETA greater than old ETA.
</p>
)}
</div>
</div>

<div className={styles.formGroup}>
<label className={styles.label} htmlFor="newEndsOn">
New ETA
</label>
<input
type="datetime-local"
id="newEndsOn"
name="newEndsOn"
value={formatDateForInput(formData.newEndsOn)}
onChange={handleChange}
className={`${styles.input} ${styles.dateTimeInput}`}
required
disabled={isLoading}
data-testid="new-eta-input"
/>
</div>

<div className={styles.formGroup}>
<label className={styles.label} htmlFor="title">
Title
</label>
<textarea
id="title"
name="title"
value={formData.title}
onChange={handleChange}
rows={3}
className={styles.textArea}
required
disabled={isLoading}
data-testid="title-input"
/>
</div>

<div className={styles.formActions}>
<button
type="button"
className={styles.cancelBtn}
onClick={onClose}
disabled={isLoading}
data-testid="cancel-button"
>
Cancel
</button>
<button
type="submit"
className={`${styles.submitBtn} ${
isLoading || etaError ? styles.disabledBtn : ''
}`}
disabled={isLoading || etaError}
data-testid="submit-button"
>
Submit
</button>
</div>
</form>
</div>
</div>
);
}
44 changes: 42 additions & 2 deletions src/components/ExtensionRequest/ExtensionStatusModal.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import styles from './ExtensionStatusModal.module.scss';
import { useGetSelfExtensionRequestsQuery } from '@/app/services/tasksApi';
import { SmallSpinner } from '../tasks/card/SmallSpinner';
import moment from 'moment';
import { ExtensionRequestDetails } from './ExtensionRequestDetails';
import { ExtensionRequest, ExtensionDetailItem } from '@/interfaces/task.type';
import { ExtensionRequestForm } from './ExtensionRequestForm';

type ExtensionStatusModalProps = {
isOpen: boolean;
Expand Down Expand Up @@ -84,6 +85,8 @@ export function ExtensionStatusModal({
{ skip: !isOpen }
);
const modalRef = useRef<HTMLDivElement>(null);
const [isRequestFormOpen, setIsRequestFormOpen] = useState(false);
const [oldEndsOn, setOldEndsOn] = useState<number | null>(null);

const extensionRequests = data?.allExtensionRequests ?? [];
const hasPendingRequest = extensionRequests.some(
Expand All @@ -105,13 +108,49 @@ export function ExtensionStatusModal({
};
}, [isOpen, onClose]);

useEffect(() => {
if (isOpen && (data?.allExtensionRequests ?? []).length > 0) {
const latestRequest = [...(data?.allExtensionRequests ?? [])].sort(
(a, b) => b.requestNumber - a.requestNumber
)[0];

const timestampMs =
latestRequest.newEndsOn < 1e12
? latestRequest.newEndsOn * 1000
: latestRequest.newEndsOn;

setOldEndsOn(timestampMs);
} else if (isOpen) {
setOldEndsOn(new Date().getTime());
}
}, [isOpen, data, taskId, dev]);

const handleOutsideClick = (e: React.MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose();
}
};

if (!isOpen) return null;
const handleOpenRequestForm = () => {
setIsRequestFormOpen(true);
onClose();
};

const handleCloseRequestForm = () => setIsRequestFormOpen(false);

if (!isOpen && !isRequestFormOpen) return null;

if (!isOpen && isRequestFormOpen) {
return (
<ExtensionRequestForm
isOpen={isRequestFormOpen}
onClose={handleCloseRequestForm}
taskId={taskId}
assignee={assignee}
oldEndsOn={oldEndsOn}
/>
);
}

if (isLoading) {
return (
Expand Down Expand Up @@ -174,6 +213,7 @@ export function ExtensionStatusModal({
<button
className={styles.extensionRequestButton}
data-testid="request-extension-button"
onClick={handleOpenRequestForm}
>
Request Extension
</button>
Expand Down
Loading