Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "elo",
"version": "1.46.0",
"version": "1.47.0",
"private": true,
"type": "module",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- AlterTable
ALTER TABLE "vehicles" ADD COLUMN "filialId" TEXT;

-- CreateIndex
CREATE INDEX "vehicles_filialId_idx" ON "vehicles"("filialId");

-- AddForeignKey
ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_filialId_fkey" FOREIGN KEY ("filialId") REFERENCES "filiais"("id") ON DELETE SET NULL ON UPDATE CASCADE;
6 changes: 6 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,16 @@ model Vehicle {
model String
plate String @unique
imageUrl String
// Enum legado mantido em sincronia com `filial.empresa.enterprise` para
// compatibilidade com relatórios/exibições antigas. A fonte da verdade é a filial.
enterprise Enterprise
filialId String?
filial Filial? @relation(fields: [filialId], references: [id], onDelete: SetNull)
kilometers BigInt
availble Boolean @default(true)
rents VehicleRent[]

@@index([filialId])
@@map("vehicles")
}

Expand Down Expand Up @@ -619,6 +624,7 @@ model Filial {
empresa Empresa @relation(fields: [empresaId], references: [id], onDelete: Restrict)
users User[]
restaurants Restaurant[]
vehicles Vehicle[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Expand Down
37 changes: 24 additions & 13 deletions src/app/(authenticated)/admin/vehicles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ export default function VehiclesAdminClient() {
}
}

const filteredVehicles = vehicles?.items?.filter(vehicle =>
vehicle.model.toLowerCase().includes(searchTerm.toLowerCase()) ||
vehicle.plate.toLowerCase().includes(searchTerm.toLowerCase()) ||
vehicle.enterprise.toLowerCase().includes(searchTerm.toLowerCase())
) ?? []
const filteredVehicles = vehicles?.items?.filter(vehicle => {
const term = searchTerm.toLowerCase()
return (
vehicle.model.toLowerCase().includes(term) ||
vehicle.plate.toLowerCase().includes(term) ||
vehicle.enterprise.toLowerCase().includes(term) ||
(vehicle.filial?.name.toLowerCase().includes(term) ?? false) ||
(vehicle.filial?.code.toLowerCase().includes(term) ?? false) ||
(vehicle.filial?.empresa.name.toLowerCase().includes(term) ?? false)
)
}) ?? []

if (isLoading) {
return (
Expand Down Expand Up @@ -110,9 +116,9 @@ export default function VehiclesAdminClient() {
onSuccess={() => {
setIsCreateDialogOpen(false)
void refetch()
} } onCancel={function (): void {
throw new Error("Function not implemented.")
} } />
}}
onCancel={() => setIsCreateDialogOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
Expand Down Expand Up @@ -199,7 +205,11 @@ export default function VehiclesAdminClient() {
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Empresa:</span>
<span>{vehicle.enterprise}</span>
<span>{vehicle.filial?.empresa.name ?? vehicle.enterprise}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Filial:</span>
<span>{vehicle.filial ? `${vehicle.filial.name} (${vehicle.filial.code})` : "—"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Quilometragem:</span>
Expand Down Expand Up @@ -232,10 +242,11 @@ export default function VehiclesAdminClient() {
setIsEditDialogOpen(false)
setSelectedVehicle(null)
void refetch()
}} onCancel={function (): void {
throw new Error("Function not implemented.")
} }

}}
onCancel={() => {
setIsEditDialogOpen(false)
setSelectedVehicle(null)
}}
/>
</DialogContent>
</Dialog>
Expand Down
33 changes: 16 additions & 17 deletions src/app/(authenticated)/cars/details/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"use client"

import { useState, Suspense } from "react"
import { useSearchParams } from "next/navigation"
import type { Enterprise, Vehicle } from "@prisma/client"
import type { Vehicle } from "@prisma/client"

import { api } from "@/trpc/react"
import { VehicleCard } from "@/components/vehicles/vehicle-card"
import { EnterpriseFilter } from "@/components/ui/enterprise-filter"
import { EmpresaFilialFilter, type EmpresaFilialValue } from "@/components/ui/empresa-filial-filter"
import { VehicleCardSkeleton } from "@/components/vehicles/vehicle-card-skeleton"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Expand All @@ -20,8 +19,8 @@ import { Clock, Filter } from "lucide-react"
type VehicleWithNumberKm = Omit<Vehicle, "kilometers"> & { kilometers: number }

function VehiclesPageContent() {
const searchParams = useSearchParams()
const enterprise = searchParams.get("enterprise") as Enterprise | null
// Filtro no padrão novo (empresa → filial)
const [empresaFilial, setEmpresaFilial] = useState<EmpresaFilialValue>({ empresaId: "", filialId: "" })

// Estados para filtros de data e horário específicos
const [selectedDate, setSelectedDate] = useState<string>("")
Expand All @@ -45,17 +44,17 @@ function VehiclesPageContent() {
},
)

const vehicles: VehicleWithNumberKm[] =
specificFilterData?.items?.map(
(vehicle): VehicleWithNumberKm => ({
...vehicle,
kilometers: Number(vehicle.kilometers),
}),
) ?? []

const filteredVehicles = enterprise
? vehicles?.filter((vehicle) => vehicle.enterprise === enterprise)
: vehicles
// Filtra por filial/empresa (padrão novo) antes de mapear quilometragem.
const filteredVehicles: VehicleWithNumberKm[] = (specificFilterData?.items ?? [])
.filter((vehicle) => {
if (empresaFilial.filialId) return vehicle.filialId === empresaFilial.filialId
if (empresaFilial.empresaId) return vehicle.filial?.empresa.id === empresaFilial.empresaId
return true
})
.map((vehicle) => ({
...vehicle,
kilometers: Number(vehicle.kilometers),
}))

// Funções para controlar os filtros
const applyFilters = () => {
Expand Down Expand Up @@ -131,7 +130,7 @@ function VehiclesPageContent() {
</Card>

<div className="flex flex-col md:flex-row gap-4 mb-4">
<EnterpriseFilter selectedEnterprise={enterprise ?? undefined} />
<EmpresaFilialFilter value={empresaFilial} onChange={setEmpresaFilial} />
</div>

{isLoading && (
Expand Down
19 changes: 16 additions & 3 deletions src/app/(authenticated)/cars/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FinishRentButton } from "@/components/vehicles/finish-rent-button"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"
import { VehicleCalendar } from "@/components/vehicles/vehicle-calendar"
import { RentForm } from "@/components/vehicles/rent-form"
import { EmpresaFilialFilter, type EmpresaFilialValue } from "@/components/ui/empresa-filial-filter"
import { Edit } from "lucide-react"
import { useState } from "react"
import { Input } from "@/components/ui/input"
Expand All @@ -27,6 +28,8 @@ export default function DashboardPage() {
const [selectedDate, setSelectedDate] = useState<string>("")
const [selectedTime, setSelectedTime] = useState<string>("")
const [filterApplied, setFilterApplied] = useState(false)
// Filtro no padrão novo (empresa → filial)
const [empresaFilial, setEmpresaFilial] = useState<EmpresaFilialValue>({ empresaId: "", filialId: "" })

// Buscar dados do usuário e reservas ativas
const { data: userData } = api.user.me.useQuery()
Expand All @@ -48,6 +51,13 @@ export default function DashboardPage() {
// Verificar se o usuário tem permissão para fazer reservas
const canReserve = userData ? canLocateCars(userData.role_config) : false

// Aplicar filtro de empresa/filial (padrão novo) sobre os veículos disponíveis
const filteredVehicles = (availableVehicles?.items ?? []).filter((vehicle) => {
if (empresaFilial.filialId) return vehicle.filialId === empresaFilial.filialId
if (empresaFilial.empresaId) return vehicle.filial?.empresa.id === empresaFilial.empresaId
return true
})

// Função para abrir o modal de edição
const openEditModal = (rent: VehicleRent & { vehicle: Vehicle }) => {
setSelectedRent(rent)
Expand Down Expand Up @@ -158,18 +168,21 @@ export default function DashboardPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Car className="h-5 w-5" />
Veículos Disponíveis ({availableVehicles?.items?.length ?? 0})
Veículos Disponíveis ({filteredVehicles.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-4">
<EmpresaFilialFilter value={empresaFilial} onChange={setEmpresaFilial} />
</div>
{isLoadingVehicles ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Carregando veículos disponíveis...</p>
</div>
) : availableVehicles?.items?.length ? (
) : filteredVehicles.length ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{availableVehicles.items.map((vehicle) => (
{filteredVehicles.map((vehicle) => (
<Card key={vehicle.id} className="p-4">
<div className="relative aspect-video h-32 w-full overflow-hidden rounded-lg mb-3">
<Image
Expand Down
89 changes: 67 additions & 22 deletions src/components/admin/vehicles/vehicle-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client"

import { useState, useEffect } from "react"
import { useState, useEffect, useMemo } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import type { z } from "zod"
Expand All @@ -15,14 +15,6 @@ import { toast } from "sonner"
import { createVehicleSchema } from "@/schemas/vehicle.schema"
import type { Vehicle } from "@prisma/client"

const enterpriseOptions = [
{ value: "NA", label: "N/A" },
{ value: "Box", label: "Box" },
{ value: "RHenz", label: "RHenz" },
{ value: "Cristallux", label: "Cristallux" },
{ value: "Box_Filial", label: "Box Filial" },
]

interface VehicleFormProps {
vehicle?: Vehicle
onSuccess: () => void
Expand All @@ -34,6 +26,11 @@ interface VehicleFormProps {
export function VehicleForm({ vehicle, onSuccess, onCancel }: VehicleFormProps) {
const [isLoading, setIsLoading] = useState(false)
const [uploadedImageUrl, setUploadedImageUrl] = useState<string>("")
// Estado local da empresa para a cascata empresa → filial (padrão novo).
const [empresaId, setEmpresaId] = useState("")

const { data: empresas = [] } = api.empresas.list.useQuery()
const { data: filiaisData = [] } = api.filiais.list.useQuery()

const {
register,
Expand All @@ -47,19 +44,44 @@ export function VehicleForm({ vehicle, onSuccess, onCancel }: VehicleFormProps)
model: vehicle.model,
plate: vehicle.plate,
imageUrl: vehicle.imageUrl || "",
enterprise: vehicle.enterprise as "NA" | "Box" | "RHenz" | "Cristallux" | "Box_Filial",
filialId: vehicle.filialId ?? "",
kilometers: Number(vehicle.kilometers),
availble: vehicle.availble,
} : {
model: "",
plate: "",
imageUrl: "",
enterprise: "NA" as const,
filialId: "",
kilometers: 0,
availble: true,
},
})

const filialId = watch("filialId")

// Filiais da empresa selecionada
const filiais = useMemo(
() => filiaisData.filter((f) => f.empresa.id === empresaId),
[filiaisData, empresaId],
)

// Ao editar, pré-seleciona a empresa a partir da filial atual do veículo.
useEffect(() => {
if (vehicle?.filialId && filiaisData.length > 0) {
const current = filiaisData.find((f) => f.id === vehicle.filialId)
if (current) setEmpresaId(current.empresa.id)
}
}, [vehicle?.filialId, filiaisData])

// Ao trocar de empresa, limpa a filial que não pertence mais à empresa.
useEffect(() => {
if (!filialId) return
const allowed = new Set(filiais.map((f) => f.id))
if (!allowed.has(filialId)) {
setValue("filialId", "")
}
}, [filiais, filialId, setValue])

// Atualizar o campo imageUrl quando uma nova imagem for enviada
useEffect(() => {
if (uploadedImageUrl) {
Expand Down Expand Up @@ -97,8 +119,6 @@ export function VehicleForm({ vehicle, onSuccess, onCancel }: VehicleFormProps)
}
}

const selectedEnterprise = watch("enterprise")

return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Expand Down Expand Up @@ -147,27 +167,52 @@ export function VehicleForm({ vehicle, onSuccess, onCancel }: VehicleFormProps)

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="enterprise">Empresa *</Label>
<Label htmlFor="empresa">Empresa *</Label>
<Select
value={selectedEnterprise}
onValueChange={(value) => setValue("enterprise", value as "NA" | "Box" | "RHenz" | "Cristallux" | "Box_Filial")}
value={empresaId}
onValueChange={(value) => {
setEmpresaId(value)
setValue("filialId", "")
}}
>
<SelectTrigger>
<SelectValue placeholder="Selecione uma empresa" />
<SelectValue placeholder="Selecione a empresa" />
</SelectTrigger>
<SelectContent>
{enterpriseOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{empresas.map((emp) => (
<SelectItem key={emp.id} value={emp.id}>
{emp.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.enterprise && (
<p className="text-sm text-destructive">{errors.enterprise.message}</p>
</div>

<div className="space-y-2">
<Label htmlFor="filial">Filial *</Label>
<Select
value={filialId ?? ""}
onValueChange={(value) => setValue("filialId", value, { shouldValidate: true })}
disabled={!empresaId}
>
<SelectTrigger>
<SelectValue placeholder={empresaId ? "Selecione a filial" : "Selecione a empresa primeiro"} />
</SelectTrigger>
<SelectContent>
{filiais.map((filial) => (
<SelectItem key={filial.id} value={filial.id}>
{filial.name} ({filial.code})
</SelectItem>
))}
</SelectContent>
</Select>
{errors.filialId && (
<p className="text-sm text-destructive">{errors.filialId.message}</p>
)}
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="kilometers">Kilometragem *</Label>
<Input
Expand Down
Loading
Loading