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
3 changes: 3 additions & 0 deletions mobile/docs/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ Decisao atualizada em 2026-04-07:
- Ajuste de UX em 2026-04-12:
- remover alerts nativos de sucesso apos confirmar presenca
- o feedback principal passa a ser a atualizacao imediata do CTA e do status na propria tela
- Ajuste de UX em 2026-05-25:
- encurtar o CTA principal para `Confirmar` / `Confirmado`, evitando quebra desigual dos botoes em `ScheduleScreen` e `EditScheduleScreen`

- [~] Solicitar troca (membro)
- Decisao atualizada em 2026-04-10:
Expand Down Expand Up @@ -302,6 +304,7 @@ Decisao atualizada em 2026-04-07:
- `room_reservations.event_id` passa a ser o vinculo estrutural opcional entre evento e sala
- `save_event_with_optional_room_reservation` salva evento + audiencia + reserva opcional em transacao
- `CreateEventScreen` agora permite sala opcional com disponibilidade real por janela para evento de data unica
- `CreateEventScreen` agora mantem data unica por padrao e so habilita multiplas datas quando o usuario marca explicitamente `Permitir multiplas datas` no modal do calendario
- `RoomsScreen` deixou de ser mock e passou a criar reservas independentes reais
- a reconciliacao de sala em edicao protege contra limpeza indevida ao mudar e voltar janela/horario

Expand Down
2 changes: 1 addition & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"web": "expo start --web",
"test": "npm run test:unit",
"test:build": "tsc -p tsconfig.test.json",
"test:unit": "npm run test:build && node .tests-dist/tests/eventDate.test.js && node .tests-dist/tests/eventCategory.test.js && node .tests-dist/tests/eventAudience.test.js && node .tests-dist/tests/eventPermissions.test.js && node .tests-dist/tests/eventPresentation.test.js && node .tests-dist/tests/roomReservationService.test.js && node .tests-dist/tests/eventService.test.js && node .tests-dist/tests/musicService.test.js && node .tests-dist/tests/eventFilters.test.js && node .tests-dist/tests/statusLabels.test.js && node .tests-dist/tests/formatDate.test.js && node .tests-dist/tests/scheduleParticipation.test.js && node .tests-dist/tests/scheduleRules.test.js && node .tests-dist/tests/ministryMappers.test.js && node .tests-dist/tests/scheduleCardMappers.test.js && node .tests-dist/tests/scheduleService.test.js && node .tests-dist/tests/blockedDateService.test.js && node .tests-dist/tests/ministryService.test.js && node .tests-dist/tests/profileService.test.js && node .tests-dist/tests/ministryStore.test.js && node .tests-dist/tests/scheduleStore.test.js && node .tests-dist/tests/migrationIntegrity.test.js && node .tests-dist/tests/audienceResults.test.js && node .tests-dist/tests/roomAvailability.test.js"
"test:unit": "npm run test:build && node .tests-dist/tests/eventDate.test.js && node .tests-dist/tests/eventCalendarSelection.test.js && node .tests-dist/tests/eventCategory.test.js && node .tests-dist/tests/eventAudience.test.js && node .tests-dist/tests/eventPermissions.test.js && node .tests-dist/tests/eventPresentation.test.js && node .tests-dist/tests/roomReservationService.test.js && node .tests-dist/tests/eventService.test.js && node .tests-dist/tests/musicService.test.js && node .tests-dist/tests/eventFilters.test.js && node .tests-dist/tests/statusLabels.test.js && node .tests-dist/tests/formatDate.test.js && node .tests-dist/tests/scheduleParticipation.test.js && node .tests-dist/tests/scheduleRules.test.js && node .tests-dist/tests/ministryMappers.test.js && node .tests-dist/tests/scheduleCardMappers.test.js && node .tests-dist/tests/scheduleService.test.js && node .tests-dist/tests/blockedDateService.test.js && node .tests-dist/tests/ministryService.test.js && node .tests-dist/tests/profileService.test.js && node .tests-dist/tests/ministryStore.test.js && node .tests-dist/tests/scheduleStore.test.js && node .tests-dist/tests/migrationIntegrity.test.js && node .tests-dist/tests/audienceResults.test.js && node .tests-dist/tests/roomAvailability.test.js"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
Expand Down
96 changes: 75 additions & 21 deletions mobile/src/screens/app/CreateEventScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TouchableOpacity as RNTouchableOpacity,
View,
} from "react-native";
import { Button, Chip, Divider, Switch, Text, TextInput } from "react-native-paper";
import { Button, Checkbox, Chip, Divider, Switch, Text, TextInput } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
import { Calendar } from "react-native-calendars";
import { Calendar as CalendarIcon, Clock, Search, Sparkles, X } from "lucide-react-native";
Expand All @@ -34,6 +34,12 @@ import {
} from "../../services/profileService";
import { useAuthStore } from "../../stores/useAuthStore";
import { useEventStore } from "../../stores/useEventStore";
import {
collapseCalendarSelectionToSingleDate,
createCalendarSelectionMark,
type CalendarSelectionMark,
toggleCalendarDateSelection,
} from "../../utils/eventCalendarSelection";
import {
createLocalDateTime,
formatLocalDateKey,
Expand Down Expand Up @@ -87,12 +93,7 @@ const PRESETS = [
},
];

type CalendarSelection = {
selected: boolean;
selectedColor: string;
};

function getEventDateLabel(selectedDays: Record<string, CalendarSelection>) {
function getEventDateLabel(selectedDays: Record<string, CalendarSelectionMark>) {
const dates = Object.keys(selectedDays);

if (dates.length === 0) return "Selecione a data";
Expand Down Expand Up @@ -228,9 +229,10 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
const [audiencePage, setAudiencePage] = useState(0);
const [hasMoreAudience, setHasMoreAudience] = useState(false);
const [showCalendar, setShowCalendar] = useState(false);
const [selectedDays, setSelectedDays] = useState<Record<string, CalendarSelection>>(() => {
const [allowMultipleDates, setAllowMultipleDates] = useState(false);
const [selectedDays, setSelectedDays] = useState<Record<string, CalendarSelectionMark>>(() => {
const todayKey = formatLocalDateKey(getNow());
return { [todayKey]: { selected: true, selectedColor: "#000" } };
return { [todayKey]: createCalendarSelectionMark() };
});
const [rooms, setRooms] = useState<RoomAvailability[]>([]);
const [isLoadingRooms, setIsLoadingRooms] = useState(false);
Expand All @@ -241,6 +243,7 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
const [isRoomSelectionAutoCleared, setIsRoomSelectionAutoCleared] = useState(false);

const selectedDateKeys = useMemo(() => Object.keys(selectedDays), [selectedDays]);
const isMultipleDateSelectionEnabled = !isEdit && allowMultipleDates;
const singleSelectedDateKey =
selectedDateKeys.length === 1 ? selectedDateKeys[0] : null;
const roomWindow = useMemo(
Expand All @@ -267,7 +270,8 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
setTime(`${hours}:${minutes}`);

const dateKey = event.start_at.split("T")[0];
setSelectedDays({ [dateKey]: { selected: true, selectedColor: "#000" } });
setAllowMultipleDates(false);
setSelectedDays({ [dateKey]: createCalendarSelectionMark() });
};

useEffect(() => {
Expand Down Expand Up @@ -528,16 +532,30 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
};

const onDayPress = (day: { dateString: string }) => {
const dateString = day.dateString;
const nextSelectedDays = isEdit ? {} : { ...selectedDays };
setSelectedDays((current) =>
toggleCalendarDateSelection({
selectedDays: current,
dateString: day.dateString,
allowMultipleDates: isMultipleDateSelectionEnabled,
isEdit,
}),
);
};

if (nextSelectedDays[dateString]) {
delete nextSelectedDays[dateString];
} else {
nextSelectedDays[dateString] = { selected: true, selectedColor: "#000" };
}
const handleToggleMultipleDates = () => {
if (isEdit) return;

setSelectedDays(nextSelectedDays);
setAllowMultipleDates((current) => {
const nextValue = !current;

if (!nextValue) {
setSelectedDays((previousSelection) =>
collapseCalendarSelectionToSingleDate(previousSelection),
);
}

return nextValue;
});
};

const handleTimeChange = (text: string) => {
Expand Down Expand Up @@ -1407,10 +1425,18 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
>
<View>
<Text style={{ fontWeight: "bold", fontSize: 16 }}>
{isEdit ? "Alterar Data" : "Selecione as Datas"}
{isEdit
? "Alterar Data"
: isMultipleDateSelectionEnabled
? "Selecione as Datas"
: "Selecione a Data"}
</Text>
<Text style={{ fontSize: 12, color: "#666" }}>
{isEdit ? "Toque para escolher um novo dia" : "Toque para selecionar vários dias"}
{isEdit
? "Toque para escolher um novo dia"
: isMultipleDateSelectionEnabled
? "Toque para selecionar ou remover várias datas"
: "Por padrão, apenas uma data fica ativa por vez"}
</Text>
</View>

Expand All @@ -1419,6 +1445,32 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
</RNTouchableOpacity>
</View>

{!isEdit ? (
<RNTouchableOpacity
onPress={handleToggleMultipleDates}
activeOpacity={0.8}
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 10,
paddingBottom: 10,
}}
>
<View style={{ flex: 1, paddingRight: 12 }}>
<Text style={{ fontWeight: "700", color: "#111827" }}>
Permitir múltiplas datas
</Text>
<Text style={{ fontSize: 12, color: "#6b7280", marginTop: 2 }}>
Desativado por padrão para manter a criação mais simples.
</Text>
</View>
<Checkbox
status={isMultipleDateSelectionEnabled ? "checked" : "unchecked"}
/>
</RNTouchableOpacity>
) : null}

<Calendar
onDayPress={onDayPress}
markedDates={selectedDays}
Expand All @@ -1436,7 +1488,9 @@ export default function CreateEventScreen({ route }: CreateEventScreenProps) {
onPress={() => setShowCalendar(false)}
style={{ marginTop: 10, backgroundColor: "#000" }}
>
Confirmar {selectedDateKeys.length} data(s)
{isMultipleDateSelectionEnabled && selectedDateKeys.length > 1
? `Confirmar ${selectedDateKeys.length} datas`
: "Confirmar data"}
</Button>
</View>
</RNTouchableOpacity>
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/screens/app/EditScheduleScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ export default function EditScheduleScreen() {
isLoading={isConfirmingPresence}
disabled={isOwnParticipationReadOnly || !hasPendingOwnAssignments}
>
{hasPendingOwnAssignments ? "Confirmar presenca" : "Presenca confirmada"}
{hasPendingOwnAssignments ? "Confirmar" : "Confirmado"}
</DefaultButton>
</View>
</View>
Expand Down
2 changes: 1 addition & 1 deletion mobile/src/screens/app/ScheduleScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export default function ScheduleScreen() {
showActions={showOwnActions}
swapLabel={hasPendingOwnSwapRequest ? "Cancelar troca" : "Preciso trocar"}
swapVariant={hasPendingOwnSwapRequest ? "destructive" : "outline"}
confirmLabel={hasPendingOwnAssignments ? "Confirmar presenca" : "Presenca confirmada"}
confirmLabel={hasPendingOwnAssignments ? "Confirmar" : "Confirmado"}
confirmDisabled={confirmDisabled}
swapDisabled={swapDisabled}
actionHint={actionHint}
Expand Down
52 changes: 52 additions & 0 deletions mobile/src/utils/eventCalendarSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export type CalendarSelectionMark = {
selected: boolean;
selectedColor: string;
};

const DEFAULT_SELECTION_COLOR = "#000";

export function createCalendarSelectionMark(): CalendarSelectionMark {
return {
selected: true,
selectedColor: DEFAULT_SELECTION_COLOR,
};
}

export function toggleCalendarDateSelection(input: {
selectedDays: Record<string, CalendarSelectionMark>;
dateString: string;
allowMultipleDates: boolean;
isEdit?: boolean;
}) {
const { selectedDays, dateString, allowMultipleDates, isEdit = false } = input;

if (!allowMultipleDates || isEdit) {
return {
[dateString]: createCalendarSelectionMark(),
};
}

const nextSelectedDays = { ...selectedDays };

if (nextSelectedDays[dateString]) {
delete nextSelectedDays[dateString];
return nextSelectedDays;
}

nextSelectedDays[dateString] = createCalendarSelectionMark();
return nextSelectedDays;
}

export function collapseCalendarSelectionToSingleDate(
selectedDays: Record<string, CalendarSelectionMark>,
) {
const firstDate = Object.keys(selectedDays).sort()[0];

if (!firstDate) {
return {};
}

return {
[firstDate]: createCalendarSelectionMark(),
};
}
73 changes: 73 additions & 0 deletions mobile/tests/eventCalendarSelection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
collapseCalendarSelectionToSingleDate,
createCalendarSelectionMark,
toggleCalendarDateSelection,
} from "../src/utils/eventCalendarSelection";

test("toggleCalendarDateSelection keeps a single selected date by default", () => {
const result = toggleCalendarDateSelection({
selectedDays: {
"2026-05-23": createCalendarSelectionMark(),
},
dateString: "2026-05-25",
allowMultipleDates: false,
});

assert.deepEqual(result, {
"2026-05-25": createCalendarSelectionMark(),
});
});

test("toggleCalendarDateSelection adds and removes dates when multiple selection is enabled", () => {
const addedResult = toggleCalendarDateSelection({
selectedDays: {
"2026-05-23": createCalendarSelectionMark(),
},
dateString: "2026-05-25",
allowMultipleDates: true,
});

assert.deepEqual(addedResult, {
"2026-05-23": createCalendarSelectionMark(),
"2026-05-25": createCalendarSelectionMark(),
});

const removedResult = toggleCalendarDateSelection({
selectedDays: addedResult,
dateString: "2026-05-23",
allowMultipleDates: true,
});

assert.deepEqual(removedResult, {
"2026-05-25": createCalendarSelectionMark(),
});
});

test("toggleCalendarDateSelection still behaves as single selection during edit", () => {
const result = toggleCalendarDateSelection({
selectedDays: {
"2026-05-23": createCalendarSelectionMark(),
},
dateString: "2026-05-25",
allowMultipleDates: true,
isEdit: true,
});

assert.deepEqual(result, {
"2026-05-25": createCalendarSelectionMark(),
});
});

test("collapseCalendarSelectionToSingleDate keeps the earliest selected date", () => {
const result = collapseCalendarSelectionToSingleDate({
"2026-05-30": createCalendarSelectionMark(),
"2026-05-25": createCalendarSelectionMark(),
"2026-05-27": createCalendarSelectionMark(),
});

assert.deepEqual(result, {
"2026-05-25": createCalendarSelectionMark(),
});
});