Inicio de usuários

This commit is contained in:
LeoMortari
2025-11-02 20:39:57 -03:00
parent 91c3cd42f6
commit e95d33f172
4 changed files with 266 additions and 285 deletions

View File

@@ -6,7 +6,7 @@
<meta description="This project is a front-end base project with Vue app" /> <meta description="This project is a front-end base project with Vue app" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>Vue Base JS</title> <title>Clipper AI</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -176,23 +176,6 @@
</p> </p>
</div> </div>
</q-scroll-area> </q-scroll-area>
<div class="app-drawer__footer">
<div class="app-drawer__cta">
<div class="app-drawer__cta-title">Precisa de ajuda?</div>
<div class="app-drawer__cta-text">
Nossa equipe está pronta para acelerar seus cortes.
</div>
<Button
color="secondary"
text-color="primary"
variant="ghost"
icon="sym_o_help"
label="Abrir suporte"
full-width
/>
</div>
</div>
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>

View File

@@ -103,19 +103,6 @@
</q-form> </q-form>
<p v-if="error" class="login-card__error">{{ error }}</p> <p v-if="error" class="login-card__error">{{ error }}</p>
<div class="login-card__footer">
<span>Precisa de ajuda?</span>
<Button
variant="ghost"
color="secondary"
text-color="primary"
icon="sym_o_chat"
label="Falar com suporte"
full-width
@click.prevent="handleSupport"
/>
</div>
</q-card> </q-card>
</section> </section>
</div> </div>
@@ -244,7 +231,7 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.login-page { .login-page {
min-height: 100vh; height: 100vh;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: center; justify-content: center;

View File

@@ -4,203 +4,246 @@
<div> <div>
<h2 class="app-page__title">Gestão de usuários</h2> <h2 class="app-page__title">Gestão de usuários</h2>
<p class="app-page__subtitle"> <p class="app-page__subtitle">
Controle quem pode acessar o Clipper IA, convide novos membros e acompanhe o status de cada conta. Controle quem pode acessar o Clipper IA, convide novos membros e
acompanhe o status de cada conta.
</p> </p>
</div> </div>
<div class="app-actions-bar"> <div class="app-actions-bar">
<Button <Button
icon="sym_o_person_add" variant="ghost"
label="Convidar usuário" icon="sym_o_refresh"
@click="handleInvite" label="Atualizar"
:disabled="loading"
@click="handleRefresh"
/> />
<Button <Button
variant="ghost" icon="sym_o_person_add"
color="secondary" label="Adicionar Usuário"
text-color="primary" @click="handleNewUser"
icon="sym_o_download"
label="Exportar"
@click="handleExport"
/> />
</div> </div>
</section> </section>
<div class="users-page__grid"> <q-card flat bordered class="users-page__filters app-card">
<q-card flat bordered class="app-card users-page__card"> <div class="users-page__filters-head">
<header class="users-page__card-head">
<div> <div>
<h3>Times e permissões</h3> <h3>Filtrar usuários</h3>
<span>Use os níveis de acesso para definir o que cada grupo pode operar.</span> <span
>Busque por nome ou e-mail para localizar rapidamente qualquer
membro.</span
>
</div> </div>
<div class="users-page__filters-actions">
<Button <Button
variant="ghost" variant="ghost"
color="secondary" color="secondary"
text-color="primary" text-color="primary"
icon="sym_o_settings" icon="sym_o_close"
label="Configurar permissões" label="Limpar filtros"
@click="handlePermissions" :disabled="!hasFilters"
@click="resetFilters"
/> />
</header>
<div class="users-page__roles">
<q-item v-for="role in roles" :key="role.name" class="users-page__role-item">
<q-item-section avatar>
<q-avatar :color="role.color" text-color="white" icon="sym_o_badge" />
</q-item-section>
<q-item-section>
<q-item-label>{{ role.name }}</q-item-label>
<q-item-label caption>{{ role.description }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-chip outline color="primary" :label="role.membersLabel" />
</q-item-section>
</q-item>
</div>
</q-card>
<q-card flat bordered class="app-card users-page__card">
<header class="users-page__card-head">
<div>
<h3>Status da equipe</h3>
<span>Indicadores rápidos para acompanhar convites e acessos recentes.</span>
</div>
</header>
<div class="users-page__stats">
<div v-for="stat in stats" :key="stat.label" class="users-page__stat">
<div class="users-page__stat-label">{{ stat.label }}</div>
<div class="users-page__stat-value">{{ stat.value }}</div>
<div class="users-page__stat-caption">{{ stat.caption }}</div>
</div> </div>
</div> </div>
<div class="users-page__cta"> <div class="users-page__filters-grid">
<TextField
v-model="filters.name"
label="Nome"
placeholder="Ex.: Ana Silva"
>
<template #prepend>
<q-icon name="sym_o_person" color="primary" />
</template>
</TextField>
<TextField
v-model="filters.email"
label="E-mail"
placeholder="Ex.: ana@clipperia.com"
>
<template #prepend>
<q-icon name="sym_o_alternate_email" color="primary" />
</template>
</TextField>
</div>
<div class="users-page__filters-footer">
<Button <Button
variant="ghost" label="Buscar usuários"
icon="sym_o_smart_toy" icon="sym_o_search"
label="Automatizar aprovações" :loading="loading"
@click="handleWorkflow" :disabled="loading"
/> @click="handleSearch({ ...pagination, page: 1 })"
<Button
icon="sym_o_notifications_active"
label="Ativar alertas"
@click="handleAlerts"
/> />
</div> </div>
</q-card> </q-card>
</div>
<q-card flat bordered class="app-card users-page__roadmap"> <Table
<header class="users-page__card-head"> key="users-table"
<div> class="users-page__table"
<h3>Próximas entregas</h3> title="Usuários"
<span>Fique de olho nos recursos que estão chegando para a gestão da sua equipe.</span> subtitle="Visualize quem tem acesso ao Clipper IA e acompanhe o status de cada conta."
</div> :columns="columns"
</header> :rows="rows"
:pagination="pagination"
:loading="loading"
@update:pagination="updatePagination"
>
<template #empty-message>
Nenhum usuário encontrado para os filtros selecionados.
</template>
<ul class="users-page__roadmap-list"> <template #body="props">
<li v-for="item in roadmap" :key="item.title"> <q-tr :props="props">
<div class="users-page__roadmap-title">{{ item.title }}</div> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<p class="users-page__roadmap-description">{{ item.description }}</p> <div class="users-page__cell">
</li> <div class="users-page__cell-label">{{ col.label }}</div>
</ul> <div class="users-page__cell-value">{{ col.value }}</div>
</q-card> </div>
</q-td>
</q-tr>
</template>
</Table>
</div> </div>
</template> </template>
<script> <script>
import Button from "@components/Button"; import Button from "@components/Button";
import Table from "@components/Table";
import TextField from "@components/TextField";
import { API } from "@config/axios";
const columns = [
{
name: "id",
label: "ID",
align: "left",
field: "id",
},
{
name: "name",
label: "Nome",
align: "left",
field: "name",
},
{
name: "email",
label: "E-mail",
align: "left",
field: "email",
},
{
name: "role",
label: "Papel",
align: "center",
field: "role",
},
{
name: "status",
label: "Status",
align: "center",
field: "status",
},
{
name: "lastAccess",
label: "Último acesso",
align: "center",
field: "lastAccess",
},
];
export default { export default {
name: "UsersPage", name: "UsersPage",
components: { components: {
Button, Button,
Table,
TextField,
}, },
data() { data() {
return { return {
roles: [ columns,
{ rows: [],
name: "Administradores", loading: false,
description: "Acesso total às configurações, convites e billing.", pagination: {
membersLabel: "3 membros", page: 1,
color: "primary", direction: "asc",
perPage: 10,
total: 0,
totalPages: 1,
hasNext: false,
hasPrev: false,
}, },
{ filters: {
name: "Editores", name: "",
description: "Podem criar cortes, revisar sugestões e publicar.", email: "",
membersLabel: "8 membros",
color: "secondary",
}, },
{
name: "Leitores",
description: "Acompanham relatórios e status dos clipes gerados.",
membersLabel: "12 membros",
color: "accent",
},
],
stats: [
{
label: "Convites pendentes",
value: "04",
caption: "Envie lembretes automáticos em 1 clique",
},
{
label: "Último acesso",
value: "há 2h",
caption: "Bruna adicionou novos cortes",
},
{
label: "Contas bloqueadas",
value: "01",
caption: "Revise motivos e restaure acessos",
},
],
roadmap: [
{
title: "Fluxo de aprovação com múltiplos níveis",
description: "Defina revisores responsáveis por validar cortes antes da publicação automática.",
},
{
title: "Integração com Slack / Discord",
description: "Envie alertas de novos convites e aprovações direto para os canais do time.",
},
{
title: "Perfis avançados",
description: "Crie papéis personalizados combinando permissões de forma granular.",
},
],
}; };
}, },
computed: {
hasFilters() {
return Boolean(this.filters.name) || Boolean(this.filters.email);
},
},
created() {
this.handleSearch(this.pagination);
},
methods: { methods: {
handleInvite() { async handleSearch(pagination = this.pagination) {
this.$q.notify({ const basePagination = {
type: "positive", ...this.pagination,
message: "Convites em lote estarão disponíveis em breve.", page: pagination.page ?? this.pagination.page,
}); perPage: pagination.perPage ?? this.pagination.perPage,
};
try {
this.loading = true;
const {
data: { content, pagination: nextPagination },
} = await API.get("/usuarios", {
params: {
page: basePagination.page,
perPage: basePagination.perPage,
name: this.filters.name,
email: this.filters.email,
}, },
handlePermissions() {
this.$q.notify({
type: "info",
message: "Configurações avançadas de permissões ainda não foram conectadas ao backend.",
}); });
console.log("====================================");
console.log(content);
console.log("====================================");
this.rows = content;
this.pagination = {
...basePagination,
...nextPagination,
};
} catch (error) {
this.$q.notify({
type: "negative",
message: "Não foi possível carregar os usuários simulados.",
});
} finally {
this.loading = false;
}
}, },
handleExport() { updatePagination(pagination) {
this.$q.notify({ this.handleSearch(pagination);
type: "info",
message: "Exportação de usuários será liberada após a integração com o Keycloak.",
});
}, },
handleWorkflow() { resetFilters() {
this.$q.notify({ this.filters = {
type: "info", name: "",
message: "Automação de aprovações está em desenvolvimento.", email: "",
}); };
this.handleSearch({ ...this.pagination, page: 1 });
}, },
handleAlerts() { handleRefresh() {
this.$q.notify({ this.handleSearch({ ...this.pagination });
type: "info",
message: "Em breve você poderá receber alertas no email ou no Slack.",
});
}, },
}, },
}; };
@@ -210,138 +253,106 @@ export default {
.users-page { .users-page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: clamp(32px, 6vw, 48px); gap: 32px;
} }
.users-page__grid { .users-page__filters h3 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: clamp(24px, 4vw, 32px);
}
.users-page__card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.users-page__card-head h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
color: #101828; color: #101828;
} }
.users-page__card-head span { .users-page__filters span {
color: #475467;
font-size: 14px; font-size: 14px;
color: #475467;
} }
.users-page__roles { .users-page__filters-head {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
gap: 12px; align-items: flex-start;
}
.users-page__role-item {
border: 1px solid rgba(127, 86, 217, 0.1);
border-radius: 14px;
padding: 8px 12px;
background: rgba(127, 86, 217, 0.04);
}
.users-page__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px; gap: 16px;
margin-top: 16px; margin-bottom: 24px;
} }
.users-page__stat { .users-page__filters-actions {
padding: 16px;
border-radius: 16px;
border: 1px solid rgba(127, 86, 217, 0.08);
background: rgba(127, 86, 217, 0.05);
display: flex; display: flex;
flex-direction: column; align-items: center;
gap: 6px;
} }
.users-page__stat-label { .users-page__filters-grid {
font-size: 13px; display: grid;
color: #475467; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.users-page__stat-value {
font-size: 24px;
font-weight: 700;
color: #101828;
}
.users-page__stat-caption {
font-size: 12px;
color: #667085;
}
.users-page__cta {
margin-top: 28px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.users-page__roadmap {
display: flex;
flex-direction: column;
gap: 20px; gap: 20px;
} }
.users-page__roadmap-list { .users-page__filters-footer {
margin: 0; display: flex;
padding-left: 0; justify-content: flex-end;
list-style: none; margin-top: 24px;
display: grid;
gap: 18px;
} }
.users-page__roadmap-title { .users-page__table {
margin-bottom: 32px;
}
.users-page__cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.users-page__cell-label {
display: none;
font-size: 12px;
font-weight: 600;
color: #98a2b3;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.users-page__cell-value {
font-size: 15px;
font-weight: 600; font-weight: 600;
color: #101828; color: #101828;
} }
.users-page__roadmap-description { body.body--dark .users-page__filters h3 {
margin: 4px 0 0; color: rgba(255, 255, 255, 0.92);
color: #475467;
} }
body.body--dark .users-page__card-head h3, body.body--dark .users-page__filters span {
body.body--dark .users-page__stat-value,
body.body--dark .users-page__roadmap-title {
color: rgba(255, 255, 255, 0.9);
}
body.body--dark .users-page__card-head span,
body.body--dark .users-page__stat-label,
body.body--dark .users-page__stat-caption,
body.body--dark .users-page__roadmap-description {
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
} }
body.body--dark .users-page__role-item { body.body--dark .users-page__cell-value {
background: rgba(244, 235, 255, 0.1); color: rgba(255, 255, 255, 0.9);
border-color: rgba(244, 235, 255, 0.18);
} }
@media (max-width: 640px) { @media (max-width: 768px) {
.users-page__card-head { .users-page__filters-head {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: flex-start;
} }
.users-page__cta { .app-actions-bar {
flex-direction: column; width: 100%;
align-items: stretch; justify-content: flex-start;
flex-wrap: wrap;
gap: 12px;
}
.users-page__filters-footer {
justify-content: stretch;
}
.users-page__filters-footer > * {
width: 100%;
}
.users-page__cell-label {
display: block;
} }
} }
</style> </style>