Exibe loading em máquinas e moderniza time picker
This commit is contained in:
parent
9bfdb451bc
commit
aef5e66718
2 changed files with 57 additions and 67 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
className={cn(
|
||||||
key={h}
|
"pl-9 text-sm [appearance:textfield] [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none",
|
||||||
type="button"
|
className
|
||||||
className={cn(
|
)}
|
||||||
"block w-full px-3 py-1.5 text-left text-sm hover:bg-muted",
|
/>
|
||||||
h === hours && "bg-muted/70 font-semibold"
|
</div>
|
||||||
)}
|
|
||||||
onClick={() => onChange?.(`${h}:${minutes || "00"}`)}
|
|
||||||
>
|
|
||||||
{h}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue