📌 CRUD com Vue
Neste material, percorremos juntos todas as etapas da construção de um CRUD completo no Vue.js. Começamos com a configuração do ambiente de desenvolvimento, passando pela criação dos modelos de dados, implementação dos serviços da API com Axios, estruturação das stores com Pinia, e finalizamos com a construção da interface do usuário para interação com o sistema.
👩💻 Principais conceitos abordados:
- Modelagem de dados 📌: Criamos as entidades Cidade, Cliente e Pedido, estruturando seus atributos e relacionamentos.
- Configuração do Axios 🌐: Implementamos chamadas HTTP para conectar nosso front-end à API.
- Gerenciamento de estado com Pinia 🍍: Utilizamos stores para facilitar a manipulação dos dados da aplicação.
- Criação dos componentes Vue 🏗️: Desenvolvemos telas interativas com botões de ação, tabelas dinâmicas e modais intuitivos.
Agora, você tem em mãos um CRUD funcional, organizado e preparado para ser expandido com novas funcionalidades! 🎯 Se quiser aprimorar ainda mais o projeto, pode incluir autenticação, filtros avançados, paginação e muito mais! ✨
Parabéns por chegar até aqui! 🎉 Agora é hora de testar o que foi aprendido e aplicar esses conhecimentos em seus próprios projetos. Bora codar? 🚀💻
Para realizar essa tarefa, nosso ponto de partida será o seguinte diagrama que contém o nome das entidades e seus atributos:
📜 PRÉ REQUISITOS
Conhecimentos básicos prévios:
- HTML - W3Schools - HTML Tutorial
- CSS - W3Schools - CSS Tutorial
- JavaScript - W3Schools - JavaScript Tutorial
⛏️ FERRAMENTAS
Vamos utilizar ferramentas que irão aumentar um pouco a complexidade do desenvolvimento web, mas, para projetos grandes facilitam o trabalho do programador e a manutenibilidade do código.
-
🔵Visual Studio Code: Um editor de código-fonte leve e que pode ser expandido com diversas funcionalidades, criado pela Microsoft. Ele é compatível com várias linguagens de programação, inclui um depurador embutido e uma vasta gama de extensões disponíveis.
-
🟢Vue.js: Um framework que evolui conforme as necessidades para criar interfaces de usuário. Destaca-se pela sua capacidade de reatividade e pela maneira como permite a composição de componentes, sendo uma opção ágil e versátil para o desenvolvimento de front-end.
-
🔵Vuetiy: um framework de componentes Vue construída desde o início para ser fácil de aprender e gratificante de dominar.
-
⚡Vite: Uma ferramenta inovadora para compilação e desenvolvimento de projetos de front-end, desenvolvida para proporcionar um setup ágil e eficaz, com suporte direto para ES Modules e substituição de módulos quentes (Hot Module Replacement - HMR).
-
🍍Pinia: O novo gerenciador de estado padrão para Vue.js, desenhado para oferecer simplicidade, reatividade e total integração com a Composition API. Ele vem como uma alternativa ao Vuex, prometendo mais performance e uma experiência de uso mais amigável.
-
🔵Axios: Uma biblioteca de JavaScript muito utilizada para efetuar solicitações HTTP, conhecida por sua integração com promessas, interceptores e um manejo simplificado de erros durante a comunicação com APIs.
As funcionalidades ficarão mais fáceis de entender no decorrer do tutorial, quando chegar a hora de utilizar cada uma das ferramentas.
🦝 PADRÃO DE PROJETO
O padrão utilizado é o MVC (Model-View-Controller), ele consiste em separar a aplicação em três camadas com o intuito de desacoplar as funcionalidades facilitando a
🦝 - Refactoring Guru - site sobre padrões de projeto
-
Model
- Representa os dados da aplicação.
- É responsável por gerenciar o estado da aplicação, recuperar informações do banco de dados e aplicar qualquer lógica necessária para a manipulação dos dados.
- O modelo não tem conhecimento da view ou do controller; ele apenas responde a solicitações de dados e notificações de alterações.
-
View
- Exibe a interface e interage com o usuário.
- A view obtém os dados do model e os apresenta de forma que sejam compreensíveis e utilizáveis para o usuário.
- Ela também pode receber entradas do usuário e passá-las para o controller.
-
Controller
- Atua como intermediário entre o model e a view.
- É a parte responsável pelas requisições do usuário, faz a integração entre as duas outras camadas. O controller recebe as entradas do usuário a partir da view, processa essas entradas (interagindo com o model se necessário) e retorna a saída apropriada, que geralmente é feita atualizando a view.
- Ele contém a lógica de controle da aplicação, decidindo qual view exibir e quando atualizar o model com novas informações.
🗂️ DIVISÃO
Cada pasta desse projeto possui uma função específica, e parte delas não são criadas quando criamos o projeto com vue, portanto observe as fotos abaixo e crie todas as pastas necessárias (deixe elas vazias por enquanto).
A seguir, uma visão geral da pasta do projeto para se basear aproximadamente como deve ficar:
-
colatina-main: É o repositório raiz, todo o projeto está dentro dele.
-
public: Possui arquivos que não são publicados pelo Vite, como por exemplo o ícone do site que aparece na barra de navegação do navegador.
-
src: É o código fonte, nele está a estrutura do MVC e outras sub pastas
- assets: Arquivos .css, ou seja arquivos que contém o estilo da página, os padrões ficam aqui.
- components: Arquivos .vue, são componentes vue que serão reutilizados, evitando repetição de código desnecessário.
- composable: Arquivos .ts, são funções que são reutilizáveis, como por exemplo o UseApi, que será chamado por várias aplicações.
- controller: Arquivos .ts, gerenciam as regras de negócio. Cada objeto possui seu arquivo.
- Dentro dessa pasta temos uma subpasta chamada store que armazena os estados globais utilizando o Pinia🍍 (é uma biblioteca de gerenciamento de estado).
- model: Arquivos .ts, modelo dos dados, onde é declarado os atributos que cada entidade terá.
- apiRoutes: Arquivos ts, possuem a declaração das rotas. As declarações se assemelham à declaração das entidades, mas não confunda, não é a mesma coisa. As rotas apenas complementam a URL base declarada no arquivo api que está na pasta services.
- repositories: Arquivos .ts, contém os métodos que complementam a URL base.
- services: Tem toda a parte de configuração das rotas e configuração, a exemplo da importação do axios.
- plugins: Arquivos .ts, com configurações de plugins de vue, como o vuetify.
- router: Arquivos .ts, configura todas as rotas para as páginas.
- service: Arquivos .ts, é responsável pela comunicação com a API.
- styles: Arquivos .scss, contendo os styles.
- view: Arquivos .vue (geralmente nomeados “index”), as páginas que são acessadas.
- Pastas chamadas “Generics” contém components que utilizam generics
De forma lúdica, para manipular os dados é necessário entrar na pasta model, se precisar de um serviço que consome api, basta entrar na pasta de services, que irá retornar dados na pasta model.
👣 PASSO A PASSO
Introdução
Para criar um CRUD, você pode seguir estas etapas:
- Modelos de Dados: Definir os modelos de dados que você vai manipular. Nesse caso, vamos utilizar os modelos definidos pelo diagrama do CRUD disponível no tópico “O PROJETO”.
- Serviços de API: Criar serviços para comunicação com a API. Isso inclui métodos para criar, ler, atualizar e deletar dados.
- Stores: Configurar stores (usando Pinia🍍) para gerenciar o estado global da aplicação.
- Componentes Vue: Criar componentes Vue para listar, adicionar, editar e deletar dados.
📦 Configurando o ambiente
Vamos por partes, para evitar termos problemas com as futuras importações que iremos fazer. Primeiro, crie uma pasta vazia no seu computador para guardar seu projeto, de preferência em um lugar organizado e que você não vá perder. Em seguida, siga em ordem os passos abaixo para instalar as dependências que serão necessárias para o desenvolvimento do projeto:
- Node.js e NPM
📥 - Download Node.js
-
Após entrar no link acima, procure pelo botão abaixo e baixe o instalador:
-
Após usar o instalador, abra o Windows PowerShell e verifique se tudo foi instalado corretamente. Use o comando abaixo para verificar o NODE:
node -v
Ele deve retornar:
v22.13.1
Agora verifique o NPM:
npm -v
Ele deve retornar:
v11.1.0
❗Lembre-se SEMPRE de selecionar o tipo de terminal
Git Bash
no VSCode❗ -
Agora no VSCode, abra o terminal usando o atalho:
Ctrl + '
para instalar o Vue.js:📥 - Link para download do Vue.js
npm create vue@latest
Durante a instalação, irá aparecer as seguintes opções no terminal, escolha o nome para o projeto e responda sim/não corretamente para adicionar o que será necessário:
Vue.js - The Progressive JavaScript Framework
√ Project name: ... frontEndCRUD
√ Package name: ... restauranteAPI
√ Add TypeScript? ... Yes
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? » Yes
√ Add Prettier for code formatting? ... Yes -
Depois de criar, entre na pasta do projeto e execute os comandos abaixo para rodar o código do vue. Para abrir o código no navegador clique no link localhost pressionando
Ctrl
ou aperte em seguidaO + Enter
.Done. Now run:
cd projetoCRUD
npm install
npm run format
npm run dev -
Após a instalação precisamos ter certeza de que os pacotes que precisamos estão corretamente instalados, então antes de iniciar a programação do código, vá para a pasta do projeto e ainda utilizando o terminal do VSCode siga o passo a passo de verificação:
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install -D vuetify vite-plugin-vuetify
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install axios
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install pinia
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install -D vite
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install vue-router@4
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install sweetalert2
-
Use o comando abaixo, caso tenha dúvidas ou ocorra algum erro, verifique o link abaixo.
npm install vue-material-design-icons
- Caso ocorra algum erro ou as dependências não sejam instaladas corretamente, acesse o Google Drive abaixo e baixe o .zip contendo os arquivos necessários e coloque esses arquivos dentro da pasta node_modules que se encontra dentro da pasta Root do seu projeto.
Após essa configuração do ambiente, você consegue iniciar sua programação sem maiores problemas causados por pacotes não instalados.
Passo 1: Definir estrutura das entidades na pasta model
-
IEntity - entidade genêrica Essa entidade deverá ficar dentor da pasta
generic
que se encontra dentro da pastageneric
, isso é feito para podermos organizar melhor seu conteúdo. Agora, vamos codar a entidade genérica para replicar atributos que são comuns à mais de um entidade. Comece declarando uma interface de nome “IEntity” que tenha o atributo “Id” que será do tipo string, veja o exemplo abaixo:export interface IEntity {
Id: string;
}Lembre-se que é necessário exportar essa interface para que ela seja reaproveitada em outros arquivos. Ao trabalhar com frontend, importar e exportar elementos passa a ser uma prática constante, portanto se torna um costume, não se preocupe.
OBS: Em um projeto, é de extrema importância dialogar com os desenvolvedores backend para que as estruturas de dados sejam padronizadas e na hora de realizar a integração não ocorra erros.
Agora vamos começar a criar as entidades que iremos utilizar diretamente no programa, são elas →
Cidade.ts
, Cliente.ts
e Pedido.ts
. Esses arquivos devem ficar dentro da pasta model
.
--> Cidade
Vamos começar pelo desenvolvimento da entidade Cidade, pois ela é a mais simples e não possui
relacionamentos que exijam tratamento especial durante a implementação. Outras entidades dependem
desses relacionamentos e precisarão de atenção extra nesse processo. Começaremos de forma similar
à entidade genérica, criando uma interface para Cidade. No CRUD do exemplo, a entidade Cidade
terá dois atributos: Id
e Nome
, ambos representados como strings.
export interface ICidade {
Id: string;
Nome: string;
}
Com a interface criada, vamos criar a classe de Cidade. A classe Cidade implementa essa interface
e define um construtor, que recebe os valores de Id
e Nome
no momento da criação do objeto.
export class Cidade implements ICidade {
public constructor (public Id: string, public Nome: string) {
this.Id = Id;
this.Nome = Nome;
}
-
Gabarito código Cidade
export interface ICidade {
Id: string;
Nome: string;
}
export class Cidade implements ICidade {
public constructor(public Id: string, public Nome: string) {
this.Id = Id;
this.Nome = Nome;
}
}
--> Cliente
Agora vamos para a entidade Cliente, que possui uma relação n para 1 com a entidade Cidade.
Apesar de parecer complexo no decorrer do processo você verá que é mais simples do que se imagina.
A implementação da classe Cliente segue o mesmo padrão utilizado para a entidade Cidade.
Primeiro, é criada uma interface ICliente para definir a estrutura do objeto, garantindo que
todos os clientes possuam os atributos necessários. A implementação da interface ICliente e
da classe Cliente segue o padrão utilizado anteriormente, garantindo que a estrutura do objeto
seja bem definida. A interface ICliente estabelece um contrato que todas as instâncias da
entidade Cliente devem seguir, garantindo que cada cliente possua os atributos Id
, Nome
,
Telefone
, Identification
, ClienteCidadeId
e CidadeId
, todos do tipo string
.
export interface ICliente {
Id: string;
Nome: string;
Telefone: string;
Identification: string;
ClienteCidadeId: string;
CidadeId: string;
}
Em seguida, a classe Cliente implementa essa interface e define um construtor, que recebe
e inicializa os valores desses atributos ao criar uma instância. O uso de ClienteCidadeId
e
CidadeId
sugere um relacionamento entre clientes e cidades, onde CidadeId
pode representar a
cidade associada ao cliente, enquanto ClienteCidadeId
pode indicar um vínculo específico dentro
do sistema, ou seja, é enviado para o backend que irá tratar esse dado como um relacionamento no
banco de dados.
export class Cliente implements ICliente {
public constructor (
public Id: string,
public Nome: string,
public Telefone: string,
public Identification: string,
public ClienteCidadeId: string,
public CidadeId: string,
) {
this.Id = Id
this.Nome = Nome
this.Telefone = Telefone
this.Identification = Identification
this.ClienteCidadeId = ClienteCidadeId
this.CidadeId = CidadeId
}
}
-
Gabarito código Cliente
export interface ICliente {
Id: string
Nome: string
Telefone: string
Identification: string
ClienteCidadeId: string
CidadeId: string
}
export class Cliente implements ICliente {
public constructor (
public Id: string,
public Nome: string,
public Telefone: string,
public Identification: string,
public ClienteCidadeId: string,
public CidadeId: string,
) {
this.Id = Id
this.Nome = Nome
this.Telefone = Telefone
this.Identification = Identification
this.ClienteCidadeId = ClienteCidadeId
this.CidadeId = CidadeId
}
}
--> Pedido
Por último temos o Pedido, o qual possui uma relação 1 para n com Cliente e segue o mesmo padrão da entidade Cliente.
Inicie desenvolvendo a interface IPedido que define a estrutura que qualquer objeto do tipo Pedido deve seguir. Cada pedido possui os atributos Id
, Data
, Valor
, PedidoClienteId
e ClienteId
, sendo Id
, Data
e ClienteId
do tipo string
, enquanto Valor
é um número, representando o valor total do pedido. Já o PedidoClienteId
parece indicar um vínculo específico entre o pedido e o cliente, possivelmente para relacionar o pedido ao cliente dentro do sistema.
export interface IPedido {
Id: string;
Data: string;
Valor: number;
PedidoClienteId: string;
ClienteId: string;
}
A classe Pedido implementa a interface IPedido, e seu construtor recebe os valores para inicializar os atributos de um pedido. Ao criar uma instância da classe, os parâmetros são passados para o construtor, que os armazena nas propriedades correspondentes. Essa estrutura permite que você crie objetos de pedido de forma estruturada e com todas as informações necessárias para interagir com outras entidades, como o cliente e o valor do pedido.
export class Pedido implements IPedido {
public constructor(
public Id: string,
public Data: string,
public Valor: number,
public PedidoClienteId: string,
public ClienteId: string
) {
this.Id = Id;
this.Data = Data;
this.Valor = Valor;
this.PedidoClienteId = PedidoClienteId;
this.ClienteId = ClienteId;
}
}
Assim como os atributos CidadeId
e CidadeClienteId
, os atributos PedidoClienteId
e ClienteId
também estabelecerão a relação entre as entidades no banco de dados.
-
Gabarito código Pedido
export interface IPedido {
Id: string;
Data: string;
Valor: number;
PedidoClienteId: string;
ClienteId: string;
}
export class Pedido implements IPedido {
public constructor(
public Id: string,
public Data: string,
public Valor: number,
public PedidoClienteId: string,
public ClienteId: string
) {
this.Id = Id;
this.Data = Data;
this.Valor = Valor;
this.PedidoClienteId = PedidoClienteId;
this.ClienteId = ClienteId;
}
}
Passo 2: Configuração das rotas e da API com Axios
Com os modelos de dados prontos, vamos iniciar as configurações para utilizar os serviços que irão consumir a API, pois, para podermos codar os métodos que chamarão as funcionalidades do CRUD é necessário ter as rotas delimitadas primeiro. No caso do nosso projeto, iremos utilizar o Axios para fazer as requisições HTTP.
Primeiramente vamos iniciar o axios, vá para a pasta services criada anteriormente e adicione um
arquivo nomeado axios.ts
e insira o conteúdo abaixo:
import axios from "axios";
const axiosServices = axios.create();
axiosServices.interceptors.response.use(
(response) => response,
(error) =>
Promise.reject(
(error.response && error.responde.data) || "Services errados"
)
);
export default axiosServices;
Com a declaração do Axios feita, podemos seguir para a definição da url base que será utilizada,
então na mesma pasta services iremos criar um documento chamado api.ts
e insira o seguinte código:
import axios, { type AxiosInstance } from "axios";
const api: AxiosInstance = axios.create({
baseURL: "https://localhost:3000/api",
});
export default api;
Agora com as configurações feitas podemos ir para o próximo passo e fazer os métodos que irão preencher as rotas e chamar os services.
Passo 3: Pasta repositories, criando os métodos.
- Agora vamos para a pasta
repositories
que se encontra dentro da pastamodel
para iniciar a codificação dos métodos de cada entidade. Lembre-se de criar a pastaapiRoutes
pois precisaremos dela para escrever as rotas, mesmo que ainda não tenhamos criados outros arquivos.
--> Cidade
Começando com a entidade cidade, crie um arquivo com o nome CidadeRepository
esse será o padrão
para nomear os arquivos contidos aqui. Primeiramente, importe o que será necessário para trabalhar,
mas atenção ao escrever as rotas das importações caso você ainda não esteja acostumado com rotas.
import api from '@/services/api'
import type { ICidade } from '../Cidade'
import { Cidade } from '../Cidade'
import CidadeRoutes from './apiRoutes/CidadeRoutes'
Os 3 operadores que usaremos e que são os mais utilizados para manusear rotas são esses:
--> ./ = retorna uma pasta para atrás
--> ../ = retorna duas pastas para atrás
--> @/ = retorna para a pasta raiz do projeto
Agora vamos começar os métodos de fato.
-
Crie a classe default CidadeRepository com uma declaração da importação da api.
export default class CidadeRepository {
apiClient
constructor() {
this.apiClient = api
} -
Crie o método que será responsável pelo nome da entidade. Esse nome é um complemento para a URL base, ou seja, a rota para cumprir determinada funcionalidade, como por exemplo
/cidade
.createBaseRoute() {
return new CidadeRoutes({}).entity
} -
Crie uma função que a rota de exclusão com id específico, como por exemplo
/cidade/{id}
.createDeleteRoute(id: string) {
return new CidadeRoutes({ id: id }).delete
} -
Crie a função que retorna a rota de atualização (update). Essa rota será utilizada para operações de update.
createDeleteRoute(id: string) {
return new CidadeRoutes({ id: id }).delete
}
--> Método que busca todas as cidades: fetchAllCidade.
async fetchAllCidade() {
try {
const baseRoute = this.createBaseRoute()
const response = await this.apiClient.get(baseRoute)
if (!response.data || !response.data.value) {
console.warn('Nenhum dado válido retornado do servidor.')
return []
}
// Filtra cidades para evitar registros inválidos
return response.data.value
.filter((cidade: ICidade) => cidade.Id && cidade.Nome) // Remove dados corrompidos
.map((cidade: ICidade) => new Cidade(cidade.Id, cidade.Nome))
} catch (error) {
console.error('Erro ao buscar cidades:', error)
return [] // Retorna um array vazio para evitar erro no front
}
}
--> Método que cadastra novas cidades: CreateCidade
async createCidade(form: ICidade) {
try {
const baseRoute = this.createBaseRoute()
const response = await this.apiClient.post(baseRoute, form)
return response.data || null // Retorna os dados ou null se falhar
} catch (error) {
console.error('Erro ao criar cidade:', error)
return null
}
}
--> Método que atualiza os dados de uma cidade: Update
A estrutura irá seguir o mesmo padrão, com pequenas modificações.
- Iniciaremos declarando a função, o try catch, e já criar a rota base com o método
createUpdateRoute
. - Agora começa a diferenciar. Com a rota estabelecida vamos criar uma nova cidade. Para isso vamos utilizar um formulário/“form” que será escrito mais para frente. Ele irá dar as informações necessárias para ser criada uma cidade nova. Além disso, é importante lembrar de passar o “form” como um parâmetro para o método.
- Após isso é só retornar a resposta e terminar o catch com uma mensagem de erro.
OBS: O método post já “vem de fábrica” com o Axios, assim como os outros.
async updateCidade(Id: string, form: ICidade) {
try {
const updateRoute = this.createUpdateRoute(Id) // Cria a rota de atualização com o ID
const response = await this.apiClient.put(updateRoute, form)
return response.data || null
} catch (error) {
console.error('Erro ao atualizar cidade:', error)
return null
}
}
--> Método que exclui uma cidade: Delete
Vamos iniciar com a mesma estrutura do método create.
- Declare o método e utilize o método
createBaseRoute
para criar a rota base. - Após isso, reforçamos o id. Basicamente iremos sobrescrever o id para garantir a informação no back-end.
- Finalizamos com a resposta utilizando o método put (também vem no Axios) e o catch de mensagem de erro.
async deleteCidade(Id: string) {
try {
const deleteRoute = this.createDeleteRoute(Id)
const response = await this.apiClient.delete(deleteRoute)
return response.status === 204 // Retorna true se deletado com sucesso
} catch (error) {
console.error('Erro ao excluir cidade:', error)
return false
}
}
O delete possui algumas diferenças.
- Iniciaremos declarando o método, passando como parâmetro o id da cidade e abrindo o try catch.
- Após isso, não chamaremos a função createBaseRoute e sim a deleteRoute, pois iremos passar o id como parâmetro desse método que irá retornar o nome do método e o id da cidade que será excluída.
- Após isso é só montar a resposta utilizando o método delete da api, lembrando de passar a rota de delete criada como parâmetro.
- Para finalizar basta retornar a resposta e o catch de mensagem de erro.
--> Pedido
Agora com a entidade pedido, ainda na mesma pasta crie um arquivo com o nome PedidoRepository
.
Primeiramente, importe o que será necessário para trabalhar, mas atenção ao escrever as rotas
das importações caso você ainda não esteja acostumado com rotas.
import api from "@/services/api";
import type { IPedido } from "../Pedido";
import { Pedido } from "../Pedido";
import PedidoRoutes from "./apiRoutes/PedidoRoutes";
Os 3 operadores que usaremos e que são os mais utilizados para manusear rotas são esses:
--> ./ = retorna uma pasta para atrás
--> ../ = retorna duas pastas para atrás
--> @/ = retorna para a pasta raiz do projeto
Após realizar as importações, vamos definir a classe e as variáveis necessárias:
-
Declaração da Classe PedidoRepository
Crie a classe padrão
PedidoRepository
, declarando a variável que armazenará a instância da API (Axios) e inicialize-a no construtor.export default class PedidoRepository {
apiClient; // Variável para armazenar a instância do axios
constructor() {
this.apiClient = api;
}
} -
Criação da Rota Base
Crie o método que retorna o nome da entidade, que é utilizado como complemento para a URL base. Por exemplo,
/pedido
.createBaseRoute() {
return new PedidoRoutes({}).entity;
} -
Criação da Rota de Exclusão
Crie o método que gera a rota de exclusão para um pedido específico, utilizando o id. Essa rota será usada para operações de delete, como por exemplo
/pedido/{id}
.createDeleteRoute(id: string) {
return new PedidoRoutes({ id: id }).delete;
} -
Crie a função que retorna a rota de atualização (update). Essa rota será utilizada para operações de update.
createDeleteRoute(id: string) {
return new CidadeRoutes({ id: id }).delete
}
Após definir as funções de criação de rotas, vamos para os métodos específicos do CRUD:
--> Método que busca todos os pedidos: fetchAllPedido
async fetchAllPedido() {
try {
// Criar rota de conexão
const baseRoute = this.createBaseRoute()
// Faz a request usando a api com o axios
const response = await this.apiClient.get(baseRoute)
if (!response.data || !response.data.value) {
console.warn('Nenhum dado válido retornado do servidor.')
return []
}
// Retorna a função com a criação de objetos
return response.data.value.map(
(pedido: IPedido) =>
new Pedido(
pedido.Id,
pedido.Data,
pedido.Valor,
pedido.PedidoClienteId,
pedido.ClienteId,
),
)
} catch (error) {
console.error('Erro ao buscar pedidos', error)
return []
}
}
Neste método, iniciamos criando a rota base com o método createBaseRoute
e, em seguida, usamos o método get
do axios para buscar os dados. Os dados retornados são mapeados para instanciar objetos da classe Pedido
.
--> Método que cadastra um novo pedido: createPedido
async createPedido(form: IPedido) {
try {
// Criar rota de conexão
const baseRoute = this.createBaseRoute()
// Faz o post usando a api com o axios e enviando os dados
const response = await this.apiClient.post(baseRoute, form)
// Retorna a resposta do backend
return response.data || null
} catch (error) {
console.error('Erro ao buscar pedidos', error)
return null
}
}
A estrutura do método createPedido
segue o padrão:
- Declarar o método e iniciar o bloco
try/catch
. - Criar a rota base com
createBaseRoute
. - Enviar o formulário (dados do pedido) com o método
post
do axios. - Retornar a resposta ou capturar e logar um eventual erro.
--> Método que atualiza os dados de um pedido: updatePedido
async updatePedido(Id: string, form: IPedido) {
try {
// Criar rota de conexão
const baseRoute = this.createUpdateRoute(Id)
// Faz o put usando a api com o axios e enviando os dados
const response = await this.apiClient.put(baseRoute, form)
// Retorna a resposta do backend
return response.data || null
} catch (error) {
console.error('Erro ao buscar pedidos', error)
return null
}
}
Neste método:
- Declaramos o método e usamos
createBaseRoute
para obter a rota. - Garantimos que o
Id
do pedido esteja presente no formulário de dados. - Utilizamos o método
put
do axios para atualizar as informações. - Retornamos a resposta ou logamos um erro caso ocorra.
--> Método que exclui um pedido: deletePedido
async deletePedido(Id: string) {
try {
// Criar rota de conexão
const deleteRoute = this.createDeleteRoute(Id)
// Faz o put usando a api com o axios e enviando os dados
const response = await this.apiClient.delete(deleteRoute)
// Retorna a resposta do backend
return response.status === 204
} catch (error) {
console.error('Erro ao buscar pedidos', error)
return false
}
}
Para o método de exclusão:
- Declaramos o método passando o id do pedido.
- Utilizamos o método
createDeleteRoute
para montar a rota específica que inclui o id. - Realizamos a requisição
delete
com o axios utilizando a rota criada. - Retornamos a resposta ou, em caso de erro, registramos a mensagem de erro.
--> Cliente
Para iniciarmos, vamos para a pasta repositories
e criar um documento para cada entidade, já que cada uma possui rotas específicas para os métodos. Neste caso, começaremos com a entidade Cliente. Crie um documento identificando que ele pertence à pasta repositories
(ClienteRepository) e inicie as importações necessárias.
import api from '@/services/api'
import type { ICliente } from '../Cliente'
import { Cliente } from '../Cliente'
import ClienteRoutes from './apiRoutes/ClienteRoutes'
Os 3 operadores que usaremos e que são os mais utilizados para manusear rotas são esses:
--> ./ = retorna uma pasta para atrás
--> ../ = retorna duas pastas para atrás
--> @/ = retorna para a pasta raiz do projeto
Após realizar as importações, vamos definir a classe e as variáveis necessárias:
-
Declaração da Classe ClienteRepository
Crie a classe padrão
ClienteRepository
, declarando a variável que armazenará a instância da api (Axios) e inicialize-a no construtor.export default class ClienteRepository {
apiClient; // Variável para armazenar a instância do axios
constructor() {
this.apiClient = api;
}
} -
Criação da Rota Base
Crie o método que retorna o nome da entidade, que é utilizado como complemento para a URL base. Por exemplo,
/cliente
.createBaseRoute() {
return new ClienteRoutes({}).entity;
} -
Criação da Rota de Exclusão
Crie o método que gera a rota de exclusão para um cliente específico, utilizando o id. Essa rota será usada para operações de delete, como por exemplo
/cliente/{id}
.createDeleteRoute(id: string) {
return new ClienteRoutes({ id: id }).delete;
} -
Crie a função que retorna a rota de atualização (update). Essa rota será utilizada para operações de update.
createUpdateRoute(id: string) {
return new ClienteRoutes({ id: id }).update
}
Após definir as funções de criação de rotas, vamos para os métodos específicos do CRUD:
--> Método que busca todos os clientes: fetchAllCliente
Neste método, iniciamos criando a rota base com o método createBaseRoute
e, em seguida, usamos o método get
do axios para buscar os dados. Os dados retornados são mapeados para instanciar objetos da classe Cliente
.
async fetchAllCliente() {
try {
// Cria a rota de conexão
const baseRoute = this.createBaseRoute();
// Realiza a requisição GET utilizando a api (axios)
const response = await this.apiClient.get(baseRoute);
// Retorna a resposta mapeando os dados para
// a criação de objetos Cliente
return response.data.value.map((cliente: ICliente) =>
new Cliente(
cliente.Id,
cliente.Nome,
cliente.Telefone,
cliente.Cpf,
cliente.CidadeId
)
);
} catch (error) {
// Captura qualquer erro que ocorra no bloco try,
// loga a mensagem de
// erro no console e retorna um error
console.error("Erro ao buscar clientes", error);
return error;
}
}
--> Método que cadastra um novo cliente: createCliente
async createCliente(form: ICliente) {
try {
// Cria a rota de conexão
const baseRoute = this.createBaseRoute();
// Realiza o post utilizando a api (axios) e envia
// os dados do formulário
const response = await this.apiClient.post(baseRoute, form);
// Retorna a resposta do backend
return response;
} catch (error) {
console.error("Erro ao criar cliente", error);
return error;
}
}
A estrutura do método createCliente
segue o padrão:
- Declarar o método e iniciar o bloco
try/catch
. - Criar a rota base com
createBaseRoute
. - Enviar o formulário (dados do cliente) com o método
post
do axios. - Retornar a resposta ou capturar e logar um eventual erro.
--> Método que atualiza os dados de um cliente: updateCliente
async updateCliente(Id: string, form: ICliente) {
try {
// Criar rota de conexão
const baseRoute = this.createUpdateRoute(Id)
// Faz o put usando a api com o axios e enviando os dados
const response = await this.apiClient.put(baseRoute, form)
// Retorna a resposta do backend
return response.data || null
} catch (error) {
console.error('Erro ao buscar clientes', error)
return null
}
}
Neste método:
- Declaramos o método e usamos
createBaseRoute
para obter a rota. - Garantimos que o
Id
do cliente esteja presente no formulário de dados. - Utilizamos o método
put
do axios para atualizar as informações. - Retornamos a resposta ou logamos um erro caso ocorra.
--> Método que exclui um cliente: deleteCliente
assync deleteCliente(Id: string) {
try {
// Criar rota de conexão
const deleteRoute = this.createDeleteRoute(Id)
// Faz o put usando a api com o axios e enviando os dados
const response = await this.apiClient.delete(deleteRoute)
// Retorna a resposta do backend
return response.status === 204
} catch (error) {
console.error('Erro ao buscar clientes', error)
return false
}
}
Para o método de exclusão:
- Declaramos o método passando o id do cliente.
- Utilizamos o método
createDeleteRoute
para montar a rota específica que inclui o id. - Realizamos a requisição
delete
com o axios utilizando a rota criada. - Retornamos a resposta ou, em caso de erro, registramos a mensagem de erro.
Passo 4: Controller
Dentro da pasta controller
irá ficar a lógica de controle da aplicação, gerenciando a comunicação
entre a interface e os modelos de dado.
Pra começar vamos para a pasta chamada store
(crie uma se ainda não o fez). Basicamente store é um
padrão de arquitetura que visa armazenar o estado global da aplicação e modificar caso necessário,
sendo assim, será necessário utilizarmos uma nova ferramenta, o Pinea🍍.
Inicialmente vamos criar uma store genérica com o intuito de reutilizar algumas estruturas, e colocar dentro de uma subpasta para separá-lo dos controllers das entidades.
Após criar a pasta e o arquivo vamos seguir os seguintes passos:
-
Importações Necessárias:
-> Importe os tipos e funções necessários, incluindo IEntity, defineStore de Pinia, e ref de Vue.
- IEntity: Interface genérica para entidades.
- defineStore: Função de Pinia para definir uma store.
- ref e Ref: Funções e tipos de Vue para reatividade.
import type { IEntity } from "@/model/generic/IEntity";
import { defineStore } from "pinia";
import { ref, type Ref } from "vue";
-
Definição da Classe GenericStore:
-> Após importar, defina a classe GenericStore que aceita um tipo genérico T que estende IEntity.
- A classe GenericStore é definida como uma classe genérica que aceita um tipo T que estende IEntity. Ela irá gerências uma lista de itens e um estado de carregamento, e vai ser definido os métodos do CRUD.
export class GenericStore<T extends IEntity> {}
- A classe GenericStore é definida como uma classe genérica que aceita um tipo T que estende IEntity. Ela irá gerências uma lista de itens e um estado de carregamento, e vai ser definido os métodos do CRUD.
-
Propriedades da Classe:
-> Defina as propriedades da classe, incluindo items, filteredItems, loading, name, e initialMockFunction.
-
items: Lista reativa de itens do tipo T.
-
filteredItems: Lista reativa de itens filtrados do tipo T.
-
loading: Estado reativo de carregamento.
-
name: Nome da store.
-
initialMockFunction: Função de mock inicial.
items: Ref<T[]> = ref([])
filteredItems: Ref<T[]> = ref([])
loading: Ref<boolean> = ref(false)
private name: string
private initialMockFunction: (() => Promise<T[]>) | null = null
-
-
Construtor da Classe:
-> Implemente o construtor da classe para inicializar a propriedade name.
- Inicializa a propriedade name com o valor passado como argumento.
constructor(name: string) {
this.name = name
}
- Inicializa a propriedade name com o valor passado como argumento.
-
Métodos para Habilitar e Desabilitar Mock:
-> Implemente os métodos enableMock e disableMock para gerenciar a função de mock.
-
enableMock: Define a função de mock.
-
disableMock: Remove a função de mock.
enableMock(mockFunction: () => Promise<T[]>) {
this.initialMockFunction = mockFunction
}
disableMock() {
this.initialMockFunction = null
}
-
-
Método createStore:
-> Implemente o método
createStore
que cria e retorna uma store usando defineStore de Pinia🍍.Inicie declarando a função
createStore
passando o controller como parâmetro, e após isso escreva oreturn
do método. O seu estado inicial inicia com:-
items
: Lista de itens armazenados. -
loading
: Estado de carregamento. -
useMock
: Se um mock está ativo. -
mockFunction
: Referência à função de mock, caso exista.createStore(controller: any) {
// preserva o valor inicial de mockFunction
const initialMockFunction = this.initialMockFunction
return defineStore(this.name, {
state: () => ({
items: this.items,
loading: this.loading,
// armazena se o mock está ativado
useMock: !!initialMockFunction,
// armazena a função mock no estado
mockFunction: initialMockFunction,
}),
actions: {}
-
-> Após isso iremos escrever as actions desse método.
- OBS: As actions são métodos que são definidas dentro do
defineStore
do código do Pinia que permitem manipular o estado da aplicação de forma síncrona ou assíncrona. Elas servem para executar operações como requisições HTTP, chamadas de APIs, manipulação de dados e atualização do estado.
Comece abrindo as actions e vamos utilizar a mesma sequência dos arquivos repository. Primeiramente adicionamos uma função assíncrona fetch que irá buscar os dados da API ou do mock.
Essa função deverá carregar os dados para itens
e ao final, modificar os status de
loading
para false, no intuito de atualizar o estado dos itens.
async fetch(params: string = '') {
this.loading = true
if (this.useMock && this.mockFunction) {
// Usa a função de mock quando ativada
const mockData = await this.mockFunction()
this.items = mockData
} else {
const data = await controller.getAll(params)
this.items = data
}
this.loading = false
},
-> Após a função de carregar, iremos fazer as função de salvamento, uma para salvar dados isolados e uma para salvar dados em conjunto.
-
SALVAR ITEM
async save(item: T) {
this.loading = true
await controller.create(item)
await this.fetch('')
this.loading = false
}, -
SALVAR CONJUNTO DE ITENS
async saveBulk(items: any) {
this.loading = true
await controller.createBulk(items)
await this.fetch('')
this.loading = false
},
As funções updateItem
e updateBulk
são responsáveis por atualizar itens no armazenamento. A
função updateItem
atualiza um único item no armazenamento, chamando o método update
do
controller
para atualizar o item com o Id
fornecido. A função updateBulk
atualiza múltiplos
itens no armazenamento, chamando o método updateBulk
do controller
para atualizar os itens
fornecidos.
-
ATUALIZAR ITEM
async updateItem(Id: string, item: T) {
this.loading = true
await controller.update(Id, item)
await this.fetch('')
this.loading = false
}, -
ATUALIZAR CONJUNTO DE ITENS
async updateBulk(items: any) {
this.loading = true
await controller.updateBulk(items)1
await this.fetch('')
this.loading = false
},
A função deleteItemLocally
remove um item da lista items
localmente, sem fazer uma chamada
ao servidor. Ela filtra a lista items
para excluir o item cujo Id
corresponde ao id
fornecido como argumento.
-
DELETAR ITEM LOCALMENTE
deleteItemLocally(id: string): void {
this.items = this.items.filter((item: any) => item.Id !== id)
}, -
DELETAR ITEM (BANCO DE DADOS)
Vamos declarar a função deleteItem e passar como parâmetro o
id
. Após isso altere o estado do item por meio do loading. Em seguida, abra um try catch e delete o elemento da store e depois chame o delete por meio do controller para remover do backend, e termina atualizando por meio do fetch.async deleteItem(Id: string) {
this.loading = true
try {
this.items = this.items.filter((item) => item.Id !== Id)
await controller.delete(Id)
await this.fetch('')
} catch (error) {
this.loading = false
}
}, -
FILTRAR ITEMS
Para finalizar é criado um método filter para finalizar os elementos por meio de um critério específico, lembrando de selecionar se as fontes virão de dados mock ou API. Assim como nos outro métodos, vamos iniciar alterando o estado (loading) e chamando a estrutura do controller, caso esteja requisitando de um api.
async filter(route: string) {
this.loading = true
if (!this.useMock || !this.mockFunction) {
const data = await controller.filter(route)
this.items = data
} else {
const mockData = await this.mockFunction()
this.items = mockData
}
this.loading = false
},
--> Código Gabarito GenericStore.ts
import type { IEntity } from '@/model/generic/IEntity'
import { defineStore } from 'pinia'
import { ref, type Ref } from 'vue'
export class GenericStore<T extends IEntity> {
items: Ref<T[]> = ref([])
filteredItems: Ref<T[]> = ref([])
loading: Ref<boolean> = ref(false)
private name: string
private initialMockFunction: (() => Promise<T[]>) | null = null
constructor(name: string) {
this.name = name
}
enableMock(mockFunction: () => Promise<T[]>) {
this.initialMockFunction = mockFunction
}
disableMock() {
this.initialMockFunction = null
}
createStore(controller: any) {
const initialMockFunction = this.initialMockFunction // preserva o valor inicial de mockFunction
return defineStore(this.name, {
state: () => ({
items: this.items,
loading: this.loading,
useMock: !!initialMockFunction, // armazena se o mock está ativado
mockFunction: initialMockFunction, // armazena a função mock no estado
}),
actions: {
async fetch(params: string = '') {
this.loading = true
if (this.useMock && this.mockFunction) {
// Usa a função de mock quando ativada
const mockData = await this.mockFunction()
this.items = mockData
} else {
const data = await controller.getAll(params)
this.items = data
}
this.loading = false
},
async save(item: T) {
this.loading = true
await controller.create(item)
await this.fetch('')
this.loading = false
},
async saveBulk(items: any) {
this.loading = true
await controller.createBulk(items)
await this.fetch('')
this.loading = false
},
async updateItem(Id: string, item: T) {
this.loading = true
await controller.update(Id, item)
await this.fetch('')
this.loading = false
},
async updateBulk(items: any) {
this.loading = true
await controller.updateBulk(items)
await this.fetch('')
this.loading = false
},
deleteItemLocally(id: string): void {
this.items = this.items.filter((item: any) => item.Id !== id)
},
async deleteItem(Id: string) {
this.loading = true
try {
this.items = this.items.filter((item) => item.Id !== Id)
await controller.delete(Id)
await this.fetch('')
} catch (error) {
this.loading = false
}
},
async filter(route: string) {
this.loading = true
if (!this.useMock || !this.mockFunction) {
const data = await controller.filter(route)
this.items = data
} else {
const mockData = await this.mockFunction()
this.items = mockData
}
this.loading = false
},
},
})()
}
--> Store Cidade
Este arquivo define uma store que é uma estrutura responsável por gerenciar o estado dos dados das cidades e garantir a consistência das informações. Para codificar, usaremos a classe genérica GenericStore que você definiu anteriormente.
-
Gabarito do código
import type { ICidade } from "@/model/Cidade";
import { Cidade } from "@/model/Cidade";
import CidadeController from "../CidadeController";
import { GenericStore } from "./generic/GenericStore";
const cidadeController = new CidadeController();
const genericStore = new GenericStore<ICidade>("cidade");
const cidades: Cidade[] = [
new Cidade("31773898-7570-43dd-85dd-39cfc88d6c2f", "Vitória"),
new Cidade("31773898-7570-43dd-85dd-39cfc99fs934", "Colatina"),
new Cidade("31773898-7570-43dd-85dd-39cfc88d6c2e", "Serra"),
];
genericStore.enableMock(async () => cidades);
export const useCidadeStore =
genericStore.createStore(cidadeController);
-> Passo a Passo para Codar CidadeStore
- Importações Necessárias:
- Importe os tipos e classes necessários, incluindo ICidade, Cidade, CidadeController, e GenericStore.
- Instanciação do Controller:
- Crie uma instância de CidadeController para gerenciar a lógica de negócios das cidades.
- Instanciação da Store Genérica:
- Crie uma instância de GenericStore passando o tipo ICidade e o nome da store (
'cidade'
).
- Crie uma instância de GenericStore passando o tipo ICidade e o nome da store (
- Definição de Dados Mock:
- Defina uma lista de cidades mockadas para uso durante o desenvolvimento.
- Habilitação do Mock:
- Habilite o mock na store genérica usando a função enableMock.
- Criação e Exportação da Store: - Crie a store usando a função createStore da GenericStore e exporte-a para uso na aplicação. Após isso, vamos para o controller da cidade.
-
Controller Cidade
-
Código
import type { ICidade } from '@/model/Cidade';
import CidadeRepository from '../data/repositories/CidadeRepository';
export default class CidadeController {
cidadeRepository;
constructor() {
this.cidadeRepository = new CidadeRepository()
}
async getAllCidade() {
return await this.cidadeRepository.fetchAllCidade();
}
async create(form: ICidade) {
if (import.meta.env.VITE_MOCK === "true") return
return await this.cidadeRepository.createCidade(form);
}
async update(Id: string, item: ICidade) {
if (import.meta.env.VITE_MOCK === "true") return
return await this.cidadeRepository.updateCidade(Id, item);
async delete(Id: string) {
if (import.meta.env.VITE_MOCK === "true") return
return await this.cidadeRepository.deleteCidade(Id);
}
- Importações Necessárias:
- Importe a interface ICidade que define a estrutura de uma cidade.
- Importe o repositório CidadeRepository que contém os métodos para interagir com os dados de cidade.
- Definição da Classe CidadeController:
- Defina a classe CidadeController que gerenciará a lógica de negócios para cidades.
- Propriedade cidadeRepository:
- Declare a propriedade cidadeRepository que será usada para acessar os métodos do repositório.
- Construtor da Classe:
- Implemente o construtor da classe para inicializar a propriedade cidadeRepository com uma nova instância de CidadeRepository.
- Método getAllCidade:
- Implemente um método assíncrono getAllCidade que busca todas as cidades chamando o método fetchAllCidade do repositório.
- Método create:
- Implemente um método assíncrono create que cria uma nova cidade chamando o método createCidade do repositório.
- Adicione uma verificação para não fazer nada se o mock estiver habilitado (VITE_MOCK).
- Método update:
- Implemente um método assíncrono update que atualiza uma cidade existente chamando o método updateCidade do repositório.
- Adicione uma verificação para não fazer nada se o mock estiver habilitado (VITE_MOCK).
- Método delete:
- Implemente um método assíncrono delete que deleta uma cidade chamando o método deleteCidade do repositório.
- Adicione uma verificação para não fazer nada se o mock estiver habilitado (VITE_MOCK).
-
Agora basta replicar essa estrutura para as outras entidades.
--> Store Pedido
import type { IPedido } from '@/model/Pedido'
import PedidoController from '../PedidoController'
import { GenericStore } from './generic/GenericStore'
const pedidoController = new PedidoController()
const genericStore = new GenericStore<IPedido>('pedido')
const pedidos: Pedido[] = [
new Pedido('31773898-7570-43dd-85dd-39cfc88d6c2f', '2024-10-01', 340),
new Pedido('31773898-7570-43dd-85dd-39cfc99fs934', '2025-01-15', 800)
];
genericStore.enableMock(async () => pedidos);
export const usePedidoStore = genericStore.createStore(pedidoController)
-
Controller Pedido
import type { IPedido } from "@/model/Pedido";
import PedidoRepository from "@/model/repositories/PedidoRepository";
export default class PedidoController {
pedidoRepository;
constructor() {
this.pedidoRepository = new PedidoRepository();
}
async getAll() {
return await this.pedidoRepository.fetchAllPedido();
}
async create(form: IPedido) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.pedidoRepository.createPedido(form);
}
async update(id: string, item: IPedido) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.pedidoRepository.updatePedido(id, item);
}
async delete(id: string) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.pedidoRepository.deletePedido(id);
}
}
--> Store Cliente
import type { ICliente } from '@/model/Cliente'
import ClienteController from '../ClienteController'
import { GenericStore } from './generic/GenericStore'
const clienteController = new ClienteController()
const genericStore = new GenericStore<ICliente>('cliente')
const clientes: Cliente[] = [
new Cliente(
'31773898-7570-43dd-85dd-39cfc88d6c2f',
'Marcos',
'27909872617',
'18374635546',
'31773898-7570-43dd-85dd-39cfc88d6c2f'),
new Cliente(
'31773898-7570-43dd-85dd-39cfc88d6c2e',
'Henrique',
'27981726346',
'01938345786',
'31773898-7570-43dd-85dd-39cfc88d6c2e')
];
genericStore.enableMock(async () => clientes);
export const useClienteStore = genericStore.createStore(clienteController)
-
Controller Cliente
import type { ICliente } from "@/model/Cliente";
import ClienteRepository from "@/data/repositories/ClienteRepository";
export default class ClienteController {
clienteRepository;
constructor() {
this.clienteRepository = new ClienteRepository();
}
async getAllCliente() {
return await this.clienteRepository.fetchAllCliente();
}
async create(form: ICliente) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.clienteRepository.createCliente(form);
}
async update(Id: string, item: ICliente) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.clienteRepository.updateCliente(Id, item);
}
async delete(Id: string) {
if (import.meta.env.VITE_MOCK === "true") return;
return await this.clienteRepository.deleteCliente(Id);
}
}
Passo 5: View
Nessa etapa, iremos fazer os componentes das tela que irão ser mostrados para o usuário. Para isso, cada entidade terá o seu arquivo para que o usuário tenha acesso aos métodos do CRUD. Como de costume iniciaremos com a cidade, por ser a mais simples.
OBS: Essa camada da aplicação possui os arquivos mais extensos de todo o projeto, por isso a estrutura irá mudar um pouco para facilitar a visualização.
Antes de iniciarmos essa etapa existem dois pontos importantes de serem pontuados. Primeiro, ao contrário
de outros frameworks, o Vue não separa os arquivos HTML, CSS e TypeScript de um componente em pastas
separadas. Em vez disso, ele utiliza um único arquivo para agrupar essas três partes, permitindo que
o HTML, o CSS e o TypeScript relacionados a um componente sejam escritos dentro do mesmo arquivo. Isso
pode fazer com que os arquivos .vue
se tornem muito longos. Segundo, para testar a aplicação, basta
acessar a pasta do projeto no VSCode, abrir o terminal, selecionar o terminal bash e, através dele,
executar o comando npm run dev
. Esse comando "compila" o projeto e inicia um servidor, permitindo
que você acesse a aplicação e interaja com ela no navegador.
No navegador, o Vue oferece o Vue Devtools, uma ferramenta que facilita o trabalho do desenvolvedor ao permitir a análise das rotas internas do projeto, pastas, nomes, relações etc. Após essas explicações podemos retornar ao CRUD.
--> Cidade
Todos os códigos criados a partir daqui serão colocados na pasta view
. Para codificar a view,
entre na pasta view e crie um arquivo .vue
seguindo o formato de nome padrão index-cidade.vue
.
No caso da pasta view a organização das subpastas e nomeação dos arquivos é mais maleável pois fica a
critério do tipo de projeto que está sendo criado, no caso do CRUD nenhuma subpasta adicional é
criada pois seria desnecessário, mas sinta-se livre para conhecer diferentes modelos de organização.
Comece o arquivo importando o que será usado:
import { onMounted, ref, watch } from 'vue' // Importando funcionalidades do Vue
As funcionalidades ref
, onMounted
e watch
pertencem ao Vue.js e são utilizados para reatividade
e execução de código após o componente ser montado, ou seja, facilitam a manipulação do ciclo de vida.
import { useCidadeStore } from '@/controller/store/CidadeStore' // Importando o store de Cidade
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
As store's useCidadeStore
e a useClienteStore
são importadas para acessar os métodos de CRUD da cidade com o backend.
import { Cidade } from '@/model/Cidade' // Importando a classe Cidade
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
Os modelos Cidade
e Cliente
são utilizados tipar os dados da nova cidade.
import Swal from 'sweetalert2' // Importando o SweetAlert2
O Sweet Alert
(Swal Fire) será utilizado para programar os alertas que devem aparecer em certas situações da execução do programa.
A seguir vamos definir e inicializar algumas variáveis reativas para controlar a interface e o comportamento do componente. As variáveis showModal
e showDelete
determinam se os respectivos modais estão visíveis ou não, enquanto isEditing
indica se o formulário está em modo de edição ou não.
const cidades = ref() // Variável reativa para armazenar a lista de cidades
// Variáveis reativas para controlar a exibição de modais
const showModal = ref(false)
const showDelete = ref(false)
const isEditing = ref(false) // Variável reativa para controlar se está editando ou não
const newCidade = ref<Cidade>({ Id: '', Nome: '' }) // Variável reativa para armazenar a nova cidade criada ou a cidade atualizada
const itemToDelete = ref<Cidade | null>(null) // Variável reativa para armazenar o item a ser deletado
// Variável reativa para armazenar o cabeçalho da tabela de cidades
// Cada item se relaciona com algum campo que deseja ser exibido
const header = ref([
{ title: 'NOME', key: 'Nome' },
{ title: 'AÇÕES', key: 'actions' },
])
No momento em que o componente é montado, a função getPosts
é chamada, ela utiliza o método fetch
da store useCidadeStore
para buscar as cidades do banco de dados. O resultado da consulta é armazenado na variável reativa cidades
, que é um array que contém todas as cidades existentes. Além disso, essa função utilizada em outras partes do código é responsável pelo "refresh" da página a cada alteração.
onMounted(async () => {
await getPosts()
})
async function getPosts() {
await useCidadeStore.fetch('')
cidades.value = useCidadeStore.items
}
A função cidadeExiste
é utilizada posteriormente no momento em que iremos codificar os alertas para certas ocasiões onde criamos ou atualizamos os dados que estamos manipulando.
async function cidadeExiste(nome: string): Promise<boolean> {
await useCidadeStore.fetch('')
return useCidadeStore.items.some((cidade: Cidade) => cidade.Nome === nome)
}
Agora seguiremos com as funções que chamam métodos CRUD por meio do store.
A primeira função que iremos desenvolver será o create interagindo com a store useCidadeStore
para gerenciar as cidades no banco de dados e atualizar a interface do usuário.
Quando o usuário deseja criar uma nova cidade, a função createCidade()
é chamada. Ela tenta salvar a cidade no banco de dados utilizando o método save
da store, passando os dados da nova cidade que estão armazenados em newCidade.value
. Se a operação for bem-sucedida, o modal de criação é fechado, e a lista de cidades é atualizada chamando a função getPosts()
. Caso haja algum erro durante o processo, um alerta irá aparecer com a explicação do erro ocorrido para o usuário. A programação dos alertas estão contidas nos if
antes do try catch
.
async function createCidade() {
if (newCidade.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await cidadeExiste(newCidade.value.Nome)) {
Swal.fire({
title: 'Cidade já existe!',
icon: 'error',
text: 'Uma cidade com esse nome já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCidade.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome da cidade não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useCidadeStore.save(newCidade.value)
Swal.fire({
title: 'Cidade salva com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro criando cidade: ', error)
Swal.fire({
title: 'Não foi possível salvar!',
text: 'Ocorreu um erro ao salvar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
A função salvarCidade()
é responsável por determinar se a ação é de criação ou edição. Se o estado isEditing.value
for verdadeiro, significa que o usuário está editando uma cidade existente, então a função chama submitEditCidade()
para atualizar os dados da cidade. Caso contrário, a função chama createCidade()
para salvar uma nova cidade.
async function salvarCidade() {
if (isEditing.value) await submitEditCidade();
else await createCidade();
}
Quando o usuário deseja editar uma cidade, a função editCidade()
é chamada. Ela recebe a cidade a ser editada como parâmetro, copia seus dados para o objeto newCidade.value
, e ativa o estado de edição ajustando isEditing.value
para true
. O modal de edição é então aberto para o usuário realizar as alterações desejadas.
function editCidade(item: Cidade) {
newCidade.value = { ...item }
isEditing.value = true
showModal.value = true
}
Após o usuário fazer as alterações, a função submitEditCidade()
é chamada. Ela usa o método updateItem
da store para atualizar a cidade no banco de dados com os novos dados. Após a atualização, o modal é fechado e a lista de cidades é atualizada novamente com a função getPosts()
. Se ocorrer algum erro durante a atualização, um alerta irá aparecer com a explicação do erro ocorrido para o usuário.
async function submitEditCidade() {
if (await cidadeExiste(newCidade.value.Nome)) {
Swal.fire({
title: 'Cidade já existe!',
icon: 'error',
text: 'Uma cidade com esse nome já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCidade.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome da cidade não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useCidadeStore.updateItem(newCidade.value.Id, newCidade.value)
Swal.fire({
title: 'Cidade atualizada com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro alterando cidade: ', error)
Swal.fire({
title: 'Não foi possível alterar!',
text: 'Ocorreu um erro ao alterar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
Antes de codificarmos a função de deletar uma cidade, precisamos criar três breves funções que são necessárias para um funcionamento correto dos alertas de erro e das situações de erro que podem vir a ocorrer. A função confirmDelete
e closeDelete
garantem que os modais de deleção funcionem corretamente e a função cidadePossuiClientes
serve para verificar se um condição de erro foi atingida para gerar um alerta de erro.
function confirmDelete(item: Cidade) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
async function cidadePossuiClientes(id: string): Promise<boolean> {
await useClienteStore.fetch('')
return useClienteStore.items.some((cliente: Cliente) => cliente.ClienteCidadeId === id)
}
Quando o usuário deseja excluir uma cidade, a função deleteCidade()
é acionada. Ela recebe o item a ser deletado como parâmetro, chama o método deleteItem
da store para remover a cidade do banco de dados e, em seguida, atualiza a lista de cidades chamando a função getPosts()
. Caso algum erro aconteça durante o processo de exclusão, um alerta irá aparecer com a explicação do erro ocorrido para o usuário.
async function deleteCidade() {
if (itemToDelete.value !== null) {
if (await cidadePossuiClientes(itemToDelete.value.Id)) {
Swal.fire({
title: 'Não foi possível apagar!',
text: 'A cidade possui clientes ligados a ela.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
return
}
try {
await useCidadeStore.deleteItem(itemToDelete.value.Id)
Swal.fire({
title: 'Deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro deletando cidade: ', error)
Swal.fire({
title: 'Não foi possível apagar!',
text: 'Ocorreu um erro ao apagar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false
}
}
}
Para finalizar a parte do Typescript, utilizamos o watch
para "observar" o estado do modal de deleção e alterá-lo quando necessário.
watch(showDelete, (val) => {
if (!val) closeDelete()
})
Essas funções trabalham de forma integrada, proporcionando ao usuário uma interface para gerenciar as cidades de forma eficiente, realizando as operações de um CRUD (criar, ler, atualizar e excluir) no banco de dados e atualizando a interface conforme necessário.
Para finalizar o arquivo cidade, vamos criar o template com a parte html, ou seja, a interface que exibirá uma lista de cidades cadastradas e permitirá que o usuário manipule as cidades por meio de um modal.
Inicialmente vamos criar um botão (v-btn
) para cadastrar uma nova cidade. Quando o usuário clica nesse botão, a variável showModal
é ativada, abrindo o modal de cadastro. Além disso, a variável isEditing
é definida como false
, indicando que se trata de uma nova cidade, e o objeto newCidade
é resetado com valores vazios para garantir que nenhum dado anterior interfira na criação.
<v-row>
<v-col cols="2" class="d-flex justify-start">
<!-- Botão cadastrar cidade -->
<v-btn
@click="
() => {
showModal = true
isEditing = false
newCidade = { Id: '', Nome: '' }
}
"
class="custom-width-2"
color="primary"
variant="flat"
>
Cadastrar Cidade
</v-btn>
</v-col>
</v-row>
A lista de cidades cadastradas é exibida em uma tabela (v-data-table
), onde os dados são preenchidos dinamicamente com o array cidades
. No cabeçalho da tabela, há duas colunas: uma para o nome da cidade e outra para ações. Na coluna de ações, há dois botões para cada cidade listada. O primeiro botão, representado por um ícone de lápis (mdi-pencil
), permite editar a cidade, chamando a função editCidade(item)
. O segundo botão, com um ícone de lixeira (mdi-delete
), aciona a função deleteCidade(item)
, removendo a cidade da lista. Caso a tabela não tenha registros, um aviso é exibido informando que não há dados disponíveis, utilizando o componente v-label
.
<!-- Tabela de cidades -->
<v-data-table :headers="header" :items="cidades">
<template #item.actions="{ item }">
<!-- Botão editar cidade -->
<v-btn @click="editCidade(item as Cidade)" color="primary" icon>
<mdicon name="pencil"></mdicon>
</v-btn>
<!-- Botão deletar cidade -->
<v-btn @click="confirmDelete(item as Cidade)" color="red" icon>
<mdicon name="delete"></mdicon>
</v-btn>
</template>
<!-- Tabela sem dados -->
<template #no-data>
<v-label>Sem dados!</v-label>
</template>
</v-data-table>
O modal de cadastro e edição é representado por um v-dialog
controlado pela variável showModal
. O título do modal é dinâmico, alternando entre "Editar Resolução" e "Cadastrar Resolução" com base no estado isEditing
. Dentro do modal, há um formulário contendo um campo de texto (v-text-field
) onde o usuário pode inserir ou editar o nome da cidade. O campo é vinculado ao objeto newCidade.Nome
para garantir que as alterações sejam refletidas diretamente no estado da aplicação.
Na parte inferior do modal, há dois botões. O primeiro, "Cancelar", simplesmente fecha o modal ao redefinir showModal
para false
. O segundo, "Salvar", chama a função salvarCidade()
, que decide se a cidade deve ser criada ou atualizada, dependendo do estado isEditing
.
<!-- Modal de Cadastro/Edição -->
<v-dialog v-model="showModal" max-width="500" :style="{ 'z-index': 1050 }">
<v-card>
<!-- Título do modal -->
<v-card-title>
{{ isEditing ? 'Editar Resolução' : 'Cadastrar Resolução' }}
</v-card-title>
<!-- Campo do formulário: Nome -->
<v-card-text>
<v-form>
<v-text-field label="Nome" v-model="newCidade.Nome" required></v-text-field>
</v-form>
</v-card-text>
<!-- Botões de ação do formulário -->
<v-card-actions>
<v-btn @click="showModal = false">Cancelar</v-btn>
<v-btn @click="salvarCidade" color="primary">Salvar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
Por fim, o modal de deleção que é muito semelhante ao modal de cadastro, com algumas diferenças pois esse modal deve apresentar apenas dois botões questionando o usuário se ele deseja realmente deletar ou não o item em questão.
<!-- Modal de confirmação de deleção -->
<v-dialog v-model="showDelete" max-width="500px" :style="{ 'z-index': 1050 }">
<v-card>
<v-card-title class="text-h5 text-center py-6"
>Tem certeza que deseja deletar esse item?</v-card-title
>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
dark
@click="closeDelete"
data-test="buttonCancelarDelecao"
>Cancelar</v-btn
>
<v-btn
color="primary"
variant="flat"
dark
@click="deleteCidade"
data-test="buttonConfirmarDelecao"
>OK</v-btn
>
<v-spacer />
</v-card-actions>
</v-card>
</v-dialog>
Esse código proporciona uma interface intuitiva para o gerenciamento de cidades, permitindo a interação do usuário de forma dinâmica e responsiva.
-
Gabarito código index-cidade.vue
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue' // Importando funcionalidades do Vue
import { useCidadeStore } from '@/controller/store/CidadeStore' // Importando o store de Cidade
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
import { Cidade } from '@/model/Cidade' // Importando a classe Cidade
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
import Swal from 'sweetalert2' // Importando o SweetAlert2
const cidades = ref() // Variável reativa para armazenar a lista de cidades
// Variáveis reativas para controlar a exibição de modais
const showModal = ref(false)
const showDelete = ref(false)
const isEditing = ref(false) // Variável reativa para controlar se está editando ou não
const newCidade = ref<Cidade>({ Id: '', Nome: '' }) // Variável reativa para armazenar a nova cidade
const itemToDelete = ref<Cidade | null>(null) // Variável reativa para armazenar o item a ser deletado
// Variável reativa para armazenar o cabeçalho da tabela
// Cada item se relaciona com algum campo que deseja ser exibido
const header = ref([
{ title: 'NOME', key: 'Nome' },
{ title: 'AÇÕES', key: 'actions' },
])
onMounted(async () => {
await getPosts()
})
async function getPosts() {
await useCidadeStore.fetch('')
cidades.value = useCidadeStore.items
}
async function cidadeExiste(nome: string): Promise<boolean> {
await useCidadeStore.fetch('')
return useCidadeStore.items.some((cidade: Cidade) => cidade.Nome === nome)
}
async function createCidade() {
if (newCidade.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await cidadeExiste(newCidade.value.Nome)) {
Swal.fire({
title: 'Cidade já existe!',
icon: 'error',
text: 'Uma cidade com esse nome já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCidade.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome da cidade não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useCidadeStore.save(newCidade.value)
Swal.fire({
title: 'Cidade salva com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro criando cidade: ', error)
Swal.fire({
title: 'Não foi possível salvar!',
text: 'Ocorreu um erro ao salvar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
async function salvarCidade() {
if (isEditing.value) await submitEditCidade()
else await createCidade()
}
function editCidade(item: Cidade) {
newCidade.value = { ...item }
isEditing.value = true
showModal.value = true
}
async function submitEditCidade() {
if (await cidadeExiste(newCidade.value.Nome)) {
Swal.fire({
title: 'Cidade já existe!',
icon: 'error',
text: 'Uma cidade com esse nome já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCidade.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome da cidade não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useCidadeStore.updateItem(newCidade.value.Id, newCidade.value)
Swal.fire({
title: 'Cidade atualizada com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro alterando cidade: ', error)
Swal.fire({
title: 'Não foi possível alterar!',
text: 'Ocorreu um erro ao alterar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
function confirmDelete(item: Cidade) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
async function cidadePossuiClientes(id: string): Promise<boolean> {
await useClienteStore.fetch('')
return useClienteStore.items.some((cliente: Cliente) => cliente.ClienteCidadeId === id)
}
async function deleteCidade() {
if (itemToDelete.value !== null) {
if (await cidadePossuiClientes(itemToDelete.value.Id)) {
Swal.fire({
title: 'Não foi possível apagar!',
text: 'A cidade possui clientes ligados a ela.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
return
}
try {
await useCidadeStore.deleteItem(itemToDelete.value.Id)
Swal.fire({
title: 'Deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro deletando cidade: ', error)
Swal.fire({
title: 'Não foi possível apagar!',
text: 'Ocorreu um erro ao apagar a cidade.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false
}
}
}
watch(showDelete, (val) => {
if (!val) closeDelete()
})
</script><template>
<v-row>
<v-col cols="2" class="d-flex justify-start">
<!-- Botão cadastrar cidade -->
<v-btn
@click="
() => {
showModal = true
isEditing = false
newCidade = { Id: '', Nome: '' }
}
"
class="custom-width-2"
color="primary"
variant="flat"
>
Cadastrar Cidade
</v-btn>
</v-col>
</v-row>
<!-- Tabela de cidades -->
<v-data-table :headers="header" :items="cidades">
<template #item.actions="{ item }">
<!-- Botão editar cidade -->
<v-btn @click="editCidade(item as Cidade)" color="primary" icon>
<mdicon name="pencil"></mdicon>
</v-btn>
<!-- Botão deletar cidade -->
<v-btn @click="confirmDelete(item as Cidade)" color="red" icon>
<mdicon name="delete"></mdicon>
</v-btn>
</template>
<!-- Tabela sem dados -->
<template #no-data>
<v-label>Sem dados!</v-label>
</template>
</v-data-table>
<!-- Modal de Cadastro/Edição -->
<v-dialog v-model="showModal" max-width="500" :style="{ 'z-index': 1050 }">
<v-card>
<!-- Título do modal -->
<v-card-title>
{{ isEditing ? 'Editar Resolução' : 'Cadastrar Resolução' }}
</v-card-title>
<!-- Campo do formulário: Nome -->
<v-card-text>
<v-form>
<v-text-field label="Nome" v-model="newCidade.Nome" required></v-text-field>
</v-form>
</v-card-text>
<!-- Botões de ação do formulário -->
<v-card-actions>
<v-btn @click="showModal = false">Cancelar</v-btn>
<v-btn @click="salvarCidade" color="primary">Salvar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal de confirmação de deleção -->
<v-dialog v-model="showDelete" max-width="500px" :style="{ 'z-index': 1050 }">
<v-card>
<v-card-title class="text-h5 text-center py-6"
>Tem certeza que deseja deletar esse item?</v-card-title
>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
dark
@click="closeDelete"
data-test="buttonCancelarDelecao"
>Cancelar</v-btn
>
<v-btn
color="primary"
variant="flat"
dark
@click="deleteCidade"
data-test="buttonConfirmarDelecao"
>OK</v-btn
>
<v-spacer />
</v-card-actions>
</v-card>
</v-dialog>
</template>
--> Cliente
O código dos clientes carrega os dados ao iniciar o componente e exibe informações relevantes, além de permitir a interação do usuário para cadastrar e editar clientes. Primeiro, importe todos os componentes necessários:
import { onMounted, ref, watch } from 'vue' // Importando o onMounted e ref do Vue
import { useCidadeStore } from '@/controller/store/CidadeStore' // Importando o store de Cidade
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
import { usePedidoStore } from '@/controller/store/PedidoStore' // Importando o store de Pedido
import { Cidade } from '@/model/Cidade' // Importando a classe Cidade
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
import { Pedido } from '@/model/Pedido' // Importando a classe Pedido
import Swal from 'sweetalert2' // Importando o SweetAlert2
Após as importações devemos declarar as variáveis que utilizaremos no decorrer do código. Os dados são armazenados em variáveis reativas (ref
) para serem utilizados na interface do usuário.
const clientes = ref()
const pedidos = ref()
const cidades = ref<Cidade[]>([])
A variável pedidosCliente
é um array que armazena os pedidos associados ao cliente selecionado. Já itemToDelete
armazena o item que será deletado.
const pedidosCliente = ref<Pedido[]>([])
const itemToDelete = ref<Cliente | null>(null)
As variáveis controlam a exibição de modais: showModal
gerencia o formulário de cadastro e edição de clientes, showPedidosModal
, controla a exibição dos pedidos de um cliente específico e showDelete
que contorla o modal de deleção de clientes.
const showModal = ref(false)
const showDelete = ref(false)
const showPedidosModal = ref(false)
O objeto newCliente
armazena temporariamente os dados do cliente e a variável isEditing
define se o código está criando ou editando um cliente.
const isEditing = ref(false)
const newCliente = ref<Cliente>({
Id: '',
Nome: '',
Telefone: '',
Identification: '',
ClienteCidadeId: '',
CidadeId: '',
})
Também é preciso criar os cabeçalhos das tabelas que serão exibidas ao usuário, especificando quais colunas devem ser exibidas para clientes e pedidos.
const header = ref([
// Cabeçalho da tabela de clientes
{ title: 'NOME', key: 'Nome' },
{ title: 'TELEFONE', key: 'Telefone' },
{ title: 'CPF', key: 'Identification' },
{ title: 'CIDADE', key: 'ClienteCidadeId' },
{ title: 'AÇÕES', key: 'actions' },
])
const headerPedidos = ref([
// Cabeçalho da tabela de pedidos
{ title: 'DATA', key: 'Data' },
{ title: 'VALOR', key: 'Valor' },
])
Quando o componente é montado (onMounted
), três funções assíncronas são chamadas: getPosts()
, getCidades()
e getPedidos()
. Cada uma delas busca os respectivos dados no store correspondente. getPosts()
recupera a lista de clientes, getCidades()
carrega todas as cidades disponíveis e getPedidos()
obtém os pedidos registrados, imprimindo os resultados no console para verificação.
onMounted(async () => {
await getPosts()
await getCidades()
await getPedidos()
})
async function getPosts() {
await useClienteStore.fetch('')
clientes.value = useClienteStore.items
}
async function getCidades() {
await useCidadeStore.fetch('')
cidades.value = useCidadeStore.items
}
async function getPedidos() {
await usePedidoStore.fetch('')
pedidos.value = usePedidoStore.items
}
Assim como no arquivo passado, iniciaremos com a função createCliente()
que é responsável por cadastrar um novo cliente. Antes de salvar os dados, ela copia o ID da cidade selecionada para o campo ClienteCidadeId
, garantindo a associação correta do cliente com sua cidade. Após a criação bem-sucedida, o modal de cadastro é fechado e a lista de clientes é atualizada com getPosts()
.
OBS: Essa função possui uma etapa a mais do que o CRUD da Cidade, pois, devido a um alinhamento com o backend, tivemos que duplicar o dado id da cidade. No arquivo de pedido um dos dados também será replicado.
async function createCliente() {
if (newCliente.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (newCliente.value.Identification === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Identidade é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await clienteExiste(newCliente.value.Identification)) {
Swal.fire({
title: 'Cliente já existe!',
icon: 'error',
text: 'Um cliente com essa identidade já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCliente.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome do cliente não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
// Copia o Id da cidade selecionada para ClienteCidadeId
newCliente.value.ClienteCidadeId = newCliente.value.CidadeId
await useClienteStore.save(newCliente.value)
Swal.fire({
title: 'Cliente cadastrado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro criando cliente: ', error)
Swal.fire({
title: 'Erro ao cadastrar cliente!',
icon: 'error',
text: 'Ocorreu um erro ao cadastrar o cliente.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
A função salvarCliente()
decide se deve criar um novo cliente ou editar um existente. Se isEditing
for true
, a função submitEditCliente()
será chamada para atualizar os dados do cliente no banco. Caso contrário, createCliente()
será chamada para registrar um novo cliente.
async function salvarCliente() {
if (isEditing.value) await submitEditCliente();
else await createCliente();
}
Para editar um cliente, a função editCliente(item)
recebe um objeto do tipo Cliente
, copia seus dados para newCliente.value
e abre o modal de edição. A variável isEditing
é definida como true
, indicando que a ação realizada será uma atualização, não um cadastro.
function editCliente(item: Cliente) {
newCliente.value = { ...item }
isEditing.value = true
showModal.value = true
}
A função submitEditCliente()
envia os dados editados para atualização no banco. Se a operação for bem-sucedida, o modal é fechado e a lista de clientes é recarregada.
async function submitEditCliente() {
if (newCliente.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (newCliente.value.Identification === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Identidade é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await clienteExiste(newCliente.value.Identification)) {
Swal.fire({
title: 'Cliente já existe!',
icon: 'error',
text: 'Um cliente com essa identidade já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCliente.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome do cliente não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useClienteStore.updateItem(newCliente.value.Id, newCliente.value)
Swal.fire({
title: 'Cliente alterado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro alterando cliente: ', error)
Swal.fire({
title: 'Erro ao alterar cliente!',
icon: 'error',
text: 'Ocorreu um erro ao alterar o cliente.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
Assim como no código anterior, é preciso das funções abaixo para criar a função deleteCliente
.
function confirmDelete(item: Cliente) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
A exclusão de um cliente é tratada por deleteCliente(item)
, que recebe um cliente como parâmetro e o remove do banco de dados. Após a exclusão, a lista de clientes é atualizada.
async function deleteCliente() {
if (itemToDelete.value !== null) {
try {
await useClienteStore.deleteItem(itemToDelete.value.Id)
Swal.fire({
title: 'Cliente deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro deletando cliente: ', error)
Swal.fire({
title: 'Não foi possível deletar!',
text: 'Ocorreu um erro ao deletar o cliente.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false
}
}
}
A função pedidoClientes(item)
permite visualizar os pedidos associados a um cliente específico. Primeiro, ela busca todos os pedidos do banco de dados e os filtra com base no ID do cliente passado como argumento. Os pedidos filtrados são armazenados em pedidosCliente.value
e exibidos em um modal para visualização.
OBS: Essa funcionalidade é exclusiva do CRUD de Cliente, pois é necessário que o cliente veja os seus pedidos sem precisar ter conhecimento do seu id.
async function pedidoClientes(item: Cliente) {
await usePedidoStore.fetch() // Buscar todos os pedidos do backend
pedidos.value = usePedidoStore.items.filter((pedido) => pedido.PedidoClienteId === item.Id) // Filtrar pedidos pelo ID do cliente
pedidosCliente.value = pedidos.value // Atribuir pedidos do cliente ao array de pedidos do cliente
showPedidosModal.value = true // Exibir modal de pedidos do cliente
}
- Gabarito código index-cliente.vue
import { onMounted, ref, watch } from 'vue' // Importando o onMounted e ref do Vue
import { useCidadeStore } from '@/controller/store/CidadeStore' // Importando o store de Cidade
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
import { usePedidoStore } from '@/controller/store/PedidoStore' // Importando o store de Pedido
import { Cidade } from '@/model/Cidade' // Importando a classe Cidade
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
import { Pedido } from '@/model/Pedido' // Importando a classe Pedido
import Swal from 'sweetalert2' // Importando o SweetAlert2
const clientes = ref() // Lista de clientes
const pedidos = ref() // Lista de pedidos dos clientes
const cidades = ref<Cidade[]>([]) // Lista dos nomes das cidades
const pedidosCliente = ref<Pedido[]>([])
const itemToDelete = ref<Cliente | null>(null)
const showModal = ref(false)
const showDelete = ref(false)
const showPedidosModal = ref(false)
const isEditing = ref(false)
const newCliente = ref<Cliente>({
Id: '',
Nome: '',
Telefone: '',
Identification: '',
ClienteCidadeId: '',
CidadeId: '',
})
const header = ref([
// Cabeçalho da tabela de clientes
{ title: 'NOME', key: 'Nome' },
{ title: 'TELEFONE', key: 'Telefone' },
{ title: 'CPF', key: 'Identification' },
{ title: 'CIDADE', key: 'ClienteCidadeId' },
{ title: 'AÇÕES', key: 'actions' },
])
const headerPedidos = ref([
// Cabeçalho da tabela de pedidos
{ title: 'DATA', key: 'Data' },
{ title: 'VALOR', key: 'Valor' },
])
onMounted(async () => {
await getPosts()
await getCidades()
await getPedidos()
})
async function getPosts() {
await useClienteStore.fetch('')
clientes.value = useClienteStore.items
}
async function getCidades() {
await useCidadeStore.fetch('')
cidades.value = useCidadeStore.items
}
async function getPedidos() {
await usePedidoStore.fetch('')
pedidos.value = usePedidoStore.items
}
async function clienteExiste(identidade: string): Promise<boolean> {
await useClienteStore.fetch('')
return useClienteStore.items.some((cliente: Cliente) => cliente.Identification === identidade)
}
async function createCliente() {
if (newCliente.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (newCliente.value.Identification === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Identidade é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await clienteExiste(newCliente.value.Identification)) {
Swal.fire({
title: 'Cliente já existe!',
icon: 'error',
text: 'Um cliente com essa identidade já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCliente.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome do cliente não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
// Copia o Id da cidade selecionada para ClienteCidadeId
newCliente.value.ClienteCidadeId = newCliente.value.CidadeId
await useClienteStore.save(newCliente.value)
Swal.fire({
title: 'Cliente cadastrado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro criando cliente: ', error)
Swal.fire({
title: 'Erro ao cadastrar cliente!',
icon: 'error',
text: 'Ocorreu um erro ao cadastrar o cliente.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
async function salvarCliente() {
if (isEditing.value) await submitEditCliente()
else await createCliente()
}
function editCliente(item: Cliente) {
newCliente.value = { ...item }
isEditing.value = true
showModal.value = true
}
async function submitEditCliente() {
if (newCliente.value.Nome === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Nome é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (newCliente.value.Identification === '') {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Identidade é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (await clienteExiste(newCliente.value.Identification)) {
Swal.fire({
title: 'Cliente já existe!',
icon: 'error',
text: 'Um cliente com essa identidade já existe.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (/\d/.test(newCliente.value.Nome)) {
Swal.fire({
title: 'Nome Inválido!',
icon: 'error',
text: 'O nome do cliente não deve conter números.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await useClienteStore.updateItem(newCliente.value.Id, newCliente.value)
Swal.fire({
title: 'Cliente alterado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro alterando cliente: ', error)
Swal.fire({
title: 'Erro ao alterar cliente!',
icon: 'error',
text: 'Ocorreu um erro ao alterar o cliente.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false
}
}
function confirmDelete(item: Cliente) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
async function deleteCliente() {
if (itemToDelete.value !== null) {
try {
await useClienteStore.deleteItem(itemToDelete.value.Id)
Swal.fire({
title: 'Cliente deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts()
})
} catch (error) {
console.error('Erro deletando cliente: ', error)
Swal.fire({
title: 'Não foi possível deletar!',
text: 'Ocorreu um erro ao deletar o cliente.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false
}
}
}
async function pedidoClientes(item: Cliente) {
await usePedidoStore.fetch() // Buscar todos os pedidos do backend
pedidos.value = usePedidoStore.items.filter((pedido) => pedido.PedidoClienteId === item.Id) // Filtrar pedidos pelo ID do cliente
pedidosCliente.value = pedidos.value // Atribuir pedidos do cliente ao array de pedidos do cliente
showPedidosModal.value = true // Exibir modal de pedidos do cliente
}
function getCidadeNome(clienteCidadeId: string): string {
const cidade = cidades.value.find((cidade) => cidade.Id === clienteCidadeId)
return cidade ? cidade.Nome : 'Indeterminada'
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
const year = date.getFullYear()
return `${day}/${month}/${year}`
}
function formatCPF(cpf: string): string {
return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4')
}
function formatTelefone(telefone: string): string {
return telefone.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3')
}
watch(showDelete, (val) => {
if (!val) closeDelete()
})
<v-row>
<v-col cols="2" class="d-flex justify-start">
<!-- Botão cadastrar cliente -->
<v-btn
@click="
() => {
showModal = true
isEditing = false
newCliente = {
Id: '',
Nome: '',
Telefone: '',
Identification: '',
ClienteCidadeId: '',
CidadeId: '',
}
}
"
class="custom-width-2"
color="primary"
variant="flat"
>
Cadastrar Cliente
</v-btn>
</v-col>
</v-row>
<!-- Tabela de clientes -->
<v-data-table :headers="header" :items="clientes">
<template #item.ClienteCidadeId="{ item }">
{{ getCidadeNome(item.ClienteCidadeId) }}
</template>
<template #item.Telefone="{ item }">
{{ formatTelefone(item.Telefone) }}
</template>
<template #item.Identification="{ item }">
{{ formatCPF(item.Identification) }}
</template>
<template #item.actions="{ item }">
<v-btn @click="editCliente(item as Cliente)" color="primary" icon>
<mdicon name="pencil"></mdicon>
</v-btn>
<v-btn @click="confirmDelete(item as Cliente)" color="red" icon>
<mdicon name="delete"></mdicon>
</v-btn>
<v-btn @click="pedidoClientes(item as Cliente)" color="orange" icon>
<mdicon name="arrow-top-right" style="color: white"></mdicon>
</v-btn>
</template>
<template v-slot:no-data>
<v-label>Sem dados!</v-label>
</template>
</v-data-table>
<!-- Modal de confirmação de deleção -->
<v-dialog v-model="showDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5 text-center py-6"
>Tem certeza que deseja deletar esse item?</v-card-title
>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
dark
@click="closeDelete"
data-test="buttonCancelarDelecao"
>Cancelar</v-btn
>
<v-btn
color="primary"
variant="flat"
dark
@click="deleteCliente"
data-test="buttonConfirmarDelecao"
>OK</v-btn
>
<v-spacer />
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal de Cadastro/Edição -->
<v-dialog v-model="showModal" max-width="500" :style="{ 'z-index': 1050 }">
<v-card>
<v-card-title>
{{ isEditing ? 'Editar Resolução' : 'Cadastrar Resolução' }}
</v-card-title>
<v-card-text>
<v-form>
<v-text-field label="Nome" v-model="newCliente.Nome" required></v-text-field>
<v-text-field label="Telefone" v-model="newCliente.Telefone" required></v-text-field>
<v-text-field
label="Identidade"
v-model="newCliente.Identification"
required
></v-text-field>
<v-select
label="Cidade"
:items="cidades"
item-value="Id"
item-title="Nome"
v-model="newCliente.CidadeId"
>
</v-select>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="showModal = false">Cancelar</v-btn>
<v-btn @click="salvarCliente" color="primary">Salvar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal de Pedidos do Cliente -->
<v-dialog v-model="showPedidosModal" max-width="600">
<v-card>
<v-card-title> Pedidos do Cliente </v-card-title>
<v-card-text>
<v-data-table :headers="headerPedidos" :items="pedidosCliente">
<template #item.Data="{ item }">
{{ formatDate(item.Data) }}
</template>
<template #no-data>
<v-label>Sem pedidos!</v-label>
</template>
</v-data-table>
</v-card-text>
<v-card-actions>
<v-btn @click="showPedidosModal = false">Fechar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
--> Pedido
Vamos finalizar o CRUD com o arquivo index-pedido.vue seguindo o mesmo formato de código anteriores.
import { onMounted, ref, watch } from 'vue' // Importando o onMounted e ref do Vue
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
import { usePedidoStore } from '@/controller/store/PedidoStore' // Importando o store de Pedido
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
import { Pedido } from '@/model/Pedido' // Importando a classe Pedido
import Swal from 'sweetalert2' // Importando o SweetAlert2
const pedidos = ref<Pedido[]>([])
const clientes = ref<Cliente[]>([])
const isEditing = ref(false)
const itemToDelete = ref<Pedido | null>(null)
const showModal = ref(false)
const showDelete = ref(false)
const header = ref([
{ title: 'DATA', key: 'Data' },
{ title: 'VALOR', key: 'Valor' },
{ title: 'CLIENTE', key: 'PedidoClienteId' },
{ title: 'AÇÕES', key: 'actions' },
])
const newPedido = ref<Pedido>({
Id: '',
Data: '',
Valor: null,
PedidoClienteId: '',
ClienteId: '',
})
Agora devemos escrever as funções onMounted
, getPosts
e getCliente
.
onMounted(async () => {
await getPosts()
await getCliente()
})
async function getPosts() {
await usePedidoStore.fetch('')
pedidos.value = usePedidoStore.items
}
async function getCliente() {
await useClienteStore.fetch('')
clientes.value = useClienteStore.items
}
Com as definições feitas vamos seguir para os métodos que chamam as funcionalidades do CRUD.
A função createPedido()
é responsável por criar um novo pedido. Antes de salvar os dados, ela copia o valor de ClienteId
para PedidoClienteId
, garantindo que o pedido esteja corretamente associado ao cliente correspondente. Em seguida, a função imprime os dados no console para conferência, envia a requisição para o store usePedidoStore.save(newPedido.value)
, fecha o modal de cadastro (showModal.value = false
) e atualiza a lista de pedidos chamando getPosts()
.
async function createPedido() {
if (newPedido.value.Valor === null) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Valor é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (!newPedido.value.Data) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Data é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
newPedido.value.PedidoClienteId = newPedido.value.ClienteId // Copia o Id da cidade selecionada para ClienteCidadeId
await usePedidoStore.save(newPedido.value) // Salva o novo pedido
Swal.fire({
title: 'Pedido criado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro criando pedido: ', error)
Swal.fire({
title: 'Erro ao criar pedido!',
icon: 'error',
text: 'Ocorreu um erro ao cadastrar o pedido.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false // Fecha o modal
}
}
A função salvarPedido()
verifica se o pedido está sendo editado ou criado. Se isEditing.value
for verdadeiro, significa que um pedido existente está sendo alterado, e a função submitEditPedido()
é chamada. Caso contrário, createPedido()
é executada para registrar um novo pedido.
async function salvarPedido() {
if (isEditing.value) await submitEditPedido();
else await createPedido();
}
A função editPedido(item: Pedido)
permite a edição de um pedido já existente. Ela recebe um objeto Pedido
, copia seus valores para newPedido.value
, marca isEditing.value
como verdadeiro e exibe o modal de edição.
function editPedido(item: Pedido) {
newPedido.value = { ...item } // Copia o item para o novo pedido
isEditing.value = true // Define que está editando
showModal.value = true // Abre o modal
}
A função submitEditPedido()
atualiza um pedido já existente. Ela utiliza usePedidoStore.updateItem()
para modificar os dados do pedido com base no seu Id
. Após a atualização, o modal é fechado e a lista de pedidos é recarregada para refletir as mudanças.
async function submitEditPedido() {
if (newPedido.value.Valor === null) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Valor é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (!newPedido.value.Data) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Data é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await usePedidoStore.updateItem(newPedido.value.Id, newPedido.value) // Atualiza o pedido
Swal.fire({
title: 'Pedido atualizado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro alterando pedido: ', error)
Swal.fire({
title: 'Erro ao alterar o pedido!',
text: 'Ocorreu um erro ao alterar o pedido.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false // Fecha o modal
}
}
Por fim, deletePedido
permite a exclusão de um pedido. A função chama usePedidoStore.deleteItem(item.Id)
, removendo o pedido correspondente e atualizando a lista de pedidos com getPosts()
.
function confirmDelete(item: Pedido) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
async function deletePedido() {
if (itemToDelete.value !== null) {
try {
await usePedidoStore.deleteItem(itemToDelete.value.Id) // Deleta o pedido
Swal.fire({
title: 'Deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro deletando pedido: ', error)
Swal.fire({
title: 'Não foi possível apagar!',
text: 'Ocorreu um erro ao apagar o pedido.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false // Fecha o modal
}
}
}
E para finalizar o typescript devemos escrever essas funções adicionais:
function getClienteNome(pedidoClienteId: string): string {
const cliente = clientes.value.find((cliente) => cliente.Id === pedidoClienteId)
return cliente ? cliente.Nome : 'Desconhecido'
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
const year = date.getFullYear()
return `${day}/${month}/${year}`
}
function formatValor(valor: number): string {
return valor.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
}
watch(showDelete, (val) => {
if (!val) closeDelete()
})
Agora vamos finalizar com o template da página de pedidos, finalizando assim todo o CRUD.
No início da parte de template, vamos repetir o processo de criar um botão cadastrar, que, ao ser clicado, exibe um modal para inserir um novo pedido. Esse botão também redefine a variável newPedido
, garantindo que os campos do formulário estejam vazios e prontos para um novo cadastro. A variável isEditing
é definida como false
para indicar que um novo pedido está sendo criado.
<v-row>
<v-col cols="2" class="d-flex justify-start">
<!-- Botão cadastrar pedido -->
<v-btn
@click="
() => {
showModal = true
isEditing = false
newPedido = { Id: '', Data: '', Valor: null, PedidoClienteId: '', ClienteId: '' }
}
"
class="custom-width-2"
color="primary"
variant="flat"
>
Cadastrar Pedido
</v-btn>
</v-col>
</v-row>
Seguindo tabela v-data-table
exibe a lista de pedidos cadastrados, utilizando a variável Pedidos
. Para cada pedido, há duas ações disponíveis: um botão de edição (mdi-pencil
), que chama a função editPedido(item)
para carregar os dados do pedido selecionado no formulário, e um botão de exclusão (mdi-delete
), que chama deletePedido(item)
, removendo o pedido do sistema.
Caso a lista de pedidos esteja vazia, a tabela exibe a mensagem "Sem dados!", utilizando a estrutura v-slot:no-data
.
<!-- Tabela de pedidos -->
<v-data-table :headers="header" :items="pedidos">
<template #item.Data="{ item }">
{{ formatDate(item.Data) }}
</template>
<template #item.Valor="{ item }">
{{ formatValor(item.Valor) }}
</template>
<template #item.PedidoClienteId="{ item }">
{{ getClienteNome(item.PedidoClienteId) }}
</template>
<template #item.actions="{ item }">
<v-btn @click="editPedido(item as Pedido)" color="primary" icon>
<mdicon name="pencil"></mdicon>
</v-btn>
<v-btn @click="confirmDelete(item as Pedido)" color="red" icon>
<mdicon name="delete"></mdicon>
</v-btn>
</template>
<template #no-data>
<v-label>Sem dados!</v-label>
</template>
</v-data-table>
O modal de deleção é o mesmo utilizado nos outros arquivos.
<!-- Modal de confirmação de deleção -->
<v-dialog v-model="showDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5 text-center py-6"
>Tem certeza que deseja deletar esse item?</v-card-title
>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
dark
@click="closeDelete"
data-test="buttonCancelarDelecao"
>Cancelar</v-btn
>
<v-btn
color="primary"
variant="flat"
dark
@click="deletePedido"
data-test="buttonConfirmarDelecao"
>OK</v-btn
>
<v-spacer />
</v-card-actions>
</v-card>
</v-dialog>
O modal de cadastro/edição (v-dialog
) exibe um formulário com três campos principais:
-
Data: um campo do tipo
Date
, onde o usuário pode inserir a data do pedido. -
Valor: um campo de entrada numérica para o valor do pedido.
-
Cliente: um
v-select
que lista os clientes disponíveis, permitindo que o usuário escolha um cliente associado ao pedido. Esse campo utilizaclientes
como fonte de dados, mapeando oId
do cliente comoitem-value
e o nome comoitem-title
. Na parte inferior do modal, há dois botões de ação. O botão "Cancelar" simplesmente fecha o modal, enquanto o botão "Salvar" chama a funçãosalvarPedido()
. Dependendo do contexto, essa função pode criar um novo pedido ou atualizar um já existente.<!-- Modal de Cadastro/Edição -->
<v-dialog v-model="showModal" max-width="500" :style="{ 'z-index': 1050 }">
<v-card>
<v-card-title>
{{ isEditing ? 'Editar Resolução' : 'Cadastrar Resolução' }}
</v-card-title>
<v-card-text>
<v-form>
<v-text-field label="Data" type="Date" v-model="newPedido.Data" required></v-text-field>
<v-text-field label="Valor" v-model="newPedido.Valor" required></v-text-field>
<v-select
label="Cliente"
:items="clientes"
item-value="Id"
item-title="Nome"
v-model="newPedido.ClienteId"
>
</v-select>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="showModal = false">Cancelar</v-btn>
<v-btn @click="salvarPedido" color="primary">Salvar</v-btn>
</v-card-actions>
</v-card>
</v-dialog> -
Gabarito código index-pedido.vue
import { onMounted, ref, watch } from 'vue' // Importando o onMounted e ref do Vue
import { useClienteStore } from '@/controller/store/ClienteStore' // Importando o store de Cliente
import { usePedidoStore } from '@/controller/store/PedidoStore' // Importando o store de Pedido
import { Cliente } from '@/model/Cliente' // Importando a classe Cliente
import { Pedido } from '@/model/Pedido' // Importando a classe Pedido
import Swal from 'sweetalert2' // Importando o SweetAlert2
const pedidos = ref<Pedido[]>([])
const clientes = ref<Cliente[]>([])
const isEditing = ref(false)
const itemToDelete = ref<Pedido | null>(null)
const showModal = ref(false)
const showDelete = ref(false)
const header = ref([
{ title: 'DATA', key: 'Data' },
{ title: 'VALOR', key: 'Valor' },
{ title: 'CLIENTE', key: 'PedidoClienteId' },
{ title: 'AÇÕES', key: 'actions' },
])
const newPedido = ref<Pedido>({
Id: '',
Data: '',
Valor: null,
PedidoClienteId: '',
ClienteId: '',
})
onMounted(async () => {
await getPosts()
await getCliente()
})
async function getPosts() {
await usePedidoStore.fetch('')
pedidos.value = usePedidoStore.items
}
async function getCliente() {
await useClienteStore.fetch('')
clientes.value = useClienteStore.items
}
async function createPedido() {
if (newPedido.value.Valor === null) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Valor é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (!newPedido.value.Data) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Data é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
newPedido.value.PedidoClienteId = newPedido.value.ClienteId // Copia o Id da cidade selecionada para ClienteCidadeId
await usePedidoStore.save(newPedido.value) // Salva o novo pedido
Swal.fire({
title: 'Pedido criado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro criando pedido: ', error)
Swal.fire({
title: 'Erro ao criar pedido!',
icon: 'error',
text: 'Ocorreu um erro ao cadastrar o pedido.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false // Fecha o modal
}
}
async function salvarPedido() {
if (isEditing.value) await submitEditPedido()
else await createPedido()
}
function editPedido(item: Pedido) {
newPedido.value = { ...item } // Copia o item para o novo pedido
isEditing.value = true // Define que está editando
showModal.value = true // Abre o modal
}
async function submitEditPedido() {
if (newPedido.value.Valor === null) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Valor é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
if (!newPedido.value.Data) {
Swal.fire({
title: 'Campo Obrigatório!',
icon: 'error',
text: 'O campo Data é obrigatório.',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
}).then(() => {})
return
}
try {
await usePedidoStore.updateItem(newPedido.value.Id, newPedido.value) // Atualiza o pedido
Swal.fire({
title: 'Pedido atualizado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro alterando pedido: ', error)
Swal.fire({
title: 'Erro ao alterar o pedido!',
text: 'Ocorreu um erro ao alterar o pedido.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showModal.value = false // Fecha o modal
}
}
function confirmDelete(item: Pedido) {
itemToDelete.value = item
showDelete.value = true
}
function closeDelete() {
showDelete.value = false
itemToDelete.value = null
}
async function deletePedido() {
if (itemToDelete.value !== null) {
try {
await usePedidoStore.deleteItem(itemToDelete.value.Id) // Deleta o pedido
Swal.fire({
title: 'Deletado com sucesso!',
icon: 'success',
timer: 2000,
showConfirmButton: false,
}).then(() => {
getPosts() // Atualiza a lista de pedidos
})
} catch (error) {
console.error('Erro deletando pedido: ', error)
Swal.fire({
title: 'Não foi possível apagar!',
text: 'Ocorreu um erro ao apagar o pedido.',
icon: 'error',
confirmButtonText: 'OK',
confirmButtonColor: '#D3D3D3',
})
} finally {
showDelete.value = false // Fecha o modal
}
}
}
function getClienteNome(pedidoClienteId: string): string {
const cliente = clientes.value.find((cliente) => cliente.Id === pedidoClienteId)
return cliente ? cliente.Nome : 'Desconhecido'
}
function formatDate(dateString: string): string {
const date = new Date(dateString)
const day = String(date.getDate()).padStart(2, '0')
const month = String(date.getMonth() + 1).padStart(2, '0')
const year = date.getFullYear()
return `${day}/${month}/${year}`
}
function formatValor(valor: number): string {
return valor.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
}
watch(showDelete, (val) => {
if (!val) closeDelete()
})
<v-row>
<v-col cols="2" class="d-flex justify-start">
<!-- Botão cadastrar pedido -->
<v-btn
@click="
() => {
showModal = true
isEditing = false
newPedido = { Id: '', Data: '', Valor: null, PedidoClienteId: '', ClienteId: '' }
}
"
class="custom-width-2"
color="primary"
variant="flat"
>
Cadastrar Pedido
</v-btn>
</v-col>
</v-row>
<!-- Tabela de pedidos -->
<v-data-table :headers="header" :items="pedidos">
<template #item.Data="{ item }">
{{ formatDate(item.Data) }}
</template>
<template #item.Valor="{ item }">
{{ formatValor(item.Valor) }}
</template>
<template #item.PedidoClienteId="{ item }">
{{ getClienteNome(item.PedidoClienteId) }}
</template>
<template #item.actions="{ item }">
<v-btn @click="editPedido(item as Pedido)" color="primary" icon>
<mdicon name="pencil"></mdicon>
</v-btn>
<v-btn @click="confirmDelete(item as Pedido)" color="red" icon>
<mdicon name="delete"></mdicon>
</v-btn>
</template>
<template #no-data>
<v-label>Sem dados!</v-label>
</template>
</v-data-table>
<!-- Modal de confirmação de deleção -->
<v-dialog v-model="showDelete" max-width="500px">
<v-card>
<v-card-title class="text-h5 text-center py-6"
>Tem certeza que deseja deletar esse item?</v-card-title
>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
dark
@click="closeDelete"
data-test="buttonCancelarDelecao"
>Cancelar</v-btn
>
<v-btn
color="primary"
variant="flat"
dark
@click="deletePedido"
data-test="buttonConfirmarDelecao"
>OK</v-btn
>
<v-spacer />
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal de Cadastro/Edição -->
<v-dialog v-model="showModal" max-width="500" :style="{ 'z-index': 1050 }">
<v-card>
<v-card-title>
{{ isEditing ? 'Editar Resolução' : 'Cadastrar Resolução' }}
</v-card-title>
<v-card-text>
<v-form>
<v-text-field label="Data" type="Date" v-model="newPedido.Data" required></v-text-field>
<v-text-field label="Valor" v-model="newPedido.Valor" required></v-text-field>
<v-select
label="Cliente"
:items="clientes"
item-value="Id"
item-title="Nome"
v-model="newPedido.ClienteId"
>
</v-select>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn @click="showModal = false">Cancelar</v-btn>
<v-btn @click="salvarPedido" color="primary">Salvar</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
Em resumo, esta documentação apresentou de forma prática e objetiva os passos necessários para construir um CRUD básico. Ao seguir cada etapa, você aprendeu a estruturar as operações essenciais de criação, leitura, atualização e exclusão de dados, fundamentais para o desenvolvimento de aplicações robustas. Lembre-se de que, embora este guia sirva como ponto de partida, a prática e a adaptação do conteúdo às necessidades específicas do seu projeto são fundamentais para o aprimoramento contínuo das suas habilidades. Continue explorando e experimentando novas funcionalidades para evoluir no desenvolvimento de soluções cada vez mais eficientes.
LinkedIn's dos criadores
LinkedIn Heloísa Borchardt Gomes