tsk-7: Schedule message (#8)

Closes #7

Reviewed-on: phoenix/textsender#8
This commit was merged in pull request #8.
This commit is contained in:
2026-06-30 12:13:52 -04:00
parent 833a044ff6
commit f2bdb60d55
10 changed files with 378 additions and 37 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "textsender", "name": "textsender",
"version": "0.0.5", "version": "0.0.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "textsender", "name": "textsender",
"version": "0.0.5", "version": "0.0.6",
"dependencies": { "dependencies": {
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "textsender", "name": "textsender",
"private": true, "private": true,
"version": "0.0.5", "version": "0.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+44
View File
@@ -0,0 +1,44 @@
import { API_BASE_URL } from '../constants/api';
export const scheduleApi = {
scheduleMessage: async (reqBody, authBearerToken) => {
console.log('Scheduling message');
const response = await fetch(`${API_BASE_URL}/api/v1/schedule/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
});
if (!response.ok) {
throw new Error('Error scheduling message');
} else {
return response.json();
}
},
prepareMessage: async (reqBody, authBearerToken) => {
console.log('Preparing Scheduled message');
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message/status/update`,
{
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
}
);
if (!response.ok) {
throw new Error('Error updating status of scheduled message');
} else {
return response.json();
}
},
};
+25
View File
@@ -0,0 +1,25 @@
import { API_BASE_URL } from '../constants/api';
export const scheduleEventApi = {
scheduleMessageEvent: async (reqBody, authBearerToken) => {
console.log('Scheduling message Event');
const response = await fetch(
`${API_BASE_URL}/api/v1/schedule/message/event`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: authBearerToken,
},
body: JSON.stringify(reqBody),
}
);
if (!response.ok) {
throw new Error('Error scheduling message event');
} else {
return response.json();
}
},
};
@@ -5,6 +5,7 @@ function ConfirmationStep({
selectedContacts, selectedContacts,
createdMessage, createdMessage,
message, message,
scheduledTime,
isSubmitting, isSubmitting,
}) { }) {
// In a real app, you might fetch full contact details here // In a real app, you might fetch full contact details here
@@ -88,6 +89,12 @@ function ConfirmationStep({
</div> </div>
</div> </div>
{/* Scheduled time Section */}
<div>
Scheduled time:{' '}
{scheduledTime ? scheduledTime.toISOString() : 'No schedule date'}
</div>
{/* Summary Section */} {/* Summary Section */}
<div className={styles.confirmationSection}> <div className={styles.confirmationSection}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
@@ -0,0 +1,89 @@
import React, { useState } from 'react';
function FutureTimeForm({ scheduledTime, setScheduledTime }) {
const [selectedTime, setSelectedTime] = useState(new Date());
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleTimeChange = (e) => {
setSelectedTime(e.target.value);
setScheduledTime(new Date(e.target.value));
setError('');
setSuccess('');
};
const validateAndSubmit = (e) => {
e.preventDefault();
if (!selectedTime) {
setError('Please select a date and time.');
return;
}
const now = new Date();
const inputDate = new Date(selectedTime);
// Calculate the minimum allowed time (Now + 10 minutes)
const minAllowedTime = new Date(now.getTime() + 10 * 60000);
if (inputDate < minAllowedTime) {
setError('The time must be at least 10 minutes in the future.');
setSuccess('');
setScheduledTime(null);
} else {
setError('');
setSuccess('Time validated successfully!');
setScheduledTime(inputDate);
}
};
return (
<div
style={{ maxWidth: '400px', margin: '2rem auto', fontFamily: 'Arial' }}
>
<h2>Schedule Event</h2>
<form onSubmit={validateAndSubmit}>
<div style={{ marginBottom: '1rem' }}>
<label
htmlFor="future-time"
style={{ display: 'block', marginBottom: '0.5rem' }}
>
Select Date and Time:
</label>
<input
type="datetime-local"
id="future-time"
value={selectedTime}
onChange={handleTimeChange}
style={{
padding: '8px',
width: '100%',
borderColor: error ? 'red' : '#ccc',
}}
/>
</div>
{error && <p style={{ color: 'red', fontSize: '0.9rem' }}>{error}</p>}
{success && (
<p style={{ color: 'green', fontSize: '0.9rem' }}>{success}</p>
)}
<button
type="submit"
style={{
padding: '10px 20px',
backgroundColor: '#007BFF',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Submit
</button>
</form>
</div>
);
}
export default FutureTimeForm;
@@ -3,9 +3,15 @@ import styles from './SendMessageWizard.module.css';
import { tokenService } from '../../services/TokenService'; import { tokenService } from '../../services/TokenService';
import { messageApi, CreateMessageRequest } from '../../api/messageApi'; import { messageApi, CreateMessageRequest } from '../../api/messageApi';
function MessageForm({ message, onMessageChange, onMessageCreated }) { function MessageForm({
message,
onMessageChange,
onMessageCreated,
onToggleScheduled,
}) {
const [characterCount, setCharacterCount] = useState(message.length); const [characterCount, setCharacterCount] = useState(message.length);
const [isCreatingMessage, setIsCreatingMessage] = useState(false); const [isCreatingMessage, setIsCreatingMessage] = useState(false);
const [isScheduledChecked, setIsScheduledChecked] = useState(false);
const [createdMessage, setCreatedMessage] = useState(null); const [createdMessage, setCreatedMessage] = useState(null);
const handleMessageChange = (e) => { const handleMessageChange = (e) => {
@@ -14,6 +20,11 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
setCharacterCount(newMessage.length); setCharacterCount(newMessage.length);
}; };
const handleIsScheduledCheckbox = (event) => {
setIsScheduledChecked(event.target.checked);
onToggleScheduled(event.target.checked);
};
const createMessage = async () => { const createMessage = async () => {
if (!message.trim()) { if (!message.trim()) {
return; return;
@@ -38,10 +49,8 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
const createdMessage = response.data[0]; const createdMessage = response.data[0];
setCreatedMessage(createdMessage); setCreatedMessage(createdMessage);
if (onMessageCreated) { if (onMessageCreated) {
console.log('Setting createdMessage');
onMessageCreated(createdMessage); onMessageCreated(createdMessage);
} }
console.log('Created message: ', createdMessage);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -97,6 +106,17 @@ function MessageForm({ message, onMessageChange, onMessageCreated }) {
<li>Check spelling and grammar</li> <li>Check spelling and grammar</li>
</ul> </ul>
</div> </div>
<div>
<label>
<input
type="checkbox"
checked={isScheduledChecked}
onChange={handleIsScheduledCheckbox}
readOnly
/>
Schedule Message
</label>
</div>
</div> </div>
); );
} }
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ContactSelection from './ContactSelection'; import ContactSelection from './ContactSelection';
import FutureTimeForm from './FutureTimeForm';
import MessageForm from './MessageForm'; import MessageForm from './MessageForm';
import ConfirmationStep from './ConfirmationStep'; import ConfirmationStep from './ConfirmationStep';
import StepNavigation from './StepNavigation'; import StepNavigation from './StepNavigation';
@@ -26,6 +27,14 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
message={wizard.message} message={wizard.message}
onMessageChange={wizard.setMessage} onMessageChange={wizard.setMessage}
onMessageCreated={wizard.setCreatedMessage} onMessageCreated={wizard.setCreatedMessage}
onToggleScheduled={wizard.toggleScheduled}
/>
);
case 'schedule':
return (
<FutureTimeForm
scheduledTime={wizard.scheduledTime}
setScheduledTime={wizard.setScheduledTime}
/> />
); );
case 'confirm': case 'confirm':
@@ -34,6 +43,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
selectedContacts={wizard.selectedContacts} selectedContacts={wizard.selectedContacts}
createdMessage={wizard.createdMessage} createdMessage={wizard.createdMessage}
message={wizard.message} message={wizard.message}
scheduledTime={wizard.scheduledTime}
isSubmitting={wizard.isSubmitting} isSubmitting={wizard.isSubmitting}
/> />
); );
@@ -74,6 +84,7 @@ function SendMessageWizardModal({ isOpen, onClose, onComplete }) {
const stepTitles = { const stepTitles = {
select: 'Select Contacts', select: 'Select Contacts',
message: 'Compose Message', message: 'Compose Message',
schedule: 'Schedule message',
confirm: 'Review & Send', confirm: 'Review & Send',
result: 'Result', result: 'Result',
}; };
@@ -34,6 +34,25 @@ function StepNavigation({
</button> </button>
</> </>
); );
case 'schedule':
return (
<>
<button
onClick={onBack}
disabled={!canGoBack}
className={`${styles.navButton} ${styles.backButton}`}
>
Back
</button>
<button
onClick={onNext}
disabled={!canProceed}
className={`${styles.navButton} ${styles.nextButton}`}
>
{currentStep === 'schedule' ? 'Next' : 'Review'}
</button>
</>
);
case 'confirm': case 'confirm':
return ( return (
<> <>
@@ -5,14 +5,18 @@ import {
sendMessageApi, sendMessageApi,
SendInstantMessageRequest, SendInstantMessageRequest,
} from '../../api/sendMessageApi'; } from '../../api/sendMessageApi';
import { scheduleApi } from '../../api/scheduleApi.js';
import { scheduleEventApi } from '../../api/scheduleEventApi.js';
const useSendMessageWizard = () => { const useSendMessageWizard = () => {
const [state, setState] = useState({ const [state, setState] = useState({
step: 'select', step: 'select',
selectedContacts: [], selectedContacts: [],
scheduledTime: null,
message: '', message: '',
createdMessage: null, createdMessage: null,
isSubmitting: false, isSubmitting: false,
isScheduled: false,
submissionError: null, submissionError: null,
}); });
@@ -25,12 +29,28 @@ const useSendMessageWizard = () => {
return state.selectedContacts.length > 0; return state.selectedContacts.length > 0;
case 'message': case 'message':
return state.message.trim().length > 0; return state.message.trim().length > 0;
case 'schedule':
console.log('Checking to see if schedule can go to the next step');
if (state.scheduledTime == null) {
return false;
}
console.log('Scheduled time is not null');
const result =
state.scheduledTime instanceof Date &&
isNaN(state.scheduledTime.getTime());
return !result;
case 'confirm': case 'confirm':
return true; return true;
default: default:
return false; return false;
} }
}, [state.step, state.selectedContacts.length, state.message]); }, [
state.step,
state.selectedContacts.length,
state.message,
state.scheduledTime,
]);
const canGoBack = state.step !== 'select'; const canGoBack = state.step !== 'select';
@@ -44,6 +64,14 @@ const useSendMessageWizard = () => {
})); }));
}; };
const toggleScheduled = (scheduled) => {
console.log('Yup');
setState((prev) => ({
...prev,
isScheduled: scheduled,
}));
};
const setMessage = (message) => { const setMessage = (message) => {
setState((prev) => ({ ...prev, message })); setState((prev) => ({ ...prev, message }));
}; };
@@ -53,29 +81,67 @@ const useSendMessageWizard = () => {
setState((prev) => ({ ...prev, createdMessage: message })); setState((prev) => ({ ...prev, createdMessage: message }));
}; };
const setScheduledTime = (selectedTime) => {
console.log('Setting scheduled time');
setState((prev) => ({ ...prev, scheduledTime: selectedTime }));
};
const nextStep = () => { const nextStep = () => {
if (!canProceedToNextStep()) return; if (!canProceedToNextStep()) return;
const stepOrder = ['select', 'message', 'confirm', 'result']; const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result'];
const currentIndex = stepOrder.indexOf(state.step); const currentIndex = stepOrder.indexOf(state.step);
if (currentIndex < stepOrder.length - 1) { if (currentIndex < stepOrder.length - 1) {
setState((prev) => ({ if (state.step === 'message') {
...prev, if (state.isScheduled) {
step: stepOrder[currentIndex + 1], setState((prev) => ({
submissionError: null, ...prev,
})); step: stepOrder[currentIndex + 1],
submissionError: null,
}));
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 2],
submissionError: null,
}));
}
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex + 1],
submissionError: null,
}));
}
} }
}; };
const prevStep = () => { const prevStep = () => {
const stepOrder = ['select', 'message', 'confirm', 'result']; const stepOrder = ['select', 'message', 'schedule', 'confirm', 'result'];
const currentIndex = stepOrder.indexOf(state.step); const currentIndex = stepOrder.indexOf(state.step);
if (currentIndex > 0) { if (currentIndex > 0) {
setState((prev) => ({ if (state.step === 'confirm') {
...prev, if (state.isScheduled) {
step: stepOrder[currentIndex - 1], setState((prev) => ({
submissionError: null, ...prev,
})); step: stepOrder[currentIndex - 1],
submissionError: null,
}));
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 2],
submissionError: null,
}));
}
} else {
setState((prev) => ({
...prev,
step: stepOrder[currentIndex - 1],
submissionError: null,
}));
}
} }
}; };
@@ -91,25 +157,77 @@ const useSendMessageWizard = () => {
})); }));
try { try {
console.log('Sending message'); if (state.scheduledTime != null) {
let reqBod = new SendInstantMessageRequest(); console.log('Going to schedule message');
reqBod.user_id = tokenService.getUserId();
reqBod.message_id = state.createdMessage.id;
state.selectedContacts.forEach((selectedContact) => {
reqBod.contact_ids.push(selectedContact);
});
const response = await sendMessageApi.sendInstantMessage(
reqBod,
tokenService.bearerToken()
);
if (response) { const schMsgReq = {
// Success - move to result step scheduled: state.scheduledTime.toISOString(),
setState((prev) => ({ status: 'PENDING',
...prev, user_id: tokenService.getUserId(),
step: 'result', };
isSubmitting: false,
})); const token = tokenService.bearerToken();
const schMsgResponse = await scheduleApi.scheduleMessage(
schMsgReq,
token
);
if (schMsgResponse) {
console.log('Message scheduled');
const scheduledMessageId = schMsgResponse.data[0].id;
for (let contact of state.selectedContacts) {
let schMsgEvtReq = {
contact_id: contact,
message_id: state.createdMessage.id,
scheduled_message_id: scheduledMessageId,
};
const schMsgEvtResponse =
await scheduleEventApi.scheduleMessageEvent(schMsgEvtReq, token);
}
console.log('Updating message status');
const req = {
scheduled_message_id: scheduledMessageId,
status: 'READY',
};
console.log(req);
const response = await scheduleApi.prepareMessage(req, token);
if (response) {
console.log('Message scheduled');
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
isScheduled: false,
scheduledTime: null,
}));
}
}
} else {
console.log('Sending message');
let reqBod = new SendInstantMessageRequest();
reqBod.user_id = tokenService.getUserId();
reqBod.message_id = state.createdMessage.id;
state.selectedContacts.forEach((selectedContact) => {
reqBod.contact_ids.push(selectedContact);
});
const response = await sendMessageApi.sendInstantMessage(
reqBod,
tokenService.bearerToken()
);
if (response) {
// Success - move to result step
setState((prev) => ({
...prev,
step: 'result',
isSubmitting: false,
isScheduled: false,
scheduledTime: null,
}));
}
} }
} catch (error) { } catch (error) {
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
@@ -121,6 +239,8 @@ const useSendMessageWizard = () => {
...prev, ...prev,
submissionError: error.message || 'Failed to send messages', submissionError: error.message || 'Failed to send messages',
isSubmitting: false, isSubmitting: false,
isScheduled: false,
scheduledTime: null,
})); }));
} }
}; };
@@ -130,9 +250,11 @@ const useSendMessageWizard = () => {
setState({ setState({
step: 'select', step: 'select',
selectedContacts: [], selectedContacts: [],
scheduledTime: null,
message: '', message: '',
createdMessage: null, createdMessage: null,
isSubmitting: false, isSubmitting: false,
isScheduled: false,
submissionError: null, submissionError: null,
}); });
}; };
@@ -146,17 +268,21 @@ const useSendMessageWizard = () => {
// Getters // Getters
currentStep: state.step, currentStep: state.step,
selectedContacts: state.selectedContacts, selectedContacts: state.selectedContacts,
scheduledTime: state.scheduledTime,
message: state.message, message: state.message,
createdMessage: state.createdMessage, createdMessage: state.createdMessage,
isSubmitting: state.isSubmitting, isSubmitting: state.isSubmitting,
isScheduled: state.isScheduled,
submissionError: state.submissionError, submissionError: state.submissionError,
canProceedToNextStep: canProceedToNextStep(), canProceedToNextStep: canProceedToNextStep(),
canGoBack, canGoBack,
// Actions // Actions
toggleContact, toggleContact,
toggleScheduled,
setMessage, setMessage,
setCreatedMessage, setCreatedMessage,
setScheduledTime,
nextStep, nextStep,
prevStep, prevStep,
submit, submit,