Skip to content
Merged
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
408 changes: 408 additions & 0 deletions src/components/BotChat/QuickVoiceAdd.tsx

Large diffs are not rendered by default.

129 changes: 121 additions & 8 deletions src/components/Calendar/CalendarView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, ScrollView, StyleSheet, Alert, ActivityIndicator, Dimensions } from 'react-native';
import { View, ScrollView, StyleSheet, Alert, ActivityIndicator, Modal, Pressable, TouchableOpacity } from 'react-native';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import { Task as TaskType, getAllTasks, addTask, deleteTask, updateTask, completeTask, disCompleteTask } from '../../services/taskService';
Expand All @@ -12,7 +12,7 @@ import { useFocusEffect } from '@react-navigation/native';
import CalendarGrid from './CalendarGrid';
import Task from '../Task/Task';
import AddTask from '../Task/AddTask';
import AddTaskButton from '../Task/AddTaskButton';
import QuickVoiceAdd from '../BotChat/QuickVoiceAdd';
import { addTaskToList } from '../TaskList/types';
import { LoadingState, EmptyState, StatusChip, AppText } from '../UI/foundation';

Expand All @@ -26,12 +26,10 @@ const CalendarView: React.FC = () => {
const [selectedDate, setSelectedDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
const [tasks, setTasks] = useState<TaskType[]>([]);
const [showAddTask, setShowAddTask] = useState(false);
const [voiceAddModalVisible, setVoiceAddModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);

// Screen dimensions
const screenWidth = Dimensions.get('window').width;

// Servizi
const cacheService = useRef(TaskCacheService.getInstance()).current;
const syncManager = useRef(SyncManager.getInstance()).current;
Expand Down Expand Up @@ -355,6 +353,65 @@ const CalendarView: React.FC = () => {
setShowAddTask(true);
};

const handleVoiceAddSuccess = () => {
fetchTasks();
setTimeout(() => {
setVoiceAddModalVisible(false);
}, 1150);
};
Comment on lines +356 to +361

const renderHeaderActions = () => (
<View style={styles.headerActionDock}>
<TouchableOpacity
style={styles.manualAddButton}
onPress={handleAddTask}
activeOpacity={0.82}
accessibilityRole="button"
accessibilityLabel={t("tasks.accessibility.addTaskLabel")}
accessibilityHint={t("tasks.accessibility.addTaskHint")}
>
<Ionicons name="create-outline" size={22} color="#000000" />
</TouchableOpacity>

<TouchableOpacity
style={styles.voiceTriggerButton}
onPress={() => setVoiceAddModalVisible(true)}
activeOpacity={0.82}
accessibilityRole="button"
accessibilityLabel={t("tasks.accessibility.addTaskLabel")}
accessibilityHint={t("tasks.accessibility.addTaskHint")}
>
<Ionicons name="add" size={28} color="#ffffff" />
</TouchableOpacity>
</View>
);

const renderVoiceAddModal = () => (
<Modal
visible={voiceAddModalVisible}
transparent
animationType="fade"
onRequestClose={() => setVoiceAddModalVisible(false)}
>
<View style={styles.voiceModalOverlay}>
<Pressable
style={StyleSheet.absoluteFill}
onPress={() => setVoiceAddModalVisible(false)}
/>
<View style={styles.voiceModalContent} pointerEvents="box-none">
<QuickVoiceAdd
model="base"
variant="fab"
autoStart
onSuccess={handleVoiceAddSuccess}
onCancel={() => setVoiceAddModalVisible(false)}
containerStyle={styles.voiceAddFab}
/>
</View>
</View>
</Modal>
);

// Gestisce la chiusura del form
const handleCloseAddTask = () => {
setShowAddTask(false);
Expand Down Expand Up @@ -421,7 +478,7 @@ const CalendarView: React.FC = () => {
{t('calendar.commitmentsOf')} {dayjs(selectedDate).format('DD')} {t(`calendar.months.${MONTH_KEYS[dayjs(selectedDate).month()]}`)} {dayjs(selectedDate).year()}
</AppText>
</View>
<AddTaskButton onPress={handleAddTask} screenWidth={screenWidth} isInline={true} />
{renderHeaderActions()}
</View>

{/* Componente di caricamento */}
Expand All @@ -436,6 +493,8 @@ const CalendarView: React.FC = () => {
categoryName={t('calendar.defaultCategory')}
initialDate={selectedDate}
/>

{renderVoiceAddModal()}
</View>
);
}
Expand Down Expand Up @@ -475,7 +534,7 @@ const CalendarView: React.FC = () => {
</View>
)}
</View>
<AddTaskButton onPress={handleAddTask} screenWidth={screenWidth} />
{renderHeaderActions()}
</View>

<ScrollView style={styles.taskList}>
Expand All @@ -499,6 +558,8 @@ const CalendarView: React.FC = () => {
)}
</ScrollView>

{renderVoiceAddModal()}

{/* Componente AddTask con selezione categorie abilitata */}
<AddTask
visible={showAddTask}
Expand Down Expand Up @@ -527,6 +588,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 5,
zIndex: 40,
},
titleContainer: {
flex: 1,
Expand All @@ -545,6 +607,57 @@ const styles = StyleSheet.create({
noTasksContainer: {
marginTop: 60,
},
headerActionDock: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 10,
minWidth: 110,
},
voiceAddFab: {
marginTop: 0,
},
voiceTriggerButton: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: '#000000',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 12,
elevation: 3,
},
manualAddButton: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#ffffff',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e1e5e9',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 12,
elevation: 3,
},
voiceModalOverlay: {
flex: 1,
backgroundColor: 'rgba(18, 18, 18, 0.18)',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
},
voiceModalContent: {
minWidth: 292,
minHeight: 80,
alignItems: 'center',
justifyContent: 'center',
},
addButton: {
backgroundColor: "#000000",
width: 56,
Expand Down Expand Up @@ -578,4 +691,4 @@ const styles = StyleSheet.create({
},
});

export default CalendarView;
export default CalendarView;
12 changes: 11 additions & 1 deletion src/components/Calendar20/Calendar20View.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const Calendar20View: React.FC<Calendar20ViewProps> = ({ onClose }) => {
const [searchVisible, setSearchVisible] = useState(false);
const [miniCalendarVisible, setMiniCalendarVisible] = useState(false);
const [addTaskVisible, setAddTaskVisible] = useState(false);
const [voiceAddActive, setVoiceAddActive] = useState(false);
const [selectedDateForTask, setSelectedDateForTask] = useState<dayjs.Dayjs | null>(null);

const cacheService = useRef(TaskCacheService.getInstance()).current;
Expand Down Expand Up @@ -298,7 +299,7 @@ const Calendar20View: React.FC<Calendar20ViewProps> = ({ onClose }) => {
}
setAddTaskVisible(false);
setSelectedDateForTask(null);
}, [currentDate, selectedDateForTask]);
}, [currentDate, selectedDateForTask, t]);

const handleCategoryToggle = useCallback((categoryName: string) => {
setEnabledCategories(prev => {
Expand Down Expand Up @@ -370,11 +371,15 @@ const Calendar20View: React.FC<Calendar20ViewProps> = ({ onClose }) => {

{renderView()}

{voiceAddActive && <View style={styles.voiceBackdrop} />}

<FABMenu
onNewTask={() => {
setSelectedDateForTask(null);
setAddTaskVisible(true);
}}
onVoiceStateChange={setVoiceAddActive}
onVoiceSuccess={fetchTasks}
/>

<ViewSelector
Expand Down Expand Up @@ -430,6 +435,11 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#ffffff',
},
voiceBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(18, 18, 18, 0.18)',
zIndex: 180,
},
});

export default Calendar20View;
48 changes: 37 additions & 11 deletions src/components/Calendar20/FABMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,68 @@ import React from 'react';
import {
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import QuickVoiceAdd from '../BotChat/QuickVoiceAdd';

interface FABMenuProps {
onNewTask: () => void;
onVoiceStateChange?: (isActive: boolean) => void;
onVoiceSuccess?: () => void;
}

const FABMenu: React.FC<FABMenuProps> = ({ onNewTask }) => {
const FABMenu: React.FC<FABMenuProps> = ({ onNewTask, onVoiceStateChange, onVoiceSuccess }) => {
return (
<TouchableOpacity
style={styles.fab}
onPress={onNewTask}
activeOpacity={0.8}
>
<Ionicons name="add" size={28} color="#ffffff" />
</TouchableOpacity>
<View pointerEvents="box-none" style={styles.dock}>
<TouchableOpacity
style={styles.manualFab}
onPress={onNewTask}
activeOpacity={0.82}
accessibilityRole="button"
accessibilityLabel="Aggiungi manualmente"
>
Comment on lines +19 to +25
<Ionicons name="create-outline" size={22} color="#000000" />
</TouchableOpacity>

<QuickVoiceAdd
model="base"
variant="fab"
onSuccess={onVoiceSuccess}
onStateChange={(state) => onVoiceStateChange?.(state !== 'idle')}
containerStyle={styles.voiceFab}
/>
</View>
);
};

const styles = StyleSheet.create({
fab: {
dock: {
position: 'absolute',
bottom: 20,
right: 20,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
zIndex: 220,
},
voiceFab: {
marginTop: 0,
},
manualFab: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: '#000000',
backgroundColor: '#ffffff',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#e5e5ea',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 12,
elevation: 3,
zIndex: 100,
},
});

Expand Down
28 changes: 26 additions & 2 deletions src/components/TaskList/TaskListContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import React, { useState, useEffect, useMemo, useCallback, useLayoutEffect } fro
import { View, ScrollView, Alert, TouchableOpacity } from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { styles } from './styles';
import { Task as TaskType, globalTasksRef } from './types';
import eventEmitter, { EVENTS } from '../../utils/eventEmitter';
import { ActiveFilters } from './ActiveFilters';
import { FilterModal } from './FilterModal';
import { AddTaskButton } from './AddTaskButton';
import { filterTasksByDay } from './TaskUtils';
import AddTask from '../Task/AddTask';
import QuickVoiceAdd from '../BotChat/QuickVoiceAdd';
import { recurringTaskService, RecurringTask, CreateRecurringTaskPayload } from '../../services/recurringTaskService';
import { LoadingState, EmptyState } from '../UI/foundation';
import { CompletedTasksButton } from './CompletedTasksButton';
Expand Down Expand Up @@ -41,6 +42,7 @@ export const TaskListContainer = ({
taskService
}: TaskListContainerProps) => {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [tasks, setTasks] = useState<TaskType[]>([]);
const [filtroImportanza, setFiltroImportanza] = useState("Tutte");
const [filtroScadenza, setFiltroScadenza] = useState("Tutte");
Expand Down Expand Up @@ -652,7 +654,29 @@ export const TaskListContainer = ({
onDeleteAll={handleDeleteAllCompleted}
/>

<AddTaskButton onPress={toggleForm} />
<View
pointerEvents="box-none"
style={[styles.voiceAddDock, { bottom: 20 + insets.bottom }]}
>
<TouchableOpacity
style={styles.manualAddButton}
onPress={toggleForm}
activeOpacity={0.82}
accessibilityRole="button"
accessibilityLabel={t("tasks.accessibility.addTaskLabel")}
accessibilityHint={t("tasks.accessibility.addTaskHint")}
>
<Ionicons name="create-outline" size={22} color="#000000" />
</TouchableOpacity>

<QuickVoiceAdd
model="base"
variant="fab"
disabled={!isOwned && permissionLevel === "READ_ONLY"}
onSuccess={fetchTasks}
containerStyle={styles.voiceAddFab}
/>
</View>

<AddTask
visible={formVisible}
Expand Down
Loading
Loading