feat(task-drawer): add recurring task configuration
Add support for configuring recurring tasks in the task drawer. This includes adding a new `schedule_id` field to the task type, creating a new `TaskDrawerRecurringConfig` component, and updating localization files for English, Spanish, and Portuguese. The configuration allows setting repeat intervals, days of the week, and monthly recurrence options.
This commit is contained in:
@@ -15,7 +15,8 @@
|
|||||||
"hide-start-date": "Hide Start Date",
|
"hide-start-date": "Hide Start Date",
|
||||||
"show-start-date": "Show Start Date",
|
"show-start-date": "Show Start Date",
|
||||||
"hours": "Hours",
|
"hours": "Hours",
|
||||||
"minutes": "Minutes"
|
"minutes": "Minutes",
|
||||||
|
"recurring": "Recurring"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Description",
|
"title": "Description",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"recurring": "Recurring",
|
||||||
|
"recurringTaskConfiguration": "Recurring task configuration",
|
||||||
|
"repeats": "Repeats",
|
||||||
|
"weekly": "Weekly",
|
||||||
|
"everyXDays": "Every X Days",
|
||||||
|
"everyXWeeks": "Every X Weeks",
|
||||||
|
"everyXMonths": "Every X Months",
|
||||||
|
"monthly": "Monthly",
|
||||||
|
"selectDaysOfWeek": "Select Days of the Week",
|
||||||
|
"mon": "Mon",
|
||||||
|
"tue": "Tue",
|
||||||
|
"wed": "Wed",
|
||||||
|
"thu": "Thu",
|
||||||
|
"fri": "Fri",
|
||||||
|
"sat": "Sat",
|
||||||
|
"sun": "Sun",
|
||||||
|
"monthlyRepeatType": "Monthly repeat type",
|
||||||
|
"onSpecificDate": "On a specific date",
|
||||||
|
"onSpecificDay": "On a specific day",
|
||||||
|
"dateOfMonth": "Date of the month",
|
||||||
|
"weekOfMonth": "Week of the month",
|
||||||
|
"dayOfWeek": "Day of the week",
|
||||||
|
"first": "First",
|
||||||
|
"second": "Second",
|
||||||
|
"third": "Third",
|
||||||
|
"fourth": "Fourth",
|
||||||
|
"last": "Last",
|
||||||
|
"intervalDays": "Interval (days)",
|
||||||
|
"intervalWeeks": "Interval (weeks)",
|
||||||
|
"intervalMonths": "Interval (months)",
|
||||||
|
"saveChanges": "Save Changes"
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"hide-start-date": "Ocultar fecha de inicio",
|
"hide-start-date": "Ocultar fecha de inicio",
|
||||||
"show-start-date": "Mostrar fecha de inicio",
|
"show-start-date": "Mostrar fecha de inicio",
|
||||||
"hours": "Horas",
|
"hours": "Horas",
|
||||||
"minutes": "Minutos"
|
"minutes": "Minutos",
|
||||||
|
"recurring": "Recurrente"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Descripción",
|
"title": "Descripción",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"recurring": "Recurrente",
|
||||||
|
"recurringTaskConfiguration": "Configuración de tarea recurrente",
|
||||||
|
"repeats": "Repeticiones",
|
||||||
|
"weekly": "Semanal",
|
||||||
|
"everyXDays": "Cada X días",
|
||||||
|
"everyXWeeks": "Cada X semanas",
|
||||||
|
"everyXMonths": "Cada X meses",
|
||||||
|
"monthly": "Mensual",
|
||||||
|
"selectDaysOfWeek": "Seleccionar días de la semana",
|
||||||
|
"mon": "Lun",
|
||||||
|
"tue": "Mar",
|
||||||
|
"wed": "Mié",
|
||||||
|
"thu": "Jue",
|
||||||
|
"fri": "Vie",
|
||||||
|
"sat": "Sáb",
|
||||||
|
"sun": "Dom",
|
||||||
|
"monthlyRepeatType": "Tipo de repetición mensual",
|
||||||
|
"onSpecificDate": "En una fecha específica",
|
||||||
|
"onSpecificDay": "En un día específico",
|
||||||
|
"dateOfMonth": "Fecha del mes",
|
||||||
|
"weekOfMonth": "Semana del mes",
|
||||||
|
"dayOfWeek": "Día de la semana",
|
||||||
|
"first": "Primero",
|
||||||
|
"second": "Segundo",
|
||||||
|
"third": "Tercero",
|
||||||
|
"fourth": "Cuarto",
|
||||||
|
"last": "Último",
|
||||||
|
"intervalDays": "Intervalo (días)",
|
||||||
|
"intervalWeeks": "Intervalo (semanas)",
|
||||||
|
"intervalMonths": "Intervalo (meses)",
|
||||||
|
"saveChanges": "Guardar cambios"
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"hide-start-date": "Ocultar data de início",
|
"hide-start-date": "Ocultar data de início",
|
||||||
"show-start-date": "Mostrar data de início",
|
"show-start-date": "Mostrar data de início",
|
||||||
"hours": "Horas",
|
"hours": "Horas",
|
||||||
"minutes": "Minutos"
|
"minutes": "Minutos",
|
||||||
|
"recurring": "Recorrente"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"title": "Descrição",
|
"title": "Descrição",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"recurring": "Recorrente",
|
||||||
|
"recurringTaskConfiguration": "Configuração de tarefa recorrente",
|
||||||
|
"repeats": "Repete",
|
||||||
|
"weekly": "Semanal",
|
||||||
|
"everyXDays": "A cada X dias",
|
||||||
|
"everyXWeeks": "A cada X semanas",
|
||||||
|
"everyXMonths": "A cada X meses",
|
||||||
|
"monthly": "Mensal",
|
||||||
|
"selectDaysOfWeek": "Selecionar dias da semana",
|
||||||
|
"mon": "Seg",
|
||||||
|
"tue": "Ter",
|
||||||
|
"wed": "Qua",
|
||||||
|
"thu": "Qui",
|
||||||
|
"fri": "Sex",
|
||||||
|
"sat": "Sáb",
|
||||||
|
"sun": "Dom",
|
||||||
|
"monthlyRepeatType": "Tipo de repetição mensal",
|
||||||
|
"onSpecificDate": "Em uma data específica",
|
||||||
|
"onSpecificDay": "Em um dia específico",
|
||||||
|
"dateOfMonth": "Data do mês",
|
||||||
|
"weekOfMonth": "Semana do mês",
|
||||||
|
"dayOfWeek": "Dia da semana",
|
||||||
|
"first": "Primeira",
|
||||||
|
"second": "Segunda",
|
||||||
|
"third": "Terceira",
|
||||||
|
"fourth": "Quarta",
|
||||||
|
"last": "Última",
|
||||||
|
"intervalDays": "Intervalo (dias)",
|
||||||
|
"intervalWeeks": "Intervalo (semanas)",
|
||||||
|
"intervalMonths": "Intervalo (meses)",
|
||||||
|
"saveChanges": "Salvar alterações"
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
|
import { Form, Switch, Button, Popover, Select, Checkbox, Radio, InputNumber, Skeleton, Row, Col } from 'antd';
|
||||||
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { ITaskRecurringScheduleData } from '@/types/tasks/task-recurring-schedule';
|
||||||
|
import { ITaskViewModel } from '@/types/tasks/task.types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
// Dummy enums and types for demonstration; replace with actual imports/types
|
||||||
|
const ITaskRecurring = {
|
||||||
|
Weekly: 'weekly',
|
||||||
|
EveryXDays: 'every_x_days',
|
||||||
|
EveryXWeeks: 'every_x_weeks',
|
||||||
|
EveryXMonths: 'every_x_months',
|
||||||
|
Monthly: 'monthly',
|
||||||
|
};
|
||||||
|
|
||||||
|
const repeatOptions = [
|
||||||
|
{ label: 'Weekly', value: ITaskRecurring.Weekly },
|
||||||
|
{ label: 'Every X Days', value: ITaskRecurring.EveryXDays },
|
||||||
|
{ label: 'Every X Weeks', value: ITaskRecurring.EveryXWeeks },
|
||||||
|
{ label: 'Every X Months', value: ITaskRecurring.EveryXMonths },
|
||||||
|
{ label: 'Monthly', value: ITaskRecurring.Monthly },
|
||||||
|
];
|
||||||
|
|
||||||
|
const daysOfWeek = [
|
||||||
|
{ label: 'Mon', value: 'mon' },
|
||||||
|
{ label: 'Tue', value: 'tue' },
|
||||||
|
{ label: 'Wed', value: 'wed' },
|
||||||
|
{ label: 'Thu', value: 'thu' },
|
||||||
|
{ label: 'Fri', value: 'fri' },
|
||||||
|
{ label: 'Sat', value: 'sat' },
|
||||||
|
{ label: 'Sun', value: 'sun' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const monthlyDateOptions = Array.from({ length: 31 }, (_, i) => i + 1);
|
||||||
|
const weekOptions = [
|
||||||
|
{ label: 'First', value: 'first' },
|
||||||
|
{ label: 'Second', value: 'second' },
|
||||||
|
{ label: 'Third', value: 'third' },
|
||||||
|
{ label: 'Fourth', value: 'fourth' },
|
||||||
|
{ label: 'Last', value: 'last' },
|
||||||
|
];
|
||||||
|
const dayOptions = daysOfWeek.map(d => ({ label: d.label, value: d.value }));
|
||||||
|
|
||||||
|
const TaskDrawerRecurringConfig = ({ task }: {task: ITaskViewModel}) => {
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
|
const { t } = useTranslation('task-drawer/task-drawer-recurring-config');
|
||||||
|
|
||||||
|
const [recurring, setRecurring] = useState(false);
|
||||||
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [repeatOption, setRepeatOption] = useState(repeatOptions[0]);
|
||||||
|
const [selectedDays, setSelectedDays] = useState([]);
|
||||||
|
const [monthlyOption, setMonthlyOption] = useState('date');
|
||||||
|
const [selectedMonthlyDate, setSelectedMonthlyDate] = useState(1);
|
||||||
|
const [selectedMonthlyWeek, setSelectedMonthlyWeek] = useState(weekOptions[0].value);
|
||||||
|
const [selectedMonthlyDay, setSelectedMonthlyDay] = useState(dayOptions[0].value);
|
||||||
|
const [intervalDays, setIntervalDays] = useState(1);
|
||||||
|
const [intervalWeeks, setIntervalWeeks] = useState(1);
|
||||||
|
const [intervalMonths, setIntervalMonths] = useState(1);
|
||||||
|
const [loadingData, setLoadingData] = useState(false);
|
||||||
|
const [updatingData, setUpdatingData] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (checked: boolean) => {
|
||||||
|
setRecurring(checked);
|
||||||
|
if (!checked) setShowConfig(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configVisibleChange = (visible: boolean) => {
|
||||||
|
setShowConfig(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMonthlySelected = useMemo(() => repeatOption.value === ITaskRecurring.Monthly, [repeatOption]);
|
||||||
|
|
||||||
|
const handleDayCheckboxChange = (checkedValues: string[]) => {
|
||||||
|
setSelectedDays(checkedValues as unknown as string[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Compose the schedule data and call the update handler
|
||||||
|
const data = {
|
||||||
|
recurring,
|
||||||
|
repeatOption,
|
||||||
|
selectedDays,
|
||||||
|
monthlyOption,
|
||||||
|
selectedMonthlyDate,
|
||||||
|
selectedMonthlyWeek,
|
||||||
|
selectedMonthlyDay,
|
||||||
|
intervalDays,
|
||||||
|
intervalWeeks,
|
||||||
|
intervalMonths,
|
||||||
|
};
|
||||||
|
// if (onUpdateSchedule) onUpdateSchedule(data);
|
||||||
|
setShowConfig(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScheduleData = () => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResponse = (response: ITaskRecurringScheduleData) => {
|
||||||
|
if (!task || !response.task_id) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (task) setRecurring(!!task.schedule_id);
|
||||||
|
if (recurring) void getScheduleData();
|
||||||
|
socket?.on(SocketEvents.TASK_RECURRING_CHANGE.toString(), handleResponse)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Form.Item className="w-100 mb-2 align-form-item" style={{ marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
|
||||||
|
<Switch checked={recurring} onChange={handleChange} />
|
||||||
|
|
||||||
|
{recurring && (
|
||||||
|
<Popover
|
||||||
|
title="Recurring task configuration"
|
||||||
|
content={
|
||||||
|
<Skeleton loading={loadingData} active>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="Repeats">
|
||||||
|
<Select
|
||||||
|
value={repeatOption.value}
|
||||||
|
onChange={val => {
|
||||||
|
const option = repeatOptions.find(opt => opt.value === val);
|
||||||
|
if (option) {
|
||||||
|
setRepeatOption(option);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={repeatOptions}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{repeatOption.value === ITaskRecurring.Weekly && (
|
||||||
|
<Form.Item label="Select Days of the Week">
|
||||||
|
<Checkbox.Group
|
||||||
|
options={daysOfWeek}
|
||||||
|
value={selectedDays}
|
||||||
|
onChange={handleDayCheckboxChange}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Row>
|
||||||
|
{daysOfWeek.map(day => (
|
||||||
|
<Col span={8} key={day.value}>
|
||||||
|
<Checkbox value={day.value}>{day.label}</Checkbox>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMonthlySelected && (
|
||||||
|
<>
|
||||||
|
<Form.Item label="Monthly repeat type">
|
||||||
|
<Radio.Group
|
||||||
|
value={monthlyOption}
|
||||||
|
onChange={e => setMonthlyOption(e.target.value)}
|
||||||
|
>
|
||||||
|
<Radio.Button value="date">On a specific date</Radio.Button>
|
||||||
|
<Radio.Button value="day">On a specific day</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{monthlyOption === 'date' && (
|
||||||
|
<Form.Item label="Date of the month">
|
||||||
|
<Select
|
||||||
|
value={selectedMonthlyDate}
|
||||||
|
onChange={setSelectedMonthlyDate}
|
||||||
|
options={monthlyDateOptions.map(date => ({ label: date.toString(), value: date }))}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{monthlyOption === 'day' && (
|
||||||
|
<>
|
||||||
|
<Form.Item label="Week of the month">
|
||||||
|
<Select
|
||||||
|
value={selectedMonthlyWeek}
|
||||||
|
onChange={setSelectedMonthlyWeek}
|
||||||
|
options={weekOptions}
|
||||||
|
style={{ width: 150 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Day of the week">
|
||||||
|
<Select
|
||||||
|
value={selectedMonthlyDay}
|
||||||
|
onChange={setSelectedMonthlyDay}
|
||||||
|
options={dayOptions}
|
||||||
|
style={{ width: 150 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{repeatOption.value === ITaskRecurring.EveryXDays && (
|
||||||
|
<Form.Item label="Interval (days)">
|
||||||
|
<InputNumber min={1} value={intervalDays} onChange={(value) => value && setIntervalDays(value)} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{repeatOption.value === ITaskRecurring.EveryXWeeks && (
|
||||||
|
<Form.Item label="Interval (weeks)">
|
||||||
|
<InputNumber min={1} value={intervalWeeks} onChange={(value) => value && setIntervalWeeks(value)} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{repeatOption.value === ITaskRecurring.EveryXMonths && (
|
||||||
|
<Form.Item label="Interval (months)">
|
||||||
|
<InputNumber min={1} value={intervalMonths} onChange={(value) => value && setIntervalMonths(value)} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
loading={updatingData}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Skeleton>
|
||||||
|
}
|
||||||
|
overlayStyle={{ width: 510 }}
|
||||||
|
open={showConfig}
|
||||||
|
onOpenChange={configVisibleChange}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<Button type="link" loading={loadingData} style={{ padding: 0 }}>
|
||||||
|
{repeatOption.label} <SettingOutlined />
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDrawerRecurringConfig;
|
||||||
@@ -29,6 +29,7 @@ import TaskDrawerBillable from './details/task-drawer-billable/task-drawer-billa
|
|||||||
import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress';
|
import TaskDrawerProgress from './details/task-drawer-progress/task-drawer-progress';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
|
import TaskDrawerRecurringConfig from './details/task-drawer-recurring-config/task-drawer-recurring-config';
|
||||||
|
|
||||||
interface TaskDetailsFormProps {
|
interface TaskDetailsFormProps {
|
||||||
taskFormViewModel?: ITaskFormViewModel | null;
|
taskFormViewModel?: ITaskFormViewModel | null;
|
||||||
@@ -175,6 +176,10 @@ const TaskDetailsForm = ({ taskFormViewModel = null }: TaskDetailsFormProps) =>
|
|||||||
<TaskDrawerBillable task={taskFormViewModel?.task as ITaskViewModel} />
|
<TaskDrawerBillable task={taskFormViewModel?.task as ITaskViewModel} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="recurring" label={t('taskInfoTab.details.recurring')}>
|
||||||
|
<TaskDrawerRecurringConfig task={taskFormViewModel?.task as ITaskViewModel} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="notify" label={t('taskInfoTab.details.notify')}>
|
<Form.Item name="notify" label={t('taskInfoTab.details.notify')}>
|
||||||
<NotifyMemberSelector task={taskFormViewModel?.task as ITaskViewModel} t={t} />
|
<NotifyMemberSelector task={taskFormViewModel?.task as ITaskViewModel} t={t} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
19
worklenz-frontend/src/types/tasks/task-recurring-schedule.ts
Normal file
19
worklenz-frontend/src/types/tasks/task-recurring-schedule.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export interface ITaskRecurringSchedule {
|
||||||
|
type: 'daily' | 'weekly' | 'monthly' | 'interval';
|
||||||
|
dayOfWeek?: number; // 0 = Sunday, 1 = Monday, ..., 6 = Saturday (for weekly tasks)
|
||||||
|
dayOfMonth?: number; // 1 - 31 (for monthly tasks)
|
||||||
|
weekOfMonth?: number; // 1 = 1st week, 2 = 2nd week, ..., 5 = Last week (for monthly tasks)
|
||||||
|
hour: number; // Time of the day in 24-hour format
|
||||||
|
minute: number; // Minute of the hour
|
||||||
|
interval?: {
|
||||||
|
days?: number; // Interval in days (for every x days)
|
||||||
|
weeks?: number; // Interval in weeks (for every x weeks)
|
||||||
|
months?: number; // Interval in months (for every x months)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITaskRecurringScheduleData {
|
||||||
|
task_id?: string,
|
||||||
|
id?: string,
|
||||||
|
schedule_type?: string
|
||||||
|
}
|
||||||
@@ -64,6 +64,7 @@ export interface ITaskViewModel extends ITask {
|
|||||||
timer_start_time?: number;
|
timer_start_time?: number;
|
||||||
recurring?: boolean;
|
recurring?: boolean;
|
||||||
task_level?: number;
|
task_level?: number;
|
||||||
|
schedule_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITaskTeamMember extends ITeamMember {
|
export interface ITaskTeamMember extends ITeamMember {
|
||||||
|
|||||||
Reference in New Issue
Block a user