Add equipment loan feature and USB bulk control
- Add emprestimos (equipment loan) module in Convex with queries/mutations - Create emprestimos page with full CRUD and status tracking - Add USB bulk control to admin devices overview - Fix Portuguese accents in USB policy control component - Fix dead code warnings in Rust agent - Fix tiptap type error in rich text editor
This commit is contained in:
parent
49aa143a80
commit
063c5dfde7
11 changed files with 1448 additions and 26 deletions
|
|
@ -38,6 +38,8 @@ import {
|
|||
Plus,
|
||||
Smartphone,
|
||||
Tablet,
|
||||
Usb,
|
||||
Loader2,
|
||||
} from "lucide-react"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
|
@ -1263,6 +1265,10 @@ export function AdminDevicesOverview({
|
|||
const [templateForCompany, setTemplateForCompany] = useState(false)
|
||||
const [templateAsDefault, setTemplateAsDefault] = useState(false)
|
||||
const [isSavingTemplate, setIsSavingTemplate] = useState(false)
|
||||
const [isUsbPolicyDialogOpen, setIsUsbPolicyDialogOpen] = useState(false)
|
||||
const [selectedUsbPolicy, setSelectedUsbPolicy] = useState<"ALLOW" | "BLOCK_ALL" | "READONLY">("ALLOW")
|
||||
const [isApplyingUsbPolicy, setIsApplyingUsbPolicy] = useState(false)
|
||||
const [usbPolicySelection, setUsbPolicySelection] = useState<string[]>([])
|
||||
const [isCreateDeviceOpen, setIsCreateDeviceOpen] = useState(false)
|
||||
const [createDeviceLoading, setCreateDeviceLoading] = useState(false)
|
||||
const [newDeviceName, setNewDeviceName] = useState("")
|
||||
|
|
@ -1386,6 +1392,7 @@ export function AdminDevicesOverview({
|
|||
|
||||
const createTemplate = useMutation(api.deviceExportTemplates.create)
|
||||
const saveDeviceProfileMutation = useMutation(api.devices.saveDeviceProfile)
|
||||
const bulkSetUsbPolicyMutation = useMutation(api.usbPolicy.bulkSetUsbPolicy)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCompany && templateForCompany) {
|
||||
|
|
@ -1676,6 +1683,69 @@ export function AdminDevicesOverview({
|
|||
}
|
||||
}, [filteredDevices])
|
||||
|
||||
const handleOpenUsbPolicyDialog = useCallback(() => {
|
||||
const windowsDevices = filteredDevices.filter(
|
||||
(m) => (m.devicePlatform ?? "").toLowerCase() === "windows"
|
||||
)
|
||||
if (windowsDevices.length === 0) {
|
||||
toast.info("Não há dispositivos Windows para aplicar política USB.")
|
||||
return
|
||||
}
|
||||
setUsbPolicySelection(windowsDevices.map((m) => m.id))
|
||||
setSelectedUsbPolicy("ALLOW")
|
||||
setIsApplyingUsbPolicy(false)
|
||||
setIsUsbPolicyDialogOpen(true)
|
||||
}, [filteredDevices])
|
||||
|
||||
const handleUsbPolicyDialogOpenChange = useCallback((open: boolean) => {
|
||||
if (!open && isApplyingUsbPolicy) return
|
||||
setIsUsbPolicyDialogOpen(open)
|
||||
}, [isApplyingUsbPolicy])
|
||||
|
||||
const handleToggleUsbDeviceSelection = useCallback((deviceId: string, checked: boolean) => {
|
||||
setUsbPolicySelection((prev) => {
|
||||
if (checked) {
|
||||
if (prev.includes(deviceId)) return prev
|
||||
return [...prev, deviceId]
|
||||
}
|
||||
return prev.filter((id) => id !== deviceId)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleSelectAllUsbDevices = useCallback((checked: boolean) => {
|
||||
const windowsDevices = filteredDevices.filter(
|
||||
(m) => (m.devicePlatform ?? "").toLowerCase() === "windows"
|
||||
)
|
||||
if (checked) {
|
||||
setUsbPolicySelection(windowsDevices.map((m) => m.id))
|
||||
} else {
|
||||
setUsbPolicySelection([])
|
||||
}
|
||||
}, [filteredDevices])
|
||||
|
||||
const handleApplyBulkUsbPolicy = useCallback(async () => {
|
||||
if (usbPolicySelection.length === 0) {
|
||||
toast.info("Selecione ao menos um dispositivo.")
|
||||
return
|
||||
}
|
||||
|
||||
setIsApplyingUsbPolicy(true)
|
||||
try {
|
||||
const result = await bulkSetUsbPolicyMutation({
|
||||
machineIds: usbPolicySelection.map((id) => id as Id<"machines">),
|
||||
policy: selectedUsbPolicy,
|
||||
actorId: convexUserId ? (convexUserId as Id<"users">) : undefined,
|
||||
})
|
||||
toast.success(`Política USB aplicada a ${result.successful} de ${result.total} dispositivos.`)
|
||||
setIsUsbPolicyDialogOpen(false)
|
||||
} catch (error) {
|
||||
console.error("[usb-policy] Falha ao aplicar política em massa", error)
|
||||
toast.error("Falha ao aplicar política USB. Tente novamente.")
|
||||
} finally {
|
||||
setIsApplyingUsbPolicy(false)
|
||||
}
|
||||
}, [usbPolicySelection, selectedUsbPolicy, convexUserId, bulkSetUsbPolicyMutation])
|
||||
|
||||
const handleConfirmExport = useCallback(async () => {
|
||||
const orderedSelection = filteredDevices.map((m) => m.id).filter((id) => exportSelection.includes(id))
|
||||
if (orderedSelection.length === 0) {
|
||||
|
|
@ -1902,6 +1972,10 @@ export function AdminDevicesOverview({
|
|||
<Download className="size-4" />
|
||||
Exportar XLSX
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="gap-2" onClick={handleOpenUsbPolicyDialog}>
|
||||
<Usb className="size-4" />
|
||||
Controle USB
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
|
|
@ -2113,6 +2187,138 @@ export function AdminDevicesOverview({
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isUsbPolicyDialogOpen} onOpenChange={handleUsbPolicyDialogOpenChange}>
|
||||
<DialogContent className="max-w-2xl space-y-5">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Usb className="size-5" />
|
||||
Controle USB em massa
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Aplique uma política de armazenamento USB a todos os dispositivos Windows selecionados.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{(() => {
|
||||
const windowsDevices = filteredDevices.filter(
|
||||
(m) => (m.devicePlatform ?? "").toLowerCase() === "windows"
|
||||
)
|
||||
if (windowsDevices.length === 0) {
|
||||
return (
|
||||
<div className="rounded-md border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
Nenhum dispositivo Windows disponível com os filtros atuais.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const allSelected = usbPolicySelection.length === windowsDevices.length
|
||||
const someSelected = usbPolicySelection.length > 0 && usbPolicySelection.length < windowsDevices.length
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between rounded-md border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
|
||||
<span>
|
||||
{usbPolicySelection.length} de {windowsDevices.length} dispositivos Windows selecionados
|
||||
</span>
|
||||
<Checkbox
|
||||
checked={allSelected ? true : someSelected ? "indeterminate" : false}
|
||||
onCheckedChange={(value) => handleSelectAllUsbDevices(value === true || value === "indeterminate")}
|
||||
disabled={isApplyingUsbPolicy}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-48 space-y-1 overflow-y-auto rounded-md border border-slate-200 p-2">
|
||||
{windowsDevices.map((device) => {
|
||||
const checked = usbPolicySelection.includes(device.id)
|
||||
return (
|
||||
<label
|
||||
key={device.id}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-sm transition hover:bg-slate-100",
|
||||
checked && "bg-slate-100"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(value) => handleToggleUsbDeviceSelection(device.id, value === true)}
|
||||
disabled={isApplyingUsbPolicy}
|
||||
/>
|
||||
<span className="flex-1 truncate">
|
||||
{device.displayName ?? device.hostname}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{device.companyName ?? "—"}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-slate-700">Política USB</label>
|
||||
<Select
|
||||
value={selectedUsbPolicy}
|
||||
onValueChange={(value) => setSelectedUsbPolicy(value as "ALLOW" | "BLOCK_ALL" | "READONLY")}
|
||||
disabled={isApplyingUsbPolicy}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione uma política" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALLOW">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="size-4 text-emerald-600" />
|
||||
<span>Permitido</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="BLOCK_ALL">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldOff className="size-4 text-red-600" />
|
||||
<span>Bloqueado</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="READONLY">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert className="size-4 text-amber-600" />
|
||||
<span>Somente leitura</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedUsbPolicy === "ALLOW" && "Acesso total a dispositivos USB de armazenamento."}
|
||||
{selectedUsbPolicy === "BLOCK_ALL" && "Nenhum acesso a dispositivos USB de armazenamento."}
|
||||
{selectedUsbPolicy === "READONLY" && "Permite leitura, bloqueia escrita em dispositivos USB."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<DialogFooter className="gap-2 sm:gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleUsbPolicyDialogOpenChange(false)}
|
||||
disabled={isApplyingUsbPolicy}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleApplyBulkUsbPolicy}
|
||||
disabled={isApplyingUsbPolicy || usbPolicySelection.length === 0}
|
||||
className="gap-2"
|
||||
>
|
||||
{isApplyingUsbPolicy ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Aplicando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Usb className="size-4" />
|
||||
Aplicar política{usbPolicySelection.length > 0 ? ` (${usbPolicySelection.length})` : ""}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isTemplateDialogOpen} onOpenChange={(open) => setIsTemplateDialogOpen(open)}>
|
||||
<DialogContent className="max-w-md space-y-4">
|
||||
<DialogHeader>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue