This commit is contained in:
Luiz Silva 2026-02-15 15:17:08 -03:00
parent 8a5596e860
commit 57325f6744
100 changed files with 16153 additions and 4623 deletions

View file

@ -147,6 +147,31 @@
z-index: 200;
}
.eli-tabela__rodape {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
flex-wrap: wrap;
}
.eli-tabela__paginacao {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 0;
flex-wrap: wrap;
}
.eli-tabela__acoes-inferiores {
display: inline-flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.eli-tabela__cabecalho {
width: 100%;
box-sizing: border-box;

File diff suppressed because it is too large Load diff

View file

@ -73,11 +73,11 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { ChevronDown, ChevronRight, MoreVertical } from "lucide-vue-next";
import EliTabelaCelula from "./celulas/EliTabelaCelula.vue";
import EliTabelaDetalhesLinha from "./EliTabelaDetalhesLinha.vue";
import type { tipoEliColuna } from "./types-eli-tabela";
import { ChevronDown, ChevronRight, MoreVertical } from "lucide-vue-next"
import { defineComponent, type PropType } from "vue"
import EliTabelaCelula from "./celulas/EliTabelaCelula.vue"
import EliTabelaDetalhesLinha from "./EliTabelaDetalhesLinha.vue"
import type { tipoEliColuna } from "./types-eli-tabela"
export default defineComponent({
name: "EliTabelaBody",
@ -90,10 +90,12 @@ export default defineComponent({
},
props: {
colunas: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
type: Array as PropType<Array<tipoEliColuna<any>>>,
required: true,
},
colunasInvisiveis: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
type: Array as PropType<Array<tipoEliColuna<any>>>,
required: true,
},
@ -134,7 +136,7 @@ export default defineComponent({
return {
ChevronRight,
ChevronDown,
};
}
},
});
})
</script>

View file

@ -30,7 +30,7 @@
type="button"
class="eli-tabela__acoes-cabecalho-botao"
:style="botao.cor ? { backgroundColor: botao.cor, color: '#fff' } : undefined"
@click="botao.acao"
@click="botao.acao(parametrosConsulta)"
>
<component
v-if="botao.icone"
@ -46,8 +46,8 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import EliTabelaCaixaDeBusca from "./EliTabelaCaixaDeBusca.vue";
import { computed, defineComponent, type PropType } from "vue"
import EliTabelaCaixaDeBusca from "./EliTabelaCaixaDeBusca.vue"
export default defineComponent({
name: "EliTabelaCabecalho",
@ -71,13 +71,20 @@ export default defineComponent({
type: String,
required: true,
},
parametrosConsulta: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic params
type: Object as PropType<any>,
required: false,
},
acoesCabecalho: {
type: Array as PropType<
Array<{
icone?: any;
cor?: string;
rotulo: string;
acao: () => void;
// biome-ignore lint/suspicious/noExplicitAny: dynamic icon
icone?: any
cor?: string
rotulo: string
// biome-ignore lint/suspicious/noExplicitAny: dynamic action
acao: (params?: any) => void
}>
>,
required: true,
@ -85,33 +92,33 @@ export default defineComponent({
},
emits: {
buscar(valor: string) {
return typeof valor === "string";
return typeof valor === "string"
},
colunas() {
return true;
return true
},
filtroAvancado() {
return true;
return true
},
},
setup(props, { emit }) {
const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0);
const temAcoesCabecalho = computed(() => props.acoesCabecalho.length > 0)
function emitBuscar(texto: string) {
emit("buscar", texto);
emit("buscar", texto)
}
function emitColunas() {
emit("colunas");
emit("colunas")
}
function emitFiltroAvancado() {
emit("filtroAvancado");
emit("filtroAvancado")
}
return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado };
return { temAcoesCabecalho, emitBuscar, emitColunas, emitFiltroAvancado }
},
});
})
</script>
<style>

View file

@ -23,8 +23,8 @@
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { Search } from "lucide-vue-next";
import { Search } from "lucide-vue-next"
import { defineComponent, ref, watch } from "vue"
export default defineComponent({
name: "EliTabelaCaixaDeBusca",
@ -38,7 +38,7 @@ export default defineComponent({
},
emits: {
buscar(valor: string) {
return typeof valor === "string";
return typeof valor === "string"
},
},
setup(props, { emit }) {
@ -46,24 +46,24 @@ export default defineComponent({
* Estado local da entrada para que o usuário possa digitar livremente antes
* de disparar uma nova consulta.
*/
const texto = ref(props.modelo ?? "");
const texto = ref(props.modelo ?? "")
watch(
() => props.modelo,
(novo) => {
if (novo !== undefined && novo !== texto.value) {
texto.value = novo;
texto.value = novo
}
}
);
},
)
function emitirBusca() {
emit("buscar", texto.value.trim());
emit("buscar", texto.value.trim())
}
return { texto, emitirBusca };
return { texto, emitirBusca }
},
});
})
</script>
<style>

View file

@ -12,7 +12,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { defineComponent, type PropType } from "vue"
export default defineComponent({
name: "EliTabelaDebug",
@ -30,5 +30,5 @@ export default defineComponent({
required: true,
},
},
});
})
</script>

View file

@ -10,9 +10,9 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import EliTabelaCelula from "./celulas/EliTabelaCelula.vue";
import type { tipoEliColuna } from "./types-eli-tabela";
import { defineComponent, type PropType } from "vue"
import EliTabelaCelula from "./celulas/EliTabelaCelula.vue"
import type { tipoEliColuna } from "./types-eli-tabela"
export default defineComponent({
name: "EliTabelaDetalhesLinha",
@ -23,11 +23,12 @@ export default defineComponent({
required: true,
},
colunasInvisiveis: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
type: Array as PropType<Array<tipoEliColuna<any>>>,
required: true,
},
},
});
})
</script>
<style>

View file

@ -17,7 +17,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { defineComponent, type PropType } from "vue"
export default defineComponent({
name: "EliTabelaEstados",
@ -36,5 +36,5 @@ export default defineComponent({
default: undefined,
},
},
});
})
</script>

View file

@ -49,15 +49,16 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { ArrowDown, ArrowUp } from "lucide-vue-next";
import type { tipoEliColuna } from "./types-eli-tabela";
import { ArrowDown, ArrowUp } from "lucide-vue-next"
import { defineComponent, type PropType } from "vue"
import type { tipoEliColuna } from "./types-eli-tabela"
export default defineComponent({
name: "EliTabelaHead",
components: { ArrowUp, ArrowDown },
props: {
colunas: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
type: Array as PropType<Array<tipoEliColuna<any>>>,
required: true,
},
@ -80,16 +81,17 @@ export default defineComponent({
},
emits: {
alternarOrdenacao(chave: string) {
return typeof chave === "string" && chave.length > 0;
return typeof chave === "string" && chave.length > 0
},
},
setup(_props, { emit }) {
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
function isOrdenavel(coluna: any) {
return coluna?.coluna_ordem !== undefined && coluna?.coluna_ordem !== null;
return coluna?.coluna_ordem !== undefined && coluna?.coluna_ordem !== null
}
function emitAlternarOrdenacao(chave: string) {
emit("alternarOrdenacao", chave);
emit("alternarOrdenacao", chave)
}
return {
@ -97,7 +99,7 @@ export default defineComponent({
ArrowDown,
isOrdenavel,
emitAlternarOrdenacao,
};
}
},
});
})
</script>

View file

@ -43,14 +43,14 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from "vue";
import type { tipoEliTabelaAcao } from "./types-eli-tabela";
import { computed, defineComponent, type PropType, ref } from "vue"
import type { tipoEliTabelaAcao } from "./types-eli-tabela"
type ItemAcao<T> = {
acao: tipoEliTabelaAcao<T>;
indice: number;
visivel: boolean;
};
acao: tipoEliTabelaAcao<T>
indice: number
visivel: boolean
}
export default defineComponent({
name: "EliTabelaMenuAcoes",
@ -64,6 +64,7 @@ export default defineComponent({
required: true,
},
acoes: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
type: Array as PropType<Array<ItemAcao<any>>>,
required: true,
},
@ -74,22 +75,24 @@ export default defineComponent({
},
},
emits: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
executar(payload: { acao: tipoEliTabelaAcao<any>; linha: unknown }) {
return payload !== null && typeof payload === "object";
return payload !== null && typeof payload === "object"
},
},
setup(props, { emit, expose }) {
const menuEl = ref<HTMLElement | null>(null);
expose({ menuEl });
const menuEl = ref<HTMLElement | null>(null)
expose({ menuEl })
const possuiAcoes = computed(() => props.acoes.length > 0);
const possuiAcoes = computed(() => props.acoes.length > 0)
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
function emitExecutar(item: { acao: tipoEliTabelaAcao<any> }) {
if (!props.linha) return;
emit("executar", { acao: item.acao, linha: props.linha });
if (!props.linha) return
emit("executar", { acao: item.acao, linha: props.linha })
}
return { menuEl, possuiAcoes, emitExecutar };
return { menuEl, possuiAcoes, emitExecutar }
},
});
})
</script>

View file

@ -72,19 +72,19 @@
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue";
import type { EliTabelaColunasConfig } from "./colunasStorage";
import type { tipoEliColuna } from "./types-eli-tabela";
import { defineComponent, type PropType, ref, watch } from "vue"
import type { EliTabelaColunasConfig } from "./colunasStorage"
import type { tipoEliColuna } from "./types-eli-tabela"
type OrigemLista = "visiveis" | "invisiveis";
type OrigemLista = "visiveis" | "invisiveis"
type DragPayload = {
rotulo: string;
origem: OrigemLista;
index: number;
};
rotulo: string
origem: OrigemLista
index: number
}
const DRAG_MIME = "application/x-eli-tabela-coluna";
const DRAG_MIME = "application/x-eli-tabela-coluna"
export default defineComponent({
name: "EliTabelaModalColunas",
@ -102,80 +102,89 @@ export default defineComponent({
required: true,
},
colunas: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
type: Array as PropType<Array<tipoEliColuna<any>>>,
required: true,
},
},
emits: {
fechar() {
return true;
return true
},
salvar(_config: EliTabelaColunasConfig) {
return true;
return true
},
},
setup(props, { emit }) {
const visiveisLocal = ref<string[]>([]);
const invisiveisLocal = ref<string[]>([]);
const visiveisLocal = ref<string[]>([])
const invisiveisLocal = ref<string[]>([])
function sincronizarEstado() {
const todos = props.rotulosColunas;
const todos = props.rotulosColunas
const configTemDados =
(props.configInicial.visiveis?.length ?? 0) > 0 ||
(props.configInicial.invisiveis?.length ?? 0) > 0;
(props.configInicial.invisiveis?.length ?? 0) > 0
const invisiveisPadraoSet = new Set(
props.colunas.filter((c) => c.visivel === false).map((c) => c.rotulo)
);
props.colunas.filter((c) => c.visivel === false).map((c) => c.rotulo),
)
const invisiveisSet = configTemDados
? new Set(props.configInicial.invisiveis ?? [])
: invisiveisPadraoSet;
: invisiveisPadraoSet
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r));
const baseVisiveis = todos.filter((r) => !invisiveisSet.has(r))
// ordenação: aplica ordem salva (visiveis) e adiciona novas ao final
const ordemSalva = props.configInicial.visiveis ?? [];
const setVis = new Set(baseVisiveis);
const ordenadas: string[] = [];
const ordemSalva = props.configInicial.visiveis ?? []
const setVis = new Set(baseVisiveis)
const ordenadas: string[] = []
for (const r of ordemSalva) {
if (setVis.has(r)) ordenadas.push(r);
if (setVis.has(r)) ordenadas.push(r)
}
for (const r of baseVisiveis) {
if (!ordenadas.includes(r)) ordenadas.push(r);
if (!ordenadas.includes(r)) ordenadas.push(r)
}
visiveisLocal.value = ordenadas;
visiveisLocal.value = ordenadas
// invisíveis: somente as que existem na tabela
invisiveisLocal.value = todos.filter((r) => invisiveisSet.has(r));
invisiveisLocal.value = todos.filter((r) => invisiveisSet.has(r))
}
watch(
() => [props.aberto, props.rotulosColunas, props.configInicial, props.colunas] as const,
() =>
[
props.aberto,
props.rotulosColunas,
props.configInicial,
props.colunas,
] as const,
() => {
if (props.aberto) sincronizarEstado();
if (props.aberto) sincronizarEstado()
},
{ deep: true, immediate: true }
);
{ deep: true, immediate: true },
)
function emitFechar() {
emit("fechar");
emit("fechar")
}
function emitSalvar() {
emit("salvar", {
visiveis: [...visiveisLocal.value],
invisiveis: [...invisiveisLocal.value],
});
})
}
function writeDragData(e: DragEvent, payload: DragPayload) {
try {
e.dataTransfer?.setData(DRAG_MIME, JSON.stringify(payload));
e.dataTransfer?.setData("text/plain", payload.rotulo);
e.dataTransfer!.effectAllowed = "move";
e.dataTransfer?.setData(DRAG_MIME, JSON.stringify(payload))
e.dataTransfer?.setData("text/plain", payload.rotulo)
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move"
}
} catch {
// ignore
}
@ -183,72 +192,93 @@ export default defineComponent({
function readDragData(e: DragEvent): DragPayload | null {
try {
const raw = e.dataTransfer?.getData(DRAG_MIME);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed.rotulo !== "string" || (parsed.origem !== "visiveis" && parsed.origem !== "invisiveis")) {
return null;
const raw = e.dataTransfer?.getData(DRAG_MIME)
if (!raw) return null
const parsed = JSON.parse(raw)
if (
!parsed ||
typeof parsed.rotulo !== "string" ||
(parsed.origem !== "visiveis" && parsed.origem !== "invisiveis")
) {
return null
}
return parsed as DragPayload;
return parsed as DragPayload
} catch {
return null;
return null
}
}
function removerDaOrigem(payload: DragPayload) {
const origemArr = payload.origem === "visiveis" ? visiveisLocal.value : invisiveisLocal.value;
const idx = origemArr.indexOf(payload.rotulo);
if (idx >= 0) origemArr.splice(idx, 1);
const origemArr =
payload.origem === "visiveis"
? visiveisLocal.value
: invisiveisLocal.value
const idx = origemArr.indexOf(payload.rotulo)
if (idx >= 0) origemArr.splice(idx, 1)
}
function inserirNoDestino(destino: OrigemLista, rotulo: string, index: number | null) {
const arr = destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value;
function inserirNoDestino(
destino: OrigemLista,
rotulo: string,
index: number | null,
) {
const arr =
destino === "visiveis" ? visiveisLocal.value : invisiveisLocal.value
// remove duplicatas
const existing = arr.indexOf(rotulo);
if (existing >= 0) arr.splice(existing, 1);
const existing = arr.indexOf(rotulo)
if (existing >= 0) arr.splice(existing, 1)
if (index === null || index < 0 || index > arr.length) {
arr.push(rotulo);
arr.push(rotulo)
} else {
arr.splice(index, 0, rotulo);
arr.splice(index, 0, rotulo)
}
}
function onDragStart(e: DragEvent, rotulo: string, origem: OrigemLista, index: number) {
writeDragData(e, { rotulo, origem, index });
function onDragStart(
e: DragEvent,
rotulo: string,
origem: OrigemLista,
index: number,
) {
writeDragData(e, { rotulo, origem, index })
}
function onDropItem(e: DragEvent, destino: OrigemLista, index: number) {
const payload = readDragData(e);
if (!payload) return;
const payload = readDragData(e)
if (!payload) return
// remove da origem e insere no destino na posição do item
removerDaOrigem(payload);
inserirNoDestino(destino, payload.rotulo, index);
removerDaOrigem(payload)
inserirNoDestino(destino, payload.rotulo, index)
// garante que uma coluna não fique nos 2 lados
if (destino === "visiveis") {
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo);
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1);
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo)
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1)
} else {
const idxVis = visiveisLocal.value.indexOf(payload.rotulo);
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1);
const idxVis = visiveisLocal.value.indexOf(payload.rotulo)
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1)
}
}
function onDropLista(e: DragEvent, destino: OrigemLista, _index: number | null) {
const payload = readDragData(e);
if (!payload) return;
function onDropLista(
e: DragEvent,
destino: OrigemLista,
_index: number | null,
) {
const payload = readDragData(e)
if (!payload) return
removerDaOrigem(payload);
inserirNoDestino(destino, payload.rotulo, null);
removerDaOrigem(payload)
inserirNoDestino(destino, payload.rotulo, null)
if (destino === "visiveis") {
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo);
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1);
const idxInv = invisiveisLocal.value.indexOf(payload.rotulo)
if (idxInv >= 0) invisiveisLocal.value.splice(idxInv, 1)
} else {
const idxVis = visiveisLocal.value.indexOf(payload.rotulo);
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1);
const idxVis = visiveisLocal.value.indexOf(payload.rotulo)
if (idxVis >= 0) visiveisLocal.value.splice(idxVis, 1)
}
}
@ -260,9 +290,9 @@ export default defineComponent({
onDragStart,
onDropItem,
onDropLista,
};
}
},
});
})
</script>
<style>

View file

@ -74,27 +74,39 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, watch } from "vue";
import { EliEntradaTexto, EliEntradaNumero, EliEntradaDataHora } from "../EliEntrada";
import type { ComponenteEntrada, TipoEntrada } from "../EliEntrada/tiposEntradas";
import type { tipoEliTabelaConsulta } from "./types-eli-tabela";
import { computed, defineComponent, type PropType, ref, watch } from "vue"
import {
EliEntradaDataHora,
EliEntradaNumero,
EliEntradaTexto,
} from "../EliEntrada"
import type {
ComponenteEntrada,
TipoEntrada,
} from "../EliEntrada/tiposEntradas"
import type { tipoEliTabelaConsulta } from "./types-eli-tabela"
type FiltroBase<T> = NonNullable<tipoEliTabelaConsulta<T>["filtroAvancado"]>[number];
type FiltroBase<T> = NonNullable<
tipoEliTabelaConsulta<T>["filtroAvancado"]
>[number]
type LinhaFiltro<T> = {
coluna: keyof T;
entrada: ComponenteEntrada;
operador: string;
valor: any;
};
function isTipoEntrada(v: unknown): v is TipoEntrada {
return v === "texto" || v === "numero" || v === "dataHora";
coluna: keyof T
entrada: ComponenteEntrada
operador: string
// biome-ignore lint/suspicious/noExplicitAny: dynamic value
valor: any
}
function isTipoEntrada(v: unknown): v is TipoEntrada {
return v === "texto" || v === "numero" || v === "dataHora"
}
// biome-ignore lint/suspicious/noExplicitAny: dynamic typing needed
function rotuloDoFiltro(f: FiltroBase<any>) {
const rotulo = (f?.entrada?.[1] as any)?.rotulo;
return rotulo ? String(rotulo) : String(f?.coluna ?? "Filtro");
// biome-ignore lint/suspicious/noExplicitAny: dynamic access
const rotulo = (f?.entrada?.[1] as any)?.rotulo
return rotulo ? String(rotulo) : String(f?.coluna ?? "Filtro")
}
export default defineComponent({
@ -102,10 +114,12 @@ export default defineComponent({
props: {
aberto: { type: Boolean, required: true },
filtrosBase: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic filter type
type: Array as PropType<Array<FiltroBase<any>>>,
required: true,
},
modelo: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic model
type: Array as PropType<Array<any>>,
required: true,
},
@ -113,67 +127,82 @@ export default defineComponent({
emits: {
fechar: () => true,
limpar: () => true,
// biome-ignore lint/suspicious/noExplicitAny: dynamic lines
salvar: (_linhas: any[]) => true,
},
setup(props, { emit }) {
const linhas = ref<Array<LinhaFiltro<any>>>([]);
// biome-ignore lint/suspicious/noExplicitAny: dynamic value
const linhas = ref<Array<LinhaFiltro<any>>>([])
const colunaParaAdicionar = ref<string>("");
const colunaParaAdicionar = ref<string>("")
const colunasDisponiveis = computed(() => (props.filtrosBase ?? []).map((b) => String(b.coluna)));
const colunasDisponiveis = computed(() =>
(props.filtrosBase ?? []).map((b) => String(b.coluna)),
)
const opcoesParaAdicionar = computed(() => {
const usadas = new Set(linhas.value.map((l) => String(l.coluna)));
return (props.filtrosBase ?? []).filter((b) => !usadas.has(String(b.coluna)));
});
const usadas = new Set(linhas.value.map((l) => String(l.coluna)))
return (props.filtrosBase ?? []).filter(
(b) => !usadas.has(String(b.coluna)),
)
})
function componenteEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return EliEntradaNumero;
if (tipo === "dataHora") return EliEntradaDataHora;
return EliEntradaTexto;
const tipo = entrada?.[0]
if (tipo === "numero") return EliEntradaNumero
if (tipo === "dataHora") return EliEntradaDataHora
return EliEntradaTexto
}
function opcoesEntrada(entrada: ComponenteEntrada) {
// o rótulo vem do próprio componente (entrada[1].rotulo)
return (entrada?.[1] as any) ?? { rotulo: "" };
// biome-ignore lint/suspicious/noExplicitAny: dynamic options
return (entrada?.[1] as any) ?? { rotulo: "" }
}
function valorInicialPorEntrada(entrada: ComponenteEntrada) {
const tipo = entrada?.[0];
if (tipo === "numero") return null;
return "";
const tipo = entrada?.[0]
if (tipo === "numero") return null
return ""
}
function normalizarModelo() {
const base = props.filtrosBase ?? [];
const modelo = Array.isArray(props.modelo) ? props.modelo : [];
const base = props.filtrosBase ?? []
const modelo = Array.isArray(props.modelo) ? props.modelo : []
// biome-ignore lint/suspicious/noExplicitAny: dynamic model
linhas.value = modelo.map((m: any) => {
// operador vem travado no base
const baseItem = base.find((b) => String(b.coluna) === String(m.coluna)) ?? base[0];
const entrada = (baseItem?.entrada ?? m.entrada) as ComponenteEntrada;
const col = (baseItem?.coluna ?? m.coluna) as any;
const op = String(baseItem?.operador ?? "=");
const val = m.valor ?? valorInicialPorEntrada(entrada);
const baseItem =
base.find((b) => String(b.coluna) === String(m.coluna)) ?? base[0]
const entrada = (baseItem?.entrada ?? m.entrada) as ComponenteEntrada
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
const col = (baseItem?.coluna ?? m.coluna) as any
const op = String(baseItem?.operador ?? "=")
const val = m.valor ?? valorInicialPorEntrada(entrada)
return {
coluna: col,
operador: op,
entrada,
valor: val,
} as LinhaFiltro<any>;
});
// biome-ignore lint/suspicious/noExplicitAny: dynamic cast
} as LinhaFiltro<any>
})
// se vazio e existe base, adiciona 1 linha default
// não auto-adiciona; usuário escolhe quais filtros quer usar
// se algum filtro mudou a coluna para valor inválido, ajusta
for (const l of linhas.value) {
if (!colunasDisponiveis.value.includes(String(l.coluna))) continue;
l.operador = String((base.find((b) => String(b.coluna) === String(l.coluna))?.operador ?? "="));
if (!colunasDisponiveis.value.includes(String(l.coluna))) continue
l.operador = String(
base.find((b) => String(b.coluna) === String(l.coluna))?.operador ??
"=",
)
// sanity
if (l.entrada && !isTipoEntrada(l.entrada[0])) {
l.entrada = ["texto", { rotulo: "Valor" }] as any;
// biome-ignore lint/suspicious/noExplicitAny: dynamic sanity
l.entrada = ["texto", { rotulo: "Valor" }] as any
}
}
}
@ -181,38 +210,42 @@ export default defineComponent({
watch(
() => [props.aberto, props.filtrosBase, props.modelo] as const,
() => {
if (props.aberto) normalizarModelo();
if (props.aberto) normalizarModelo()
},
{ deep: true, immediate: true }
);
{ deep: true, immediate: true },
)
function adicionar() {
if (!colunaParaAdicionar.value) return;
const b0 = (props.filtrosBase ?? []).find((b) => String(b.coluna) === String(colunaParaAdicionar.value));
if (!b0) return;
if (!colunaParaAdicionar.value) return
const b0 = (props.filtrosBase ?? []).find(
(b) => String(b.coluna) === String(colunaParaAdicionar.value),
)
if (!b0) return
// evita repetição
if (linhas.value.some((l) => String(l.coluna) === String(b0.coluna))) return;
if (linhas.value.some((l) => String(l.coluna) === String(b0.coluna)))
return
linhas.value.push({
// biome-ignore lint/suspicious/noExplicitAny: dynamic column
coluna: b0.coluna as any,
entrada: b0.entrada,
operador: String(b0.operador ?? "="),
valor: valorInicialPorEntrada(b0.entrada),
});
})
colunaParaAdicionar.value = "";
colunaParaAdicionar.value = ""
}
function remover(idx: number) {
linhas.value.splice(idx, 1);
linhas.value.splice(idx, 1)
}
function emitFechar() {
emit("fechar");
emit("fechar")
}
function emitLimpar() {
emit("limpar");
emit("limpar")
}
function emitSalvar() {
@ -221,8 +254,8 @@ export default defineComponent({
linhas.value.map((l) => ({
coluna: l.coluna,
valor: l.valor,
}))
);
})),
)
}
return {
@ -238,9 +271,9 @@ export default defineComponent({
emitSalvar,
emitLimpar,
rotuloDoFiltro,
};
}
},
});
})
</script>
<style>

View file

@ -1,56 +1,82 @@
<template>
<nav
v-if="totalPaginasExibidas > 1"
class="eli-tabela__paginacao"
role="navigation"
aria-label="Paginação de resultados"
>
<button
type="button"
class="eli-tabela__pagina-botao"
:disabled="anteriorDesabilitado"
aria-label="Página anterior"
@click="irParaPagina(paginaAtual - 1)"
<div class="eli-tabela__rodape">
<div
v-if="acoes.length > 0"
class="eli-tabela__acoes-inferiores"
style="margin-right: auto"
>
<button
v-for="(botao, indice) in acoes"
:key="`${botao.rotulo}-${indice}`"
type="button"
class="eli-tabela__acao-inferior"
:style="botao.cor ? { borderColor: botao.cor, color: botao.cor } : undefined"
@click="botao.acao(parametrosConsulta)"
>
<component
v-if="botao.icone"
:is="botao.icone"
class="eli-tabela__acoes-cabecalho-icone"
:size="16"
:stroke-width="2"
/>
<span class="eli-tabela__acoes-cabecalho-rotulo">{{ botao.rotulo }}</span>
</button>
</div>
<nav
v-if="totalPaginasExibidas > 1"
class="eli-tabela__paginacao"
role="navigation"
aria-label="Paginação de resultados"
>
<<
</button>
<template v-for="(item, index) in botoes" :key="`${item.label}-${index}`">
<span
v-if="item.ehEllipsis"
class="eli-tabela__pagina-ellipsis"
aria-hidden="true"
>
{{ item.label }}
</span>
<button
v-else
type="button"
class="eli-tabela__pagina-botao"
:class="item.ativo ? 'eli-tabela__pagina-botao--ativo' : undefined"
:disabled="item.ativo"
:aria-current="item.ativo ? 'page' : undefined"
:aria-label="`Ir para página ${item.label}`"
@click="irParaPagina(item.pagina)"
:disabled="anteriorDesabilitado"
aria-label="Página anterior"
@click="irParaPagina(paginaAtual - 1)"
>
{{ item.label }}
&lt;&lt;
</button>
</template>
<button
type="button"
class="eli-tabela__pagina-botao"
:disabled="proximaDesabilitada"
aria-label="Próxima página"
@click="irParaPagina(paginaAtual + 1)"
>
>>
</button>
</nav>
<template v-for="(item, index) in botoes" :key="`${item.label}-${index}`">
<span
v-if="item.ehEllipsis"
class="eli-tabela__pagina-ellipsis"
aria-hidden="true"
>
{{ item.label }}
</span>
<button
v-else
type="button"
class="eli-tabela__pagina-botao"
:class="item.ativo ? 'eli-tabela__pagina-botao--ativo' : undefined"
:disabled="item.ativo"
:aria-current="item.ativo ? 'page' : undefined"
:aria-label="`Ir para página ${item.label}`"
@click="irParaPagina(item.pagina)"
>
{{ item.label }}
</button>
</template>
<button
type="button"
class="eli-tabela__pagina-botao"
:disabled="proximaDesabilitada"
aria-label="Próxima página"
@click="irParaPagina(paginaAtual + 1)"
>
&gt;&gt;
</button>
</nav>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import { computed, defineComponent, type PropType } from "vue"
export default defineComponent({
name: "EliTabelaPaginacao",
@ -67,10 +93,29 @@ export default defineComponent({
type: Number,
required: false,
},
acoes: {
type: Array as PropType<
Array<{
// biome-ignore lint/suspicious/noExplicitAny: dynamic icon
icone?: any
cor?: string
rotulo: string
// biome-ignore lint/suspicious/noExplicitAny: dynamic action
acao: (params?: any) => void
}>
>,
required: false,
default: () => [],
},
parametrosConsulta: {
// biome-ignore lint/suspicious/noExplicitAny: dynamic params
type: Object as PropType<any>,
required: false,
},
},
emits: {
alterar(pagina: number) {
return Number.isFinite(pagina);
return Number.isFinite(pagina)
},
},
setup(props, { emit }) {
@ -79,12 +124,12 @@ export default defineComponent({
* uma navegação confortável, mesmo quando o consumidor não informa o valor.
*/
const maximoBotoesVisiveis = computed(() => {
const valor = props.maximoBotoes;
const valor = props.maximoBotoes
if (typeof valor === "number" && valor >= 5) {
return Math.floor(valor);
return Math.floor(valor)
}
return 7;
});
return 7
})
/**
* Constrói a lista de botões/reticências que serão exibidos na paginação.
@ -92,62 +137,62 @@ export default defineComponent({
* demais de forma dinâmica ao redor da página atual.
*/
const botoes = computed(() => {
const total = props.totalPaginas;
const atual = props.pagina;
const limite = maximoBotoesVisiveis.value;
const total = props.totalPaginas
const atual = props.pagina
const limite = maximoBotoesVisiveis.value
const resultado: Array<{
label: string;
pagina?: number;
ativo?: boolean;
ehEllipsis?: boolean;
}> = [];
label: string
pagina?: number
ativo?: boolean
ehEllipsis?: boolean
}> = []
const adicionarPagina = (pagina: number) => {
resultado.push({
label: String(pagina),
pagina,
ativo: pagina === atual,
});
};
})
}
const adicionarReticencias = () => {
resultado.push({ label: "…", ehEllipsis: true });
};
resultado.push({ label: "…", ehEllipsis: true })
}
if (total <= limite) {
for (let pagina = 1; pagina <= total; pagina += 1) {
adicionarPagina(pagina);
adicionarPagina(pagina)
}
return resultado;
return resultado
}
const visiveisCentrais = Math.max(3, limite - 2);
let inicio = Math.max(2, atual - Math.floor(visiveisCentrais / 2));
let fim = inicio + visiveisCentrais - 1;
const visiveisCentrais = Math.max(3, limite - 2)
let inicio = Math.max(2, atual - Math.floor(visiveisCentrais / 2))
let fim = inicio + visiveisCentrais - 1
if (fim >= total) {
fim = total - 1;
inicio = fim - visiveisCentrais + 1;
fim = total - 1
inicio = fim - visiveisCentrais + 1
}
adicionarPagina(1);
adicionarPagina(1)
if (inicio > 2) {
adicionarReticencias();
adicionarReticencias()
}
for (let pagina = inicio; pagina <= fim; pagina += 1) {
adicionarPagina(pagina);
adicionarPagina(pagina)
}
if (fim < total - 1) {
adicionarReticencias();
adicionarReticencias()
}
adicionarPagina(total);
adicionarPagina(total)
return resultado;
});
return resultado
})
/**
* Emite a requisição de mudança de página garantindo que o valor esteja
@ -155,18 +200,20 @@ export default defineComponent({
*/
function irParaPagina(pagina: number | undefined) {
if (!pagina) {
return;
return
}
const alvo = Math.min(Math.max(1, pagina), props.totalPaginas);
const alvo = Math.min(Math.max(1, pagina), props.totalPaginas)
if (alvo !== props.pagina) {
emit("alterar", alvo);
emit("alterar", alvo)
}
}
const anteriorDesabilitado = computed(() => props.pagina <= 1);
const proximaDesabilitada = computed(() => props.pagina >= props.totalPaginas);
const paginaAtual = computed(() => props.pagina);
const totalPaginasExibidas = computed(() => props.totalPaginas);
const anteriorDesabilitado = computed(() => props.pagina <= 1)
const proximaDesabilitada = computed(
() => props.pagina >= props.totalPaginas,
)
const paginaAtual = computed(() => props.pagina)
const totalPaginasExibidas = computed(() => props.totalPaginas)
return {
botoes,
@ -175,9 +222,9 @@ export default defineComponent({
proximaDesabilitada,
paginaAtual,
totalPaginasExibidas,
};
}
},
});
})
</script>
<style>
@ -241,4 +288,31 @@ export default defineComponent({
color: rgba(107, 114, 128, 0.85);
font-size: 0.9rem;
}
.eli-tabela__acao-inferior {
display: inline-flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 14px;
border-radius: 8px;
border: 1px solid #16a34a; /* default green */
background: #ffffff;
color: #16a34a;
font-size: 0.875rem;
font-weight: 500;
line-height: 1;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.eli-tabela__acao-inferior:hover,
.eli-tabela__acao-inferior:focus-visible {
background-color: #f0fdf4;
}
.eli-tabela__acao-inferior:focus-visible {
outline: 2px solid #16a34a;
outline-offset: 2px;
}
</style>

View file

@ -8,11 +8,15 @@
</template>
<script lang="ts">
import type { Component } from "vue";
import { computed, defineComponent, PropType } from "vue";
import type { Component } from "vue"
import { computed, defineComponent, type PropType } from "vue"
import type { tipoComponenteCelula, tipoTabelaCelula, tiposTabelaCelulas } from "../types-eli-tabela";
import { registryTabelaCelulas } from "./registryTabelaCelulas";
import type {
tipoComponenteCelula,
tiposTabelaCelulas,
tipoTabelaCelula,
} from "../types-eli-tabela"
import { registryTabelaCelulas } from "./registryTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelula",
@ -24,16 +28,20 @@ export default defineComponent({
},
},
setup(props) {
const tipo = computed(() => props.celula[0] as tipoTabelaCelula);
const dados = computed(() => props.celula[1] as tiposTabelaCelulas[tipoTabelaCelula]);
const tipo = computed(() => props.celula[0] as tipoTabelaCelula)
const dados = computed(
() => props.celula[1] as tiposTabelaCelulas[tipoTabelaCelula],
)
// Observação: mantemos o registry tipado, mas o TS do template não consegue
// fazer narrowing do componente com base em `tipo`, então tipamos como `Component`.
const Componente = computed(() => registryTabelaCelulas[tipo.value] as unknown as Component);
const Componente = computed(
() => registryTabelaCelulas[tipo.value] as unknown as Component,
)
const dadosParaComponente = computed(() => dados.value);
const dadosParaComponente = computed(() => dados.value)
return { Componente, dadosParaComponente };
return { Componente, dadosParaComponente }
},
});
})
</script>

View file

@ -12,14 +12,9 @@
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
// Necessário para `fromNow()`.
dayjs.extend(relativeTime);
import { dayjsbr } from "p-comuns"
import { computed, defineComponent, type PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelulaData",
@ -31,27 +26,27 @@ export default defineComponent({
},
setup({ dados }) {
const textoData = computed(() => {
const valorIso = dados?.valor;
if (!valorIso) return "";
const valorIso = dados?.valor
if (!valorIso) return ""
const formato = dados?.formato ?? "data";
const formato = dados?.formato ?? "data"
if (formato === "relativo") {
return dayjs(valorIso).fromNow();
return dayjsbr(valorIso).fromNow()
}
if (formato === "data_hora") {
// Padrão pt-BR simples (sem depender de locale do dayjs)
return dayjs(valorIso).format("DD/MM/YYYY HH:mm");
// Padrão pt-BR simples (sem depender de locale do dayjsbr)
return dayjsbr(valorIso).format("DD/MM/YYYY HH:mm")
}
// formato === "data"
return dayjs(valorIso).format("DD/MM/YYYY");
});
return dayjsbr(valorIso).format("DD/MM/YYYY")
})
return { dados, textoData };
return { dados, textoData }
},
});
})
</script>
<style>

View file

@ -10,9 +10,22 @@
<span v-else>{{ textoNumero }}</span>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import { computed, defineComponent, type PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelulaNumero",
@ -25,15 +38,15 @@ export default defineComponent({
setup({ dados }) {
const textoNumero = computed(() => {
// Mantemos o comportamento anterior (trocar "." por ","), mas agora suportamos prefixo/sufixo.
const numero = String(dados?.numero).replace(".", ",");
const prefixo = dados?.prefixo?.trim();
const sufixo = dados?.sufixo?.trim();
const numero = String(dados?.numero).replace(".", ",")
const prefixo = dados?.prefixo?.trim()
const sufixo = dados?.sufixo?.trim()
const inicio = prefixo ? `${prefixo} ` : "";
const fim = sufixo ? ` ${sufixo}` : "";
const inicio = prefixo ? `${prefixo} ` : ""
const fim = sufixo ? ` ${sufixo}` : ""
return `${inicio}${numero}${fim}`;
});
return `${inicio}${numero}${fim}`
})
return { dados, textoNumero }
},

View file

@ -23,10 +23,10 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { VChip } from "vuetify/components";
import { defineComponent, type PropType } from "vue"
import { VChip } from "vuetify/components"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelulaTags",
@ -38,9 +38,9 @@ export default defineComponent({
},
},
setup({ dados }) {
return { dados };
return { dados }
},
});
})
</script>
<style>

View file

@ -11,8 +11,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import { defineComponent, type PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelulaTextoSimples",
@ -23,12 +23,9 @@ export default defineComponent({
},
},
data() {
return {
}
},
methods: {
return {}
},
methods: {},
setup({ dados }) {
return { dados }
},

View file

@ -13,8 +13,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas";
import { defineComponent, type PropType } from "vue"
import type { tiposTabelaCelulas } from "./tiposTabelaCelulas"
export default defineComponent({
name: "EliTabelaCelulaTextoTruncado",
@ -24,9 +24,9 @@ export default defineComponent({
},
},
setup({ dados }) {
return { dados };
return { dados }
},
});
})
</script>
<style>

View file

@ -1,11 +1,10 @@
import type { Component } from "vue";
import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue";
import EliTabelaCelulaTextoTruncado from "./EliTabelaCelulaTextoTruncado.vue";
import EliTabelaCelulaNumero from "./EliTabelaCelulaNumero.vue";
import EliTabelaCelulaTags from "./EliTabelaCelulaTags.vue";
import EliTabelaCelulaData from "./EliTabelaCelulaData.vue";
import type { tipoTabelaCelula } from "./tiposTabelaCelulas";
import type { Component } from "vue"
import EliTabelaCelulaData from "./EliTabelaCelulaData.vue"
import EliTabelaCelulaNumero from "./EliTabelaCelulaNumero.vue"
import EliTabelaCelulaTags from "./EliTabelaCelulaTags.vue"
import EliTabelaCelulaTextoSimples from "./EliTabelaCelulaTextoSimples.vue"
import EliTabelaCelulaTextoTruncado from "./EliTabelaCelulaTextoTruncado.vue"
import type { tipoTabelaCelula } from "./tiposTabelaCelulas"
export const registryTabelaCelulas = {
textoSimples: EliTabelaCelulaTextoSimples,
@ -13,4 +12,4 @@ export const registryTabelaCelulas = {
numero: EliTabelaCelulaNumero,
tags: EliTabelaCelulaTags,
data: EliTabelaCelulaData,
} as const satisfies Record<tipoTabelaCelula, Component>;
} as const satisfies Record<tipoTabelaCelula, Component>

View file

@ -1,49 +1,48 @@
/**
* Tipagem dos dados de entrada dos componentes de celulas
* Tipagem dos dados de entrada dos componentes de celulas
*/
import type { LucideIcon } from "lucide-vue-next";
import type { LucideIcon } from "lucide-vue-next"
export type tiposTabelaCelulas = {
textoSimples: {
texto: string;
acao?: () => void;
};
texto: string
acao?: () => void
}
textoTruncado: {
texto: string;
acao?: () => void;
};
texto: string
acao?: () => void
}
numero: {
numero: number;
numero: number
/** Texto opcional exibido antes do número (ex.: "R$", "≈"). */
prefixo?: string;
prefixo?: string
/** Texto opcional exibido depois do número (ex.: "kg", "%"). */
sufixo?: string;
acao?: () => void;
};
sufixo?: string
acao?: () => void
}
tags: {
opcoes: {
/** Texto exibido dentro da tag. */
rotulo: string;
rotulo: string
/** Cor do chip (segue as cores do Vuetify, ex.: "primary", "success", "error"). */
cor?: string;
cor?: string
/** Ícone (Lucide) opcional exibido antes do rótulo. */
icone?: LucideIcon;
icone?: LucideIcon
/** Ação opcional da tag. Quando existir, o chip vira clicável. */
acao?: () => void;
}[];
};
acao?: () => void
}[]
}
data: {
/** Valor em ISO 8601 (ex.: "2026-01-09T16:15:00Z"). */
valor: string;
valor: string
/** Define o formato de exibição. */
formato: "data" | "data_hora" | "relativo";
formato: "data" | "data_hora" | "relativo"
/** Ação opcional ao clicar no valor. */
acao?: () => void;
};
};
export type tipoTabelaCelula = keyof tiposTabelaCelulas;
acao?: () => void
}
}
export type tipoTabelaCelula = keyof tiposTabelaCelulas

View file

@ -1,40 +1,55 @@
export type EliTabelaColunasConfig = {
/** Rotulos das colunas visiveis (em ordem). */
visiveis: string[];
visiveis: string[]
/** Rotulos das colunas invisiveis. */
invisiveis: string[];
};
invisiveis: string[]
}
const STORAGE_PREFIX = "eli:tabela";
const STORAGE_PREFIX = "eli:tabela"
export function storageKeyColunas(nomeTabela: string) {
return `${STORAGE_PREFIX}:${nomeTabela}:colunas`;
return `${STORAGE_PREFIX}:${nomeTabela}:colunas`
}
function normalizarConfig(valor: unknown): EliTabelaColunasConfig {
if (!valor || typeof valor !== "object") {
return { visiveis: [], invisiveis: [] };
return { visiveis: [], invisiveis: [] }
}
const v = valor as any;
const visiveis = Array.isArray(v.visiveis) ? v.visiveis.filter((x: any) => typeof x === "string") : [];
const invisiveis = Array.isArray(v.invisiveis) ? v.invisiveis.filter((x: any) => typeof x === "string") : [];
return { visiveis, invisiveis };
// biome-ignore lint/suspicious/noExplicitAny: dynamic config
const v = valor as any
const visiveis = Array.isArray(v.visiveis)
? // biome-ignore lint/suspicious/noExplicitAny: dynamic array item
v.visiveis.filter((x: any) => typeof x === "string")
: []
const invisiveis = Array.isArray(v.invisiveis)
? // biome-ignore lint/suspicious/noExplicitAny: dynamic array item
v.invisiveis.filter((x: any) => typeof x === "string")
: []
return { visiveis, invisiveis }
}
export function carregarConfigColunas(nomeTabela: string): EliTabelaColunasConfig {
export function carregarConfigColunas(
nomeTabela: string,
): EliTabelaColunasConfig {
try {
const raw = window.localStorage.getItem(storageKeyColunas(nomeTabela));
if (!raw) return { visiveis: [], invisiveis: [] };
return normalizarConfig(JSON.parse(raw));
const raw = window.localStorage.getItem(storageKeyColunas(nomeTabela))
if (!raw) return { visiveis: [], invisiveis: [] }
return normalizarConfig(JSON.parse(raw))
} catch {
return { visiveis: [], invisiveis: [] };
return { visiveis: [], invisiveis: [] }
}
}
export function salvarConfigColunas(nomeTabela: string, config: EliTabelaColunasConfig) {
export function salvarConfigColunas(
nomeTabela: string,
config: EliTabelaColunasConfig,
) {
try {
window.localStorage.setItem(storageKeyColunas(nomeTabela), JSON.stringify(normalizarConfig(config)));
window.localStorage.setItem(
storageKeyColunas(nomeTabela),
JSON.stringify(normalizarConfig(config)),
)
} catch {
// ignore
}
@ -42,7 +57,7 @@ export function salvarConfigColunas(nomeTabela: string, config: EliTabelaColunas
export function limparConfigColunas(nomeTabela: string) {
try {
window.localStorage.removeItem(storageKeyColunas(nomeTabela));
window.localStorage.removeItem(storageKeyColunas(nomeTabela))
} catch {
// ignore
}

View file

@ -1,26 +1,35 @@
export type EliTabelaFiltroAvancadoSalvo<T> = Array<{
coluna: keyof T
// biome-ignore lint/suspicious/noExplicitAny: dynamic value
valor: any
}>;
}>
function key(nomeTabela: string) {
return `eli_tabela:${nomeTabela}:filtro_avancado`;
return `eli_tabela:${nomeTabela}:filtro_avancado`
}
export function carregarFiltroAvancado<T>(nomeTabela: string): EliTabelaFiltroAvancadoSalvo<T> {
export function carregarFiltroAvancado<T>(
nomeTabela: string,
): EliTabelaFiltroAvancadoSalvo<T> {
try {
const raw = localStorage.getItem(key(nomeTabela));
if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? (parsed as EliTabelaFiltroAvancadoSalvo<T>) : ([] as any);
const raw = localStorage.getItem(key(nomeTabela))
if (!raw) return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>
const parsed = JSON.parse(raw)
return Array.isArray(parsed)
? (parsed as EliTabelaFiltroAvancadoSalvo<T>)
: // biome-ignore lint/suspicious/noExplicitAny: dynamic cast
([] as any)
} catch {
return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>;
return [] as unknown as EliTabelaFiltroAvancadoSalvo<T>
}
}
export function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFiltroAvancadoSalvo<T>) {
export function salvarFiltroAvancado<T>(
nomeTabela: string,
filtros: EliTabelaFiltroAvancadoSalvo<T>,
) {
try {
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? []));
localStorage.setItem(key(nomeTabela), JSON.stringify(filtros ?? []))
} catch {
// ignore
}
@ -28,7 +37,7 @@ export function salvarFiltroAvancado<T>(nomeTabela: string, filtros: EliTabelaFi
export function limparFiltroAvancado(nomeTabela: string) {
try {
localStorage.removeItem(key(nomeTabela));
localStorage.removeItem(key(nomeTabela))
} catch {
// ignore
}

View file

@ -1,7 +1,6 @@
export { default as EliTabela } from "./EliTabela.vue";
export * from "./types-eli-tabela";
export * from "./celulas/tiposTabelaCelulas";
export * from "./celulas/tiposTabelaCelulas"
export { default as EliTabela } from "./EliTabela.vue"
export * from "./types-eli-tabela"
// Helper para construção de células tipadas.
export { celulaTabela } from "./types-eli-tabela";
export { celulaTabela } from "./types-eli-tabela"

View file

@ -1,18 +1,19 @@
import type { tipoResposta } from "p-respostas";
import type { LucideIcon } from "lucide-vue-next";
import type { tipoTabelaCelula, tiposTabelaCelulas } from "./celulas/tiposTabelaCelulas";
import { operadores, zFiltro } from "p-comuns";
import { ComponenteEntrada } from "../EliEntrada/tiposEntradas";
import type { LucideIcon } from "lucide-vue-next"
import type { operadores, zFiltro } from "p-comuns"
import type { tipoResposta } from "p-respostas"
import type { ComponenteEntrada } from "../EliEntrada/tiposEntradas"
import type {
tiposTabelaCelulas,
tipoTabelaCelula,
} from "./celulas/tiposTabelaCelulas"
// `p-comuns` expõe `zFiltro` (schema). Inferimos o tipo a partir do `parse`.
export type tipoFiltro = ReturnType<(typeof zFiltro)["parse"]>
export type tipoComponenteCelulaBase<T extends tipoTabelaCelula> =
readonly [T, tiposTabelaCelulas[T]]
export type tipoComponenteCelulaBase<T extends tipoTabelaCelula> = readonly [
T,
tiposTabelaCelulas[T],
]
export type tipoComponenteCelula = {
[K in tipoTabelaCelula]: tipoComponenteCelulaBase<K>
@ -25,54 +26,60 @@ export const celulaTabela = <T extends tipoTabelaCelula>(
return [tipo, dados] as const
}
export type { tipoTabelaCelula, tiposTabelaCelulas };
export type { tipoTabelaCelula, tiposTabelaCelulas }
export type tipoEliColuna<T> = {
/** Texto exibido no cabeçalho da coluna. */
rotulo: string;
rotulo: string
/** Função responsável por renderizar o conteúdo da célula. */
celula: (linha: T) => tipoComponenteCelula;
celula: (linha: T) => tipoComponenteCelula
/** Ação opcional disparada ao clicar na célula. */
/**
* Campo de ordenação associado à coluna. Caso informado, a coluna passa a
* exibir controles de ordenação e utiliza o valor como chave para o backend.
*/
coluna_ordem?: keyof T;
coluna_ordem?: keyof T
/**
* indica que a coluna será visivel, se false incia em detalhe
* Caso tenha salvo a propriedade de visibilidade será adotado a propriedade salva
*/
visivel: boolean
};
}
export type tipoEliConsultaPaginada<T> = {
/** Registros retornados na consulta. */
valores: T[];
valores: T[]
/** Total de registros disponíveis no backend. */
quantidade: number;
};
quantidade: number
}
export type tipoEliTabelaAcao<T> = {
/** Ícone (Lucide) exibido para representar a ação. */
icone: LucideIcon;
icone: LucideIcon
/** Cor aplicada ao ícone e rótulo. */
cor: string;
cor: string
/** Texto descritivo da ação. */
rotulo: string;
rotulo: string
/** Função executada quando o usuário ativa a ação. */
acao: (linha: T) => void;
acao: (linha: T) => void
/**
* Define se a ação deve ser exibida para a linha. Pode ser um booleano fixo
* ou uma função (sincrona/assíncrona) que recebe a linha para decisão dinâmica.
*/
exibir?: boolean | ((linha: T) => Promise<boolean> | boolean);
};
exibir?: boolean | ((linha: T) => Promise<boolean> | boolean)
}
export type parametrosConsulta<T> = {
filtros?: tipoFiltro[]
coluna_ordem?: keyof T
direcao_ordem?: "asc" | "desc"
offSet?: number
limit?: number
/** Texto digitado na caixa de busca, quando habilitada. */
texto_busca?: string
}
/**
* Estrutura de dados para uma tabela alimentada por uma consulta.
@ -85,45 +92,40 @@ export type tipoEliTabelaConsulta<T> = {
/** nome da tabela, um identificador unico */
nome: string
/** Indica se a caixa de busca deve ser exibida acima da tabela. */
mostrarCaixaDeBusca?: boolean;
mostrarCaixaDeBusca?: boolean
/** Lista de colunas da tabela. */
colunas: tipoEliColuna<T>[];
colunas: tipoEliColuna<T>[]
/** Quantidade de registros solicitados por consulta (padrão `10`). */
registros_por_consulta?: number;
registros_por_consulta?: number
/**
* Função responsável por buscar os dados. Recebe parâmetros opcionais de
* ordenação (`coluna_ordem`/`direcao_ordem`) e paginação (`offSet`/`limit`).
*/
consulta: (parametrosConsulta?: {
filtros?: tipoFiltro[]
coluna_ordem?: keyof T;
direcao_ordem?: "asc" | "desc";
offSet?: number;
limit?: number;
/** Texto digitado na caixa de busca, quando habilitada. */
texto_busca?: string;
}) => Promise<tipoResposta<tipoEliConsultaPaginada<T>>>;
consulta: (
parametrosConsulta?: parametrosConsulta<T>,
) => Promise<tipoResposta<tipoEliConsultaPaginada<T>>>
/** Quantidade máxima de botões exibidos na paginação (padrão `7`). */
maximo_botoes_paginacao?: number;
maximo_botoes_paginacao?: number
/** Mensagem exibida quando a consulta retorna ok porém sem dados. */
mensagemVazio?: string;
mensagemVazio?: string
/** Ações exibidas à direita de cada linha. */
acoesLinha?: tipoEliTabelaAcao<T>[];
acoesLinha?: tipoEliTabelaAcao<T>[]
/**
* Configurações dos botões que serão inseridos a direita da caixa de busca.
* Seu uso mais comum será para criar novos registros, mas poderá ter outras utilidades.
*/
acoesTabela?: {
/** superio será exibido a direita da caixa de busca, inferior a direita da paginação */
posicao: "superior" | "inferior"
/** Ícone (Lucide) exibido no botão */
icone?: LucideIcon;
icone?: LucideIcon
/** Cor aplicada ao botão. */
cor?: string;
cor?: string
/** Texto descritivo da ação. */
rotulo: string;
rotulo: string
/** Função executada ao clicar no botão. */
acao: () => void;
acao: (parametrosConsulta?: parametrosConsulta<T>) => void
/**
* Callback opcional para forçar atualização da consulta.
@ -135,14 +137,11 @@ export type tipoEliTabelaConsulta<T> = {
* Observação: o componente `EliTabela` pode ignorar isso dependendo do modo de uso.
*/
editarLista?: (lista: T[]) => Promise<T[]>
}[];
filtroAvancado?: {
coluna: keyof T,
operador: operadores | keyof typeof operadores,
entrada: ComponenteEntrada
}[]
};
filtroAvancado?: {
coluna: keyof T
operador: operadores | keyof typeof operadores
entrada: ComponenteEntrada
}[]
}