diff --git a/app/components/ExecucaoCicloviaria/CityContent.tsx b/app/components/ExecucaoCicloviaria/CityContent.tsx index 5eb6f1e3..5e87ff36 100644 --- a/app/components/ExecucaoCicloviaria/CityContent.tsx +++ b/app/components/ExecucaoCicloviaria/CityContent.tsx @@ -1,8 +1,25 @@ import React, { useState, useEffect, useRef, useMemo } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; import { filterById, filterByName, IntlNumberMax1Digit, IntlPercentil } from "~/services/utils"; import { CyclingInfrastructureByCity } from "./CyclingInfrastructureByCity"; import { StatisticsBox } from "./StatisticsBox"; -import Table from "../Commom/Table/Table"; +import { DataTable } from "~/components/ui/data-table"; + +interface PdcRelation { + name: string; + pdc_typology: string; + typologies_str: string; + length: number; + has_cycleway_length: number; +} + +const pdcColumns: ColumnDef[] = [ + { header: "Nome", accessorKey: "name" }, + { header: "Tipologia Prevista", accessorKey: "pdc_typology" }, + { header: "Tipologia Executada", accessorKey: "typologies_str" }, + { header: "Extensão Prevista (km)", accessorKey: "length" }, + { header: "Extensão Executada (km)", accessorKey: "has_cycleway_length" }, +]; interface CityContentProps { citiesStats: any; @@ -197,17 +214,40 @@ export function CityContent({ {localSelectedCity?.relations && localSelectedCity.relations.length > 0 && (
- + Estruturas do PDC para {localSelectedCity?.name || ""} + + { + const column = table.getColumn("pdc_typology"); + const rawFilterValue = column?.getFilterValue(); + const filterValue = typeof rawFilterValue === "string" ? rawFilterValue : ""; + const options = Array.from(column?.getFacetedUniqueValues().keys() ?? []) + .filter((v): v is string => typeof v === "string" && v.length > 0) + .sort(); + return ( +
+ + +
+ ); + }} /> )} diff --git a/app/components/Samu/SamuClientSide.tsx b/app/components/Samu/SamuClientSide.tsx index de4c2e4a..e5b293e1 100644 --- a/app/components/Samu/SamuClientSide.tsx +++ b/app/components/Samu/SamuClientSide.tsx @@ -1,10 +1,25 @@ import { useState, useEffect, useMemo } from "react"; -import Table from "../Commom/Table/Table"; +import type { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "~/components/ui/data-table"; import { VerticalBarChart } from "../Charts/VerticalBarChart"; import { NumberCards } from "../Commom/NumberCards"; import { SamuChoroplethMap } from "./SamuChoroplethMap"; import { SAMU_CALLS_OUTCOMES, SAMU_CALLS_PROFILES } from "~/servers"; +interface CityRow { + ranking: number; + municipio: string; + total_chamadas: string; + percentual: string; +} + +const citiesTableColumns: ColumnDef[] = [ + { header: "Ranking", accessorKey: "ranking" }, + { header: "Município", accessorKey: "municipio" }, + { header: "Total de Chamadas", accessorKey: "total_chamadas" }, + { header: "Percentual (%)", accessorKey: "percentual" }, +]; + interface CityData { id?: string | number; name?: string; @@ -598,16 +613,8 @@ export default function SamuClientSide({ citiesData }: SamuClientSideProps) {
-
+

Lista completa das cidades

+ diff --git a/app/components/ui/data-table.tsx b/app/components/ui/data-table.tsx new file mode 100644 index 00000000..abee91f1 --- /dev/null +++ b/app/components/ui/data-table.tsx @@ -0,0 +1,183 @@ +import { useState } from "react"; +import { + ColumnDef, + ColumnFiltersState, + SortingState, + Table as TanstackTable, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table"; + +export interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + /** Initial page size (defaults to 10) */ + pageSize?: number; + /** Enables the pagination controls (defaults to true) */ + pagination?: boolean; + /** Copy shown when data is empty */ + emptyMessage?: string; + /** Optional className overrides */ + className?: string; + /** Optional per-row className — useful for type-based coloring */ + rowClassName?: (row: TData) => string | undefined; + /** + * Optional render-prop for a toolbar above the table. Receives the TanStack + * table instance so the consumer can wire up filter inputs, selects, etc. + * Example: + * toolbar={(table) => ( + * table.getColumn("x")?.setFilterValue(v)} /> + * )} + */ + toolbar?: (table: TanstackTable) => React.ReactNode; +} + +export function DataTable({ + columns, + data, + pageSize = 10, + pagination = true, + emptyMessage = "Nenhum resultado encontrado.", + className, + rowClassName, + toolbar, +}: DataTableProps) { + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + + const table = useReactTable({ + data, + columns, + state: { sorting, columnFilters }, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + initialState: { pagination: { pageSize } }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: pagination ? getPaginationRowModel() : undefined, + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + const pageIndex = table.getState().pagination.pageIndex; + const pageCount = table.getPageCount(); + + return ( +
+ {toolbar &&
{toolbar(table)}
} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const column = header.column; + const canSort = column.getCanSort(); + const isSorted = column.getIsSorted(); + const headerText = + typeof column.columnDef.header === "string" ? column.columnDef.header : "coluna"; + return ( + + {canSort ? ( + + ) : ( + flexRender(column.columnDef.header, header.getContext()) + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {emptyMessage} + + + )} + +
+ + {pagination && pageCount > 1 && ( +
+
+ {table.getFilteredRowModel().rows.length} resultados • Página {pageIndex + 1} de {Math.max(pageCount, 1)} +
+
+ + +
+
+ )} +
+ + ); +} diff --git a/app/components/ui/table.tsx b/app/components/ui/table.tsx new file mode 100644 index 00000000..aec8caac --- /dev/null +++ b/app/components/ui/table.tsx @@ -0,0 +1,89 @@ +import * as React from "react"; +import { cn } from "~/lib/utils"; + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ) +); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +TableCaption.displayName = "TableCaption"; + +export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }; diff --git a/app/lib/utils.ts b/app/lib/utils.ts new file mode 100644 index 00000000..365058ce --- /dev/null +++ b/app/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/components.json b/components.json new file mode 100644 index 00000000..8dafa61d --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/tailwind.css", + "baseColor": "slate", + "cssVariables": false, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/package-lock.json b/package-lock.json index 3a0910f7..c0af50ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "@tanstack/react-table": "^8.21.3", "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "framer-motion": "^11.18.0", "fuse.js": "^7.1.0", "highcharts": "^12.2.0", @@ -36,7 +38,8 @@ "react-markdown": "^10.1.0", "react-spinners": "^0.17.0", "styled-components": "^6.1.14", - "swiper": "^12.0.3" + "swiper": "^12.0.3", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.0.0", @@ -4493,6 +4496,27 @@ "fsevents": "~2.3.2" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -11768,6 +11792,16 @@ "node": ">= 4.7.0" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", diff --git a/package.json b/package.json index 30e19f6f..b792d021 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "@tanstack/react-table": "^8.21.3", "@turf/bbox": "^7.2.0", "@turf/helpers": "^7.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "framer-motion": "^11.18.0", "fuse.js": "^7.1.0", "highcharts": "^12.2.0", @@ -42,7 +44,8 @@ "react-markdown": "^10.1.0", "react-spinners": "^0.17.0", "styled-components": "^6.1.14", - "swiper": "^12.0.3" + "swiper": "^12.0.3", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.0.0",