Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/app/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const api = createApi({
'Progress_Details',
'User_Standup',
'TASK_REQUEST',
'Extension_Requests',
],
/**
* This api has endpoints injected in adjacent files,
Expand Down
40 changes: 40 additions & 0 deletions src/app/services/tasksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import task, {
TaskRequestPayload,
TasksResponseType,
GetAllTaskParamType,
ExtensionRequest,
ExtensionRequestsResponse,
ExtensionRequestCreatePayload,
ExtensionRequestCreateResponse,
} from '@/interfaces/task.type';
import { api } from './api';
import { MINE_TASKS_URL, TASKS_URL } from '@/constants/url';
Expand Down Expand Up @@ -100,6 +104,40 @@ export const tasksApi = api.injectEndpoints({
},
],
}),
getSelfExtensionRequests: builder.query<
ExtensionRequestsResponse,
{ taskId: string; dev: boolean }
>({
query: ({ taskId, dev }) => ({
url: '/extension-requests/self',
params: {
taskId,
dev,
},
}),
providesTags: ['Extension_Requests'],
}),

createExtensionRequest: builder.mutation<
ExtensionRequestCreateResponse,
ExtensionRequestCreatePayload & { dev?: boolean }
>({
query: (payload) => ({
url: '/extension-requests',
method: 'POST',
params: { dev: payload.dev ?? true },
body: {
assignee: payload.assignee,
newEndsOn: payload.newEndsOn,
oldEndsOn: payload.oldEndsOn,
reason: payload.reason,
status: payload.status,
taskId: payload.taskId,
title: payload.title,
},
}),
invalidatesTags: ['Extension_Requests'],
}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider simplifying payload handling and review the default dev flag value.

The implementation works but has two areas for improvement:

  1. The payload is destructured and reconstructed in the body, which adds unnecessary verbosity.
  2. The dev parameter defaults to true in line 128, which might not be the intended behavior in production.
 createExtensionRequest: builder.mutation<
     ExtensionRequestCreateResponse,
     ExtensionRequestCreatePayload & { dev?: boolean }
 >({
     query: (payload) => ({
         url: '/extension-requests',
         method: 'POST',
-        params: { dev: payload.dev ?? true },
+        params: { dev: payload.dev },
         body: {
-            assignee: payload.assignee,
-            newEndsOn: payload.newEndsOn,
-            oldEndsOn: payload.oldEndsOn,
-            reason: payload.reason,
-            status: payload.status,
-            taskId: payload.taskId,
-            title: payload.title,
+            ...payload,
+            dev: undefined, // Remove dev from body as it's in params
         },
     }),
     invalidatesTags: ['Extension_Requests'],
 }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
createExtensionRequest: builder.mutation<
ExtensionRequestCreateResponse,
ExtensionRequestCreatePayload & { dev?: boolean }
>({
query: (payload) => ({
url: '/extension-requests',
method: 'POST',
params: { dev: payload.dev ?? true },
body: {
assignee: payload.assignee,
newEndsOn: payload.newEndsOn,
oldEndsOn: payload.oldEndsOn,
reason: payload.reason,
status: payload.status,
taskId: payload.taskId,
title: payload.title,
},
}),
invalidatesTags: ['Extension_Requests'],
}),
createExtensionRequest: builder.mutation<
ExtensionRequestCreateResponse,
ExtensionRequestCreatePayload & { dev?: boolean }
>({
query: (payload) => ({
url: '/extension-requests',
method: 'POST',
params: { dev: payload.dev },
body: {
...payload,
dev: undefined, // Remove dev from body as it's now in params
},
}),
invalidatesTags: ['Extension_Requests'],
}),

}),
overrideExisting: true,
});
Expand All @@ -110,4 +148,6 @@ export const {
useAddTaskMutation,
useUpdateTaskMutation,
useUpdateSelfTaskMutation,
useGetSelfExtensionRequestsQuery,
useCreateExtensionRequestMutation,
} = tasksApi;
181 changes: 181 additions & 0 deletions src/components/Form/ExtensionRequestForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState, useEffect } from 'react';
import styles from './Form.module.scss';
import { useCreateExtensionRequestMutation } from 'src/app/services/tasksApi';

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

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

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

const [createExtensionRequest, { isLoading }] =
useCreateExtensionRequestMutation();

useEffect(() => {
if (isOpen) {
const defaultNewEndsOn = oldEndsOn
? oldEndsOn + 3 * 24 * 60 * 60 * 1000
: new Date().getTime();
setFormData((prev) => ({ ...prev, newEndsOn: defaultNewEndsOn }));
}
}, [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 }));
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
}
};

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

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

try {
console.log('Sending data:', {
...formData,
taskId,
oldEndsOn: submissionOldEndsOn,
});

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

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Hard-coded dev: true

The dev flag is hard-coded; pass it from the caller (already available in ExtensionStatusModal) to keep environments configurable.

-    dev: true,
+    dev,

You’ll need to add dev to the component props.

Committable suggestion skipped: line range outside the PR's diff.

onClose();
} catch (error) {
console.error('Failed to create extension request:', error);
}
};

if (!isOpen) return null;

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

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

return (
<div className={styles.modalOverlay}>
<div className={styles.modalContent}>
<h2 className={styles.heading}>Extension Request Form</h2>

<form onSubmit={handleSubmit}>
<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
/>
</div>

<div className={styles.formGroup}>
<div className={styles.oldEta}>
Old ETA - {oldEndsOnDate}
</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
/>
</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
/>
</div>

<div className={styles.formActions}>
<button
type="button"
className={styles.cancelBtn}
onClick={onClose}
disabled={isLoading}
>
Cancel
</button>
<button
type="submit"
className={styles.submitBtn}
disabled={isLoading}
>
{isLoading ? 'Submitting...' : 'Submit'}
</button>
</div>
</form>
</div>
</div>
);
};
92 changes: 92 additions & 0 deletions src/components/Form/Form.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.modalContent {
background-color: white;
padding: 25px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 450px;
}

.heading {
color: #333;
font-size: 24px;
margin-top: 0;
margin-bottom: 20px;
}

.formGroup {
margin-bottom: 15px;
}

.label {
display: block;
font-weight: 500;
margin-bottom: 5px;
color: #333;
}

.textArea,
.input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
}

.oldEta {
margin-bottom: 10px;
font-weight: 500;
}

.dateTimeInput {
padding: 8px;
}

.formActions {
display: flex;
justify-content: space-between;
margin-top: 25px;
}

.button {
padding: 10px 15px;
font-size: 16px;
border: none;
border-radius: 4px;
cursor: pointer;
color: white;
width: 48%;
}

.cancelBtn {
composes: button;
background-color: #b80000;
}

.cancelBtn:hover {
background-color: #900000;
}

.submitBtn {
composes: button;
background-color: #008800;
}

.submitBtn:hover {
background-color: #006600;
}
Loading
Loading