fix(C103): reaplica timeVisible/allDay do C104 perdidos no ciclo de rebase

This commit is contained in:
fsolla 2026-08-10 03:22:21 -03:00 committed by Francisco Solla
parent 437b5f0c2f
commit 5abc0e0881
2 changed files with 96 additions and 38 deletions

View file

@ -18,6 +18,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover
import {
floorToMinuteStep,
formatBahiaCivilDate,
formatBahiaCivilDateLabel,
formatBahiaCivilDateTimeLabel,
hourOptions,
minuteOptionsForStep,
@ -37,6 +38,9 @@ import { cn } from '@/lib/utils'
* C103 — on narrow viewports the picker opens as a nested bottom sheet
* (calendar + hour/minute fully on screen, "Pronto" applies and closes),
* keeping the desktop popover untouched.
*
* C104 — `timeVisible={false}` is the all-day mode: the trigger formats only
* the date and the picker renders just the calendar (no time selects).
*/
export const ActivityDateTimeField = ({
id,
@ -47,6 +51,7 @@ export const ActivityDateTimeField = ({
isNarrow = false,
label,
required = false,
timeVisible = true,
}: {
id: string
value: string
@ -60,6 +65,8 @@ export const ActivityDateTimeField = ({
label?: string
/** Marks the required fields with a visible asterisk on the narrow trigger. */
required?: boolean
/** When false, hide the time selects and label the trigger with the date only. */
timeVisible?: boolean
}) => {
const [open, setOpen] = useState(false)
const civil = floorToMinuteStep(value)
@ -96,7 +103,9 @@ export const ActivityDateTimeField = ({
'border-destructive ring-3 ring-destructive/20 focus-visible:border-destructive focus-visible:ring-destructive/20',
)}
>
<span className="truncate">{formatBahiaCivilDateTimeLabel(civil)}</span>
<span className="truncate">
{timeVisible ? formatBahiaCivilDateTimeLabel(civil) : formatBahiaCivilDateLabel(civil)}
</span>
{isNarrow && required ? (
<span aria-hidden="true" className="font-semibold text-destructive">
*
@ -122,38 +131,40 @@ export const ActivityDateTimeField = ({
className="w-full p-2"
/>
) : null}
<div className="flex flex-col gap-3 border-t p-3">
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground select-none">Hora</span>
<NativeSelect
aria-label="Hora"
value={hour}
onChange={(event) => setHour(event.target.value)}
>
{hourOptions.map((option) => (
<NativeSelectOption key={option} value={option}>
{option}h
</NativeSelectOption>
))}
</NativeSelect>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground select-none">Minuto</span>
<NativeSelect
aria-label="Minuto"
value={minute}
onChange={(event) => setMinute(event.target.value)}
>
{minuteOptionsForStep().map((option) => (
<NativeSelectOption key={option} value={option}>
{option}
</NativeSelectOption>
))}
</NativeSelect>
{timeVisible ? (
<div className="flex flex-col gap-3 border-t p-3">
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground select-none">Hora</span>
<NativeSelect
aria-label="Hora"
value={hour}
onChange={(event) => setHour(event.target.value)}
>
{hourOptions.map((option) => (
<NativeSelectOption key={option} value={option}>
{option}h
</NativeSelectOption>
))}
</NativeSelect>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs font-medium text-muted-foreground select-none">Minuto</span>
<NativeSelect
aria-label="Minuto"
value={minute}
onChange={(event) => setMinute(event.target.value)}
>
{minuteOptionsForStep().map((option) => (
<NativeSelectOption key={option} value={option}>
{option}
</NativeSelectOption>
))}
</NativeSelect>
</div>
</div>
</div>
</div>
) : null}
</>
)

View file

@ -13,6 +13,7 @@ import { ResponsibleMultiSelect } from '@/components/campaign/shared/Responsible
import { StrictCombobox } from '@/components/campaign/shared/StrictCombobox'
import { Alert, AlertDescription } from '@/components/ui/Alert'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/Checkbox'
import {
Drawer,
DrawerContent,
@ -24,6 +25,7 @@ import { Field, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/Popover'
import { Spinner } from '@/components/ui/Spinner'
import { allDayEndInstant, allDayRangeValid, allDayStartInstant } from '@/lib/activityAllDay'
import {
formatBahiaDateTimeLabel,
formatIsoAsBahiaDateTimeInput,
@ -113,6 +115,8 @@ type InlineDateTimeFieldProps = {
invalid: boolean
error?: string
required?: boolean
/** C104 — false hides the time selects (all-day mode). */
timeVisible?: boolean
}
const InlineDateTimeField = ({
@ -124,6 +128,7 @@ const InlineDateTimeField = ({
invalid,
error,
required = false,
timeVisible = true,
}: InlineDateTimeFieldProps) => (
<Field data-invalid={invalid}>
<FieldLabel htmlFor={id} className={sheet ? 'sr-only' : undefined}>
@ -141,6 +146,7 @@ const InlineDateTimeField = ({
isNarrow={sheet}
label={label}
required={required}
timeVisible={timeVisible}
/>
{error ? <FieldError id={`${id}-error`}>{error}</FieldError> : null}
</Field>
@ -167,6 +173,7 @@ const ActivityInlineCreateForm = ({
const [title, setTitle] = useState('')
const [start, setStart] = useState(formatIsoAsBahiaDateTimeInput(startAt))
const [end, setEnd] = useState(formatIsoAsBahiaDateTimeInput(endAt))
const [allDay, setAllDay] = useState(false)
const [locality, setLocality] = useState('')
const [tags, setTags] = useState<string[]>([])
const preselectedMunicipalityId =
@ -181,12 +188,19 @@ const ActivityInlineCreateForm = ({
const errorFor = (name: string) => fieldErrors[name]?.[0]
const allDayStartDate = start.slice(0, 10)
const allDayEndDate = end ? end.slice(0, 10) : allDayStartDate
// C105 — the href mirrors the LIVE state (times edited in the sheet, typed
// tags), not the slot draft, so "Mais detalhes" carries everything the user
// actually filled.
const moreDetailsHref = buildActivityCreateHref(agendaState, {
startAt: parseBahiaDateTimeInput(start) ?? undefined,
endAt: end ? (parseBahiaDateTimeInput(end) ?? undefined) : undefined,
...(allDay
? { allDay: true, startAt: allDayStartDate, endAt: allDayEndDate }
: {
startAt: parseBahiaDateTimeInput(start) ?? undefined,
endAt: end ? (parseBahiaDateTimeInput(end) ?? undefined) : undefined,
}),
municipalityId: municipalityValue ? Number(municipalityValue) : undefined,
title: title.trim() || undefined,
tags: tags.length > 0 ? tags : undefined,
@ -207,9 +221,17 @@ const ActivityInlineCreateForm = ({
nextFieldErrors.title = ['Informe um título com ao menos 2 caracteres.']
}
if (!municipalityId) nextFieldErrors.municipality = ['Informe o município.']
if (!startIso) nextFieldErrors.startAt = ['Informe a data e horário de início do compromisso.']
if (startIso && endIso && new Date(endIso) <= new Date(startIso)) {
nextFieldErrors.endAt = ['O horário de término deve ser posterior ao de início.']
if (allDay) {
if (!startIso) nextFieldErrors.startAt = ['Informe a data de início do compromisso.']
if (startIso && endIso && !allDayRangeValid(startIso, endIso)) {
nextFieldErrors.endAt = ['A data de término deve ser igual ou posterior à de início.']
}
} else {
if (!startIso)
nextFieldErrors.startAt = ['Informe a data e horário de início do compromisso.']
if (startIso && endIso && new Date(endIso) <= new Date(startIso)) {
nextFieldErrors.endAt = ['O horário de término deve ser posterior ao de início.']
}
}
if (Object.keys(nextFieldErrors).length > 0) {
setFieldErrors(nextFieldErrors)
@ -224,8 +246,16 @@ const ActivityInlineCreateForm = ({
const result = await createActivityInline({
title: trimmedTitle,
municipality: municipalityId,
startAt: startIso as string,
...(endIso ? { endAt: endIso } : {}),
...(allDay
? {
allDay: true,
startAt: allDayStartInstant(allDayStartDate),
...(end ? { endAt: allDayEndInstant(allDayEndDate) } : {}),
}
: {
startAt: startIso as string,
...(endIso ? { endAt: endIso } : {}),
}),
...(locality.trim() ? { locality: locality.trim() } : {}),
...(responsible ? { responsible } : {}),
// C105 — the tags mirror state (every chip mutation fires `onChange`),
@ -259,6 +289,7 @@ const ActivityInlineCreateForm = ({
invalid={Boolean(errorFor('startAt'))}
error={errorFor('startAt')}
required
timeVisible={!allDay}
/>
<InlineDateTimeField
sheet={sheet}
@ -268,6 +299,7 @@ const ActivityInlineCreateForm = ({
onValueChange={setEnd}
invalid={Boolean(errorFor('endAt'))}
error={errorFor('endAt')}
timeVisible={!allDay}
/>
</>
)
@ -307,6 +339,21 @@ const ActivityInlineCreateForm = ({
) : null}
</Field>
{/* C104 — all-day toggle: hides the time part of Início/Término. */}
<Field
orientation="horizontal"
className={
sheet ? 'min-h-11 gap-2 rounded-none border-0 px-0' : 'min-h-11 rounded-lg border p-3'
}
>
<Checkbox
checked={allDay}
onCheckedChange={(next) => setAllDay(Boolean(next))}
aria-label="Todo o dia"
/>
<span>Todo o dia</span>
</Field>
{sheet ? (
startEndFields
) : (