chore: sync staging
This commit is contained in:
parent
c5ddd54a3e
commit
561b19cf66
610 changed files with 105285 additions and 1206 deletions
|
|
@ -7,17 +7,29 @@ import { api } from "@/convex/_generated/api"
|
|||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { useAuth } from "@/lib/auth-client"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
import { usePersistentCompanyFilter } from "@/lib/use-company-filter"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
||||
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart"
|
||||
import type { ChartConfig } from "@/components/ui/chart"
|
||||
import { formatHoursCompact } from "@/lib/utils"
|
||||
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
|
||||
import { ReportsFilterToolbar } from "@/components/reports/report-filter-toolbar"
|
||||
import { ReportScheduleDrawer } from "@/components/reports/report-schedule-drawer"
|
||||
|
||||
type HoursItem = {
|
||||
companyId: string
|
||||
|
|
@ -43,7 +55,9 @@ const topClientsChartConfig = {
|
|||
export function HoursReport() {
|
||||
const [timeRange, setTimeRange] = useState("90d")
|
||||
const [companyId, setCompanyId] = usePersistentCompanyFilter("all")
|
||||
const { session, convexUserId, isStaff } = useAuth()
|
||||
const [billingFilter, setBillingFilter] = useState<"all" | "avulso" | "contratado">("all")
|
||||
const [schedulerOpen, setSchedulerOpen] = useState(false)
|
||||
const { session, convexUserId, isStaff, isAdmin } = useAuth()
|
||||
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
const enabled = Boolean(isStaff && convexUserId)
|
||||
|
|
@ -71,12 +85,17 @@ export function HoursReport() {
|
|||
]
|
||||
}, [companies])
|
||||
const filtered = useMemo(() => {
|
||||
const items = data?.items ?? []
|
||||
let items = data?.items ?? []
|
||||
if (companyId !== "all") {
|
||||
return items.filter((it) => String(it.companyId) === companyId)
|
||||
items = items.filter((it) => String(it.companyId) === companyId)
|
||||
}
|
||||
if (billingFilter === "avulso") {
|
||||
items = items.filter((it) => it.isAvulso)
|
||||
} else if (billingFilter === "contratado") {
|
||||
items = items.filter((it) => !it.isAvulso)
|
||||
}
|
||||
return items
|
||||
}, [data?.items, companyId])
|
||||
}, [data?.items, companyId, billingFilter])
|
||||
|
||||
const totals = useMemo(() => {
|
||||
return filtered.reduce(
|
||||
|
|
@ -128,6 +147,30 @@ export function HoursReport() {
|
|||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{isAdmin ? (
|
||||
<ReportScheduleDrawer
|
||||
open={schedulerOpen}
|
||||
onOpenChange={setSchedulerOpen}
|
||||
defaultReports={["hours"]}
|
||||
defaultRange={timeRange}
|
||||
defaultCompanyId={companyId === "all" ? null : companyId}
|
||||
companyOptions={companyOptions}
|
||||
/>
|
||||
) : null}
|
||||
<ReportsFilterToolbar
|
||||
companyId={companyId}
|
||||
onCompanyChange={(value) => setCompanyId(value)}
|
||||
companyOptions={companyOptions}
|
||||
timeRange={timeRange as "90d" | "30d" | "7d"}
|
||||
onTimeRangeChange={(value) => setTimeRange(value)}
|
||||
showBillingFilter
|
||||
billingFilter={billingFilter}
|
||||
onBillingFilterChange={(value) => setBillingFilter(value)}
|
||||
exportHref={`/api/reports/hours-by-client.xlsx?range=${timeRange}${
|
||||
companyId !== "all" ? `&companyId=${companyId}` : ""
|
||||
}`}
|
||||
onOpenScheduler={isAdmin ? () => setSchedulerOpen(true) : undefined}
|
||||
/>
|
||||
<Card className="border-slate-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Top clientes por horas</CardTitle>
|
||||
|
|
@ -176,45 +219,10 @@ export function HoursReport() {
|
|||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Horas</CardTitle>
|
||||
<CardDescription>Visualize o esforço interno e externo por empresa e acompanhe o consumo contratado.</CardDescription>
|
||||
<CardAction>
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<SearchableCombobox
|
||||
value={companyId}
|
||||
onValueChange={(next) => setCompanyId(next ?? "all")}
|
||||
options={companyOptions}
|
||||
placeholder="Todas as empresas"
|
||||
className="w-full min-w-56 lg:w-72"
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{["90d", "30d", "7d"].map((range) => (
|
||||
<Button
|
||||
key={range}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={timeRange === range ? "default" : "outline"}
|
||||
onClick={() => setTimeRange(range)}
|
||||
>
|
||||
{range === "90d" ? "90 dias" : range === "30d" ? "30 dias" : "7 dias"}
|
||||
</Button>
|
||||
))}
|
||||
<Button asChild size="sm" variant="outline" className="gap-2">
|
||||
<a
|
||||
href={`/api/reports/hours-by-client.xlsx?range=${timeRange}${companyId !== "all" ? `&companyId=${companyId}` : ""}`}
|
||||
download
|
||||
>
|
||||
Exportar XLSX
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue