ajuste visual calendario

This commit is contained in:
Jonas Francisco 2025-11-05 23:58:53 -03:00
parent 93e7b20390
commit 334adb5ba1
2 changed files with 65 additions and 394 deletions

View File

@ -17,6 +17,21 @@ export default function AgendamentoPage() {
const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar"); const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar");
const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]); const [threeDEvents, setThreeDEvents] = useState<CalendarEvent[]>([]);
// Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento)
useEffect(() => {
try {
// Atributos no <html>
document.documentElement.lang = "pt-BR";
document.documentElement.setAttribute("xml:lang", "pt-BR");
document.documentElement.setAttribute("data-lang", "pt-BR");
// Cookie de locale (usado por apps com i18n)
const oneYear = 60 * 60 * 24 * 365;
document.cookie = `NEXT_LOCALE=pt-BR; Path=/; Max-Age=${oneYear}; SameSite=Lax`;
} catch {
// ignore
}
}, []);
// --- NOVO ESTADO --- // --- NOVO ESTADO ---
// Estado para alimentar o NOVO EventManager com dados da API // Estado para alimentar o NOVO EventManager com dados da API
const [managerEvents, setManagerEvents] = useState<Event[]>([]); const [managerEvents, setManagerEvents] = useState<Event[]>([]);

View File

@ -60,16 +60,20 @@ const defaultColors = [
{ name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" }, { name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" },
] ]
// Helper para padrão pt-BR (data e hora)
const formatDateTimeBR = (date?: Date) =>
date ? new Intl.DateTimeFormat("pt-BR", { dateStyle: "short", timeStyle: "short" }).format(date) : ""
export function EventManager({ export function EventManager({
events: initialEvents = [], events: initialEvents = [],
onEventCreate, onEventCreate,
onEventUpdate, onEventUpdate,
onEventDelete, onEventDelete,
categories = ["Meeting", "Task", "Reminder", "Personal"], categories: _categories = ["Meeting", "Task", "Reminder", "Personal"],
colors = defaultColors, colors = defaultColors,
defaultView = "month", defaultView = "month",
className, className,
availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"], availableTags: _availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"],
}: EventManagerProps) { }: EventManagerProps) {
const [events, setEvents] = useState<Event[]>(initialEvents) const [events, setEvents] = useState<Event[]>(initialEvents)
const [currentDate, setCurrentDate] = useState(new Date()) const [currentDate, setCurrentDate] = useState(new Date())
@ -82,10 +86,9 @@ export function EventManager({
title: "", title: "",
description: "", description: "",
color: colors[0].value, color: colors[0].value,
category: categories[0], category: _categories[0],
tags: [], tags: [],
}) })
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [selectedColors, setSelectedColors] = useState<string[]>([]) const [selectedColors, setSelectedColors] = useState<string[]>([])
const [selectedTags, setSelectedTags] = useState<string[]>([]) const [selectedTags, setSelectedTags] = useState<string[]>([])
@ -108,9 +111,7 @@ export function EventManager({
const query = searchQuery.toLowerCase() const query = searchQuery.toLowerCase()
const matchesSearch = const matchesSearch =
event.title.toLowerCase().includes(query) || event.title.toLowerCase().includes(query) ||
event.description?.toLowerCase().includes(query) || event.description?.toLowerCase().includes(query)
event.category?.toLowerCase().includes(query) ||
event.tags?.some((tag) => tag.toLowerCase().includes(query))
if (!matchesSearch) return false if (!matchesSearch) return false
} }
@ -156,7 +157,6 @@ export function EventManager({
color: newEvent.color || colors[0].value, color: newEvent.color || colors[0].value,
category: newEvent.category, category: newEvent.category,
attendees: newEvent.attendees, attendees: newEvent.attendees,
tags: newEvent.tags || [],
} }
setEvents((prev) => [...prev, event]) setEvents((prev) => [...prev, event])
@ -167,10 +167,10 @@ export function EventManager({
title: "", title: "",
description: "", description: "",
color: colors[0].value, color: colors[0].value,
category: categories[0], category: _categories[0],
tags: [], tags: [],
}) })
}, [newEvent, colors, categories, onEventCreate]) }, [newEvent, colors, _categories, onEventCreate])
const handleUpdateEvent = useCallback(() => { const handleUpdateEvent = useCallback(() => {
if (!selectedEvent) return if (!selectedEvent) return
@ -248,24 +248,6 @@ export function EventManager({
[colors], [colors],
) )
const toggleTag = (tag: string, isCreating: boolean) => {
if (isCreating) {
setNewEvent((prev) => ({
...prev,
tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
}))
} else {
setSelectedEvent((prev) =>
prev
? {
...prev,
tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag],
}
: null,
)
}
}
return ( return (
<div className={cn("flex flex-col gap-4", className)}> <div className={cn("flex flex-col gap-4", className)}>
{/* Header */} {/* Header */}
@ -295,9 +277,6 @@ export function EventManager({
<Button variant="outline" size="icon" onClick={() => navigateDate("prev")} className="h-8 w-8"> <Button variant="outline" size="icon" onClick={() => navigateDate("prev")} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())}>
Hoje
</Button>
<Button variant="outline" size="icon" onClick={() => navigateDate("next")} className="h-8 w-8"> <Button variant="outline" size="icon" onClick={() => navigateDate("next")} className="h-8 w-8">
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Button> </Button>
@ -434,123 +413,8 @@ export function EventManager({
</div> </div>
</div> </div>
{/* Mobile: Horizontal scroll with full-length buttons */} {/* Filtro de Cores (único), logo abaixo do input */}
<div className="sm:hidden -mx-4 px-4"> <div className="flex items-center gap-2 pt-2">
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-hide">
{/* Color Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Cores
{selectedColors.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedColors.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
<DropdownMenuSeparator />
{colors.map((color) => (
<DropdownMenuCheckboxItem
key={color.value}
checked={selectedColors.includes(color.value)}
onCheckedChange={(checked) => {
setSelectedColors((prev) =>
checked ? [...prev, color.value] : prev.filter((c) => c !== color.value),
)
}}
>
<div className="flex items-center gap-2">
<div className={cn("h-3 w-3 rounded", color.bg)} />
{color.name}
</div>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Tag Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Tags
{selectedTags.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedTags.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
<DropdownMenuSeparator />
{availableTags.map((tag) => (
<DropdownMenuCheckboxItem
key={tag}
checked={selectedTags.includes(tag)}
onCheckedChange={(checked) => {
setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
}}
>
{tag}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Category Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 whitespace-nowrap flex-shrink-0 bg-transparent">
<Filter className="h-4 w-4" />
Categorias
{selectedCategories.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{selectedCategories.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
<DropdownMenuSeparator />
{categories.map((category) => (
<DropdownMenuCheckboxItem
key={category}
checked={selectedCategories.includes(category)}
onCheckedChange={(checked) => {
setSelectedCategories((prev) =>
checked ? [...prev, category] : prev.filter((c) => c !== category),
)
}}
>
{category}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="gap-2 whitespace-nowrap flex-shrink-0"
>
<X className="h-4 w-4" />
Limpar Filtros
</Button>
)}
</div>
</div>
{/* Desktop: Original layout */}
<div className="hidden sm:flex items-center gap-2">
{/* Color Filter */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent"> <Button variant="outline" size="sm" className="gap-2 bg-transparent">
@ -563,7 +427,7 @@ export function EventManager({
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel> <DropdownMenuLabel>Filtrar por Cor</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{colors.map((color) => ( {colors.map((color) => (
@ -584,69 +448,6 @@ export function EventManager({
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* Tag Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Filter className="h-4 w-4" />
Tags
{selectedTags.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1">
{selectedTags.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Filtrar por Tag</DropdownMenuLabel>
<DropdownMenuSeparator />
{availableTags.map((tag) => (
<DropdownMenuCheckboxItem
key={tag}
checked={selectedTags.includes(tag)}
onCheckedChange={(checked) => {
setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag)))
}}
>
{tag}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Category Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 bg-transparent">
<Filter className="h-4 w-4" />
Categorias
{selectedCategories.length > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1">
{selectedCategories.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Filtrar por Categoria</DropdownMenuLabel>
<DropdownMenuSeparator />
{categories.map((category) => (
<DropdownMenuCheckboxItem
key={category}
checked={selectedCategories.includes(category)}
onCheckedChange={(checked) => {
setSelectedCategories((prev) =>
checked ? [...prev, category] : prev.filter((c) => c !== category),
)
}}
>
{category}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{hasActiveFilters && ( {hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="gap-2"> <Button variant="ghost" size="sm" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@ -699,7 +500,7 @@ export function EventManager({
</div> </div>
)} )}
{/* Calendar Views - Pass filteredEvents instead of events */} {/* Calendar Views - Month */}
{view === "month" && ( {view === "month" && (
<MonthView <MonthView
currentDate={currentDate} currentDate={currentDate}
@ -731,20 +532,12 @@ export function EventManager({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="font-semibold truncate">{ev.title}</div> <div className="font-semibold truncate">{ev.title}</div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
{ev.startTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})} {formatDateTimeBR(ev.startTime)} - {formatDateTimeBR(ev.endTime)}
{" - "}
{ev.endTime.toLocaleTimeString("pt-BR",{hour:"2-digit",minute:"2-digit"})}
</div> </div>
</div> </div>
{ev.description && ( {ev.description && (
<div className="text-xs text-muted-foreground line-clamp-2">{ev.description}</div> <div className="text-xs text-muted-foreground line-clamp-2">{ev.description}</div>
)} )}
<div className="mt-1 flex flex-wrap gap-1">
{ev.category && <Badge variant="secondary" className="text-[11px] h-5">{ev.category}</Badge>}
{ev.tags?.map((t) => (
<Badge key={t} variant="outline" className="text-[11px] h-5">{t}</Badge>
))}
</div>
</div> </div>
</div> </div>
))} ))}
@ -755,47 +548,6 @@ export function EventManager({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{view === "week" && (
<WeekView
currentDate={currentDate}
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
onDragStart={(event) => handleDragStart(event)}
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
/>
)}
{view === "day" && (
<DayView
currentDate={currentDate}
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
onDragStart={(event) => handleDragStart(event)}
onDragEnd={() => handleDragEnd()}
onDrop={handleDrop}
getColorClasses={getColorClasses}
/>
)}
{view === "list" && (
<ListView
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event)
setIsDialogOpen(true)
}}
getColorClasses={getColorClasses}
/>
)}
{/* Event Dialog */} {/* Event Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-md max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-md max-h-[90vh] overflow-y-auto">
@ -867,6 +619,10 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null)) : setSelectedEvent((prev) => (prev ? { ...prev, startTime: date } : null))
}} }}
/> />
{/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
<p className="text-xs text-muted-foreground">
{formatDateTimeBR(isCreating ? newEvent.startTime : selectedEvent?.startTime)}
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -894,77 +650,37 @@ export function EventManager({
: setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null)) : setSelectedEvent((prev) => (prev ? { ...prev, endTime: date } : null))
}} }}
/> />
</div> {/* Exibe também em pt-BR para usuários com SO/teclado em inglês */}
</div> <p className="text-xs text-muted-foreground">
{formatDateTimeBR(isCreating ? newEvent.endTime : selectedEvent?.endTime)}
<div className="grid grid-cols-2 gap-4"> </p>
<div className="space-y-2">
<Label htmlFor="category">Categoria</Label>
<Select
value={isCreating ? newEvent.category : selectedEvent?.category}
onValueChange={(value) =>
isCreating
? setNewEvent((prev) => ({ ...prev, category: value }))
: setSelectedEvent((prev) => (prev ? { ...prev, category: value } : null))
}
>
<SelectTrigger id="category">
<SelectValue placeholder="Selecione a categoria" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat} value={cat}>
{cat}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="color">Cor</Label>
<Select
value={isCreating ? newEvent.color : selectedEvent?.color}
onValueChange={(value) =>
isCreating
? setNewEvent((prev) => ({ ...prev, color: value }))
: setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
}
>
<SelectTrigger id="color">
<SelectValue placeholder="Selecione a cor" />
</SelectTrigger>
<SelectContent>
{colors.map((color) => (
<SelectItem key={color.value} value={color.value}>
<div className="flex items-center gap-2">
<div className={cn("h-4 w-4 rounded", color.bg)} />
{color.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Tags</Label> <Label htmlFor="color">Cor</Label>
<div className="flex flex-wrap gap-2"> <Select
{availableTags.map((tag) => { value={isCreating ? newEvent.color : selectedEvent?.color}
const isSelected = isCreating ? newEvent.tags?.includes(tag) : selectedEvent?.tags?.includes(tag) onValueChange={(value) =>
return ( isCreating
<Badge ? setNewEvent((prev) => ({ ...prev, color: value }))
key={tag} : setSelectedEvent((prev) => (prev ? { ...prev, color: value } : null))
variant={isSelected ? "default" : "outline"} }
className="cursor-pointer transition-all hover:scale-105" >
onClick={() => toggleTag(tag, isCreating)} <SelectTrigger id="color">
> <SelectValue placeholder="Selecione a cor" />
{tag} </SelectTrigger>
</Badge> <SelectContent>
) {colors.map((color) => (
})} <SelectItem key={color.value} value={color.value}>
</div> <div className="flex items-center gap-2">
<div className={cn("h-4 w-4 rounded", color.bg)} />
{color.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
@ -1013,12 +729,8 @@ function EventCard({
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
const colorClasses = getColorClasses(event.color) const colorClasses = getColorClasses(event.color)
const formatTime = (date: Date) => { const formatTime = (date: Date) =>
return date.toLocaleTimeString("en-US", { date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })
hour: "2-digit",
minute: "2-digit",
})
}
const getDuration = () => { const getDuration = () => {
const diff = event.endTime.getTime() - event.startTime.getTime() const diff = event.endTime.getTime() - event.startTime.getTime()
@ -1067,18 +779,6 @@ function EventCard({
</span> </span>
<span className="text-[10px]">({getDuration()})</span> <span className="text-[10px]">({getDuration()})</span>
</div> </div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-[10px] h-5">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-[10px] h-5">
{tag}
</Badge>
))}
</div>
</div> </div>
</Card> </Card>
</div> </div>
@ -1109,20 +809,6 @@ function EventCard({
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{formatTime(event.startTime)} - {formatTime(event.endTime)} {formatTime(event.startTime)} - {formatTime(event.endTime)}
</div> </div>
{isHovered && (
<div className="mt-2 flex flex-wrap gap-1 animate-in fade-in slide-in-from-bottom-1 duration-200">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
</div> </div>
) )
} }
@ -1164,18 +850,6 @@ function EventCard({
</span> </span>
<span className="text-[10px]">({getDuration()})</span> <span className="text-[10px]">({getDuration()})</span>
</div> </div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
{event.tags?.map((tag) => (
<Badge key={tag} variant="outline" className="text-xs">
{tag}
</Badge>
))}
</div>
</div> </div>
</div> </div>
</Card> </Card>
@ -1491,13 +1165,11 @@ function ListView({
month: "long", month: "long",
day: "numeric", day: "numeric",
}) })
if (!acc[dateKey]) { if (!acc[dateKey]) acc[dateKey] = []
acc[dateKey] = []
}
acc[dateKey].push(event) acc[dateKey].push(event)
return acc return acc
}, },
{} as Record<string, Event[]>, {} as Record<string, Event[]>
) )
return ( return (
@ -1529,28 +1201,12 @@ function ListView({
</p> </p>
)} )}
</div> </div>
<div className="flex flex-wrap gap-1">
{event.category && (
<Badge variant="secondary" className="text-xs">
{event.category}
</Badge>
)}
</div>
</div> </div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-muted-foreground sm:gap-4 sm:text-xs"> <div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-muted-foreground sm:gap-4 sm:text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} {event.startTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} - {event.endTime.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })}
</div> </div>
{event.tags && event.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{event.tags.map((tag) => (
<Badge key={tag} variant="outline" className="text-[10px] h-4 sm:text-xs sm:h-5">
{tag}
</Badge>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </div>