Exibe loading em máquinas e moderniza time picker

This commit is contained in:
codex-bot 2025-10-23 15:06:41 -03:00
parent 9bfdb451bc
commit aef5e66718
2 changed files with 57 additions and 67 deletions

View file

@ -36,6 +36,7 @@ import { api } from "@/convex/_generated/api"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Spinner } from "@/components/ui/spinner"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
@ -737,12 +738,16 @@ export function normalizeMachineItem(raw: Record<string, unknown>): MachinesQuer
} }
} }
function useMachinesQuery(tenantId: string): MachinesQueryItem[] { function useMachinesQuery(tenantId: string): { machines: MachinesQueryItem[]; isLoading: boolean } {
const result = useQuery(api.machines.listByTenant, { const result = useQuery(api.machines.listByTenant, {
tenantId, tenantId,
includeMetadata: true, includeMetadata: true,
}) as Array<Record<string, unknown>> | undefined }) as Array<Record<string, unknown>> | undefined
return useMemo(() => (result ?? []).map((item) => normalizeMachineItem(item)), [result]) const machines = useMemo(() => (result ?? []).map((item) => normalizeMachineItem(item)), [result])
return {
machines,
isLoading: result === undefined,
}
} }
const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000 const DEFAULT_OFFLINE_THRESHOLD_MS = 10 * 60 * 1000
@ -1047,7 +1052,7 @@ function OsIcon({ osName }: { osName?: string | null }) {
} }
export function AdminMachinesOverview({ tenantId, initialCompanyFilterSlug = "all" }: { tenantId: string; initialCompanyFilterSlug?: string }) { export function AdminMachinesOverview({ tenantId, initialCompanyFilterSlug = "all" }: { tenantId: string; initialCompanyFilterSlug?: string }) {
const machines = useMachinesQuery(tenantId) const { machines, isLoading } = useMachinesQuery(tenantId)
const [q, setQ] = useState("") const [q, setQ] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all") const [statusFilter, setStatusFilter] = useState<string>("all")
const [companyFilterSlug, setCompanyFilterSlug] = useState<string>(initialCompanyFilterSlug) const [companyFilterSlug, setCompanyFilterSlug] = useState<string>(initialCompanyFilterSlug)
@ -1184,7 +1189,9 @@ const filteredMachines = useMemo(() => {
</label> </label>
<Button variant="outline" onClick={() => { setQ(""); setStatusFilter("all"); setCompanyFilterSlug("all"); setCompanySearch(""); setOnlyAlerts(false) }}>Limpar</Button> <Button variant="outline" onClick={() => { setQ(""); setStatusFilter("all"); setCompanyFilterSlug("all"); setCompanySearch(""); setOnlyAlerts(false) }}>Limpar</Button>
</div> </div>
{machines.length === 0 ? ( {isLoading ? (
<LoadingState />
) : machines.length === 0 ? (
<EmptyState /> <EmptyState />
) : ( ) : (
<MachinesGrid machines={filteredMachines} companyNameBySlug={companyNameBySlug} /> <MachinesGrid machines={filteredMachines} companyNameBySlug={companyNameBySlug} />
@ -1251,6 +1258,22 @@ function EmptyState() {
) )
} }
function LoadingState() {
return (
<div className="flex flex-col items-center gap-3 rounded-lg border border-dashed border-slate-300 bg-slate-50/50 py-12 text-center">
<div className="inline-flex size-12 items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm">
<Spinner className="size-6 text-slate-500" />
</div>
<div className="space-y-1">
<p className="text-sm font-semibold text-slate-600">Carregando máquinas...</p>
<p className="text-sm text-muted-foreground">
Sincronizando o inventário em tempo real. Isso leva apenas alguns instantes.
</p>
</div>
</div>
)
}
type MachineDetailsProps = { type MachineDetailsProps = {
machine: MachinesQueryItem | null machine: MachinesQueryItem | null
} }

View file

@ -1,10 +1,9 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { ChevronDownIcon } from "lucide-react" import { Clock8Icon } from "lucide-react"
import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
type TimePickerProps = { type TimePickerProps = {
@ -13,71 +12,39 @@ type TimePickerProps = {
className?: string className?: string
placeholder?: string placeholder?: string
stepMinutes?: number stepMinutes?: number
disabled?: boolean
} }
function pad2(n: number) { export function TimePicker({ value, onChange, className, placeholder = "Selecionar horário", stepMinutes = 15, disabled }: TimePickerProps) {
return String(n).padStart(2, "0") const handleChange = React.useCallback(
} (event: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(event.target.value)
},
[onChange]
)
export function TimePicker({ value, onChange, className, placeholder = "Selecionar horário", stepMinutes = 15 }: TimePickerProps) { const stepSeconds = React.useMemo(() => {
const [open, setOpen] = React.useState(false) if (!stepMinutes || stepMinutes <= 0) return undefined
return Math.max(1, Math.round(stepMinutes * 60))
const [hours, minutes] = React.useMemo(() => {
if (!value || !/^\d{2}:\d{2}$/.test(value)) return ["", ""]
const [h, m] = value.split(":")
return [h, m]
}, [value])
const minuteOptions = React.useMemo(() => {
const list: string[] = []
for (let i = 0; i < 60; i += stepMinutes) list.push(pad2(i))
if (!list.includes("00")) list.unshift("00")
return list
}, [stepMinutes]) }, [stepMinutes])
return ( return (
<Popover open={open} onOpenChange={setOpen}> <div className="relative">
<PopoverTrigger asChild> <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground">
<Button variant="outline" className={cn("w-full justify-between font-normal", className)}> <Clock8Icon aria-hidden className="size-4" />
{value ? value : placeholder} </div>
<ChevronDownIcon className="ml-2 size-4 opacity-60" /> <Input
</Button> type="time"
</PopoverTrigger> value={value ?? ""}
<PopoverContent className="w-auto p-0" align="start"> onChange={handleChange}
<div className="flex gap-2 p-2"> step={stepSeconds}
<div className="max-h-56 w-16 overflow-auto rounded-md border"> disabled={disabled}
{Array.from({ length: 24 }, (_, h) => pad2(h)).map((h) => ( aria-label={placeholder}
<button
key={h}
type="button"
className={cn( className={cn(
"block w-full px-3 py-1.5 text-left text-sm hover:bg-muted", "pl-9 text-sm [appearance:textfield] [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none",
h === hours && "bg-muted/70 font-semibold" className
)} )}
onClick={() => onChange?.(`${h}:${minutes || "00"}`)} />
>
{h}
</button>
))}
</div> </div>
<div className="max-h-56 w-16 overflow-auto rounded-md border">
{minuteOptions.map((m) => (
<button
key={m}
type="button"
className={cn(
"block w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
m === minutes && "bg-muted/70 font-semibold"
)}
onClick={() => onChange?.(`${hours || "00"}:${m}`)}
>
{m}
</button>
))}
</div>
</div>
</PopoverContent>
</Popover>
) )
} }