fix(erp): enable erp pages and menu items
This commit is contained in:
74
.agent/agent-gemini.md
Normal file
74
.agent/agent-gemini.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Agent Gemini - Log de Evolução do Projeto Aggios
|
||||
|
||||
Este arquivo documenta as contribuições do Agente Code AI (Gemini) e a compreensão técnica consolidada sobre o ecossistema Aggios.
|
||||
|
||||
## 🚀 Visão Geral do Projeto
|
||||
O **Aggios** é uma plataforma SaaS multi-tenant focada em agências, oferecendo uma suíte "all-in-one" que inclui CRM, ERP, Gestão de Projetos, entre outros.
|
||||
|
||||
### Stack Tecnológica
|
||||
- **Frontend:** Next.js (App Router), TypeScript, Tailwind CSS, Headless UI.
|
||||
- **Backend:** Go (Golang) com roteamento `gorilla/mux`.
|
||||
- **Banco de Dados:** PostgreSQL (migrações SQL puras).
|
||||
- **Infraestrutura:** Docker Compose (backend, agency-frontend, minio, postgres, redis).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Contribuições do Agente (Dezembro/2025)
|
||||
|
||||
### 1. Módulo ERP - Finanças & Caixa
|
||||
- **Gestão de Múltiplas Contas:** Implementação completa (CRUD) de contas bancárias no backend e frontend.
|
||||
- **Controle de Saldo em Tempo Real:** Desenvolvimento da lógica de repositório em Go para atualizar o `current_balance` de contas baseando-se no status das transações financeiras (`paid`, `pending`).
|
||||
- **Resumo Financeiro:** Refatoração dos cartões de estatísticas para exibir o "Saldo de Caixa" real (somatório de contas) em vez de apenas totais de lançamentos filtrados.
|
||||
- **Dashboard ERP Real:** Dados reais, gráficos automáticos e filtros de status/data avançados.
|
||||
- **Módulo de Documentos:** Implementado sistema de documentos (estilo Google Docs) com editor de texto e gestão por tenant.
|
||||
|
||||
### 2. UI/UX & Design System (Padrão Aggios)
|
||||
- **Refinação Minimalista Flat:** Aplicação do padrão visual "Clean & Flat" na página de finanças, removendo sombras pesadas e mantendo foco no contraste e tipografia.
|
||||
- **Componentização:** Utilização e refinamento de componentes em `components/ui` (Input, CustomSelect, DataTable).
|
||||
- **Barra de Busca:** Implementação de busca reativa integrada ao `Input` padronizado.
|
||||
|
||||
### 3. Otimização e Reatividade
|
||||
- **Correção de Cache da API:** Configuração de `cache: 'no-store'` nas chamadas de serviço para garantir integridade dos dados sem necessidade de recarregar a página (F5).
|
||||
- **Sync de Estado:** Ajuste nos handlers do React para usar `await fetchData()` em todas as operações de escrita, garantindo que a UI reflita as mudanças do backend instantaneamente.
|
||||
|
||||
### 4. Novas Funcionalidades (27 de Dezembro de 2025)
|
||||
- **Ações em Lote (Bulk Actions):** Implementação de seleção múltipla em transações financeiras e produtos. Adição de barra de ações flutuante para exclusão em massa e atualização de status coletiva.
|
||||
- **Melhorias no Dashboard & Filtros:** Refinamento dos filtros de data, busca reativa e integração de ações em lote nos módulos de "Contas a Pagar" e "Contas a Receber".
|
||||
- **Gestão de Contas Bancárias:** Refatoração da interface de contas (cards) com feedback visual de saldo e integração direta com o fluxo de caixa.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Entendimento Técnico do Sistema
|
||||
|
||||
### Arquitetura de Soluções
|
||||
O sistema utiliza um sistema de **Solutions** vinculadas a **Planos**.
|
||||
- Slugs identificados: `crm`, `erp`, `projetos`, `helpdesk`, `pagamentos`, `contratos`, `documentos`, `social`.
|
||||
- O acesso é controlado via `SolutionGuard` no frontend e middleware de tenant no backend.
|
||||
|
||||
### Estrutura de Autenticação
|
||||
- **Níveis de Acesso:** `SUPERADMIN` (Aggios), `ADMIN_AGENCIA` (Dono da agência/Tenant), `CLIENTE` (Portal do Cliente).
|
||||
- **Segurança:** JWT armazenado no `localStorage` com envio no header `Authorization`.
|
||||
|
||||
### Padrão de Design "Aggios Pattern"
|
||||
- **Cards:** Bordas sutis (`zinc-100/800`), sem sombras, `rounded-2xl` ou `[32px]`.
|
||||
- **Botões:** Uso de gradientes (`var(--gradient)`) para ações primárias e visual flat para secundárias.
|
||||
- **Feedback:** Uso intensivo de `react-hot-toast` para notificações de sucesso/erro.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Diretrizes de Desenvolvimento
|
||||
|
||||
### 📋 Uso de Templates e Padronização
|
||||
Para manter a consistência visual e técnica do ecossistema Aggios, o Agente deve seguir rigorosamente estas regras:
|
||||
|
||||
1. **Aggios App Pattern:** Sempre basear novas telas e funcionalidades no workflow `aggios-app-pattern.md`. Isso garante que a hierarquia visual (PageHeader -> StatsCards -> Tabs -> DataTable) seja preservada.
|
||||
2. **Componentes UI Reutilizáveis:** Nunca criar elementos de interface ad-hoc se existir um componente correspondente em `components/ui`. Priorizar o uso de:
|
||||
- `DataTable` para listagens.
|
||||
- `Input` e `CustomSelect` para formulários e buscas.
|
||||
- `StatsCard` para indicadores numéricos e financeiros.
|
||||
3. **Visual Minimalista Flat:** Evitar o uso de sombras (`shadow`), utilizando bordas sutis (`border-zinc-100` / `dark:border-zinc-800`) e fundos contrastantes para separar camadas.
|
||||
4. **Reatividade Garantida:** Manter o padrão de execução assíncrona com `await fetchData()` e desativação de cache da API para que os templates reflitam mudanças instantaneamente sem recarregar a página.
|
||||
5. **Rebuild de Containers:** Sempre que houver mudanças estruturais no frontend (especialmente no `front-end-agency`), é necessário rodar `docker-compose up -d --build agency` para refletir as alterações no ambiente de produção/Docker.
|
||||
|
||||
---
|
||||
*Documentado por Gemini (Agent Gemini) em 27 de Dezembro de 2025.*
|
||||
117
.agent/workflows/aggios-app-pattern.md
Normal file
117
.agent/workflows/aggios-app-pattern.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
description: Padrão de Design Aggios App para Páginas de Listagem e Dashboards
|
||||
---
|
||||
|
||||
# Padrão de Design Aggios App
|
||||
|
||||
Este workflow descreve como construir uma página seguindo o design system da Aggios, utilizando os componentes padronizados na pasta `components/ui`.
|
||||
|
||||
## 1. Estrutura Básica da Página
|
||||
|
||||
Toda página deve ser envolvida por um container com padding e largura máxima:
|
||||
|
||||
```tsx
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
{/* Conteúdo aqui */}
|
||||
</div>
|
||||
```
|
||||
|
||||
## 2. Cabeçalho (`PageHeader`)
|
||||
|
||||
Utilize o `PageHeader` para títulos, descrições e ações globais da página.
|
||||
|
||||
```tsx
|
||||
<PageHeader
|
||||
title="Título da Página"
|
||||
description="Breve descrição da funcionalidade."
|
||||
primaryAction={{
|
||||
label: "Novo Item",
|
||||
icon: <PlusIcon className="w-4 h-4" />,
|
||||
onClick: () => handleCreate()
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## 3. Cartões de Estatísticas (`StatsCard`)
|
||||
|
||||
Para dashboards ou resumos, utilize o grid de stats:
|
||||
|
||||
```tsx
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Métrica"
|
||||
value="R$ 1.000"
|
||||
icon={<CurrencyDollarIcon className="w-6 h-6" />}
|
||||
trend={{ value: '10%', label: 'vs ontem', type: 'up' }}
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 4. Filtros e Pesquisa (Minimalista Flat)
|
||||
|
||||
Os filtros não devem ter sombras nem cores de marca no estado inicial/focus. Devem usar um visual "Clean" com contraste sólido.
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="flex-1 w-full">
|
||||
<Input
|
||||
placeholder="Pesquisar..."
|
||||
leftIcon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-80">
|
||||
<DatePicker
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
buttonClassName="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-56">
|
||||
<CustomSelect
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
options={[
|
||||
{ label: 'Todos', value: 'all' },
|
||||
{ label: 'Ativo', value: 'active', color: 'bg-emerald-500' },
|
||||
]}
|
||||
buttonClassName="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 hover:border-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 5. Abas e Tabelas (`Tabs` & `DataTable`)
|
||||
|
||||
Para organizar o conteúdo principal, utilize o componente `Tabs`. Dentro de cada aba, utilize `Card` com `noPadding` para envolver a `DataTable`.
|
||||
|
||||
```tsx
|
||||
<Tabs
|
||||
variant="pills" // ou 'underline'
|
||||
items={[
|
||||
{
|
||||
label: 'Listagem',
|
||||
icon: <TableIcon />,
|
||||
content: (
|
||||
<Card noPadding title="Itens" description="Gerenciamento de registros.">
|
||||
<DataTable
|
||||
columns={COLUMNS}
|
||||
data={DATA}
|
||||
pagination={{ ... }}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Regras de Estilo e Cores
|
||||
- **Botões Primários**: Sempre use `variant="primary"` e aplique o gradiente via style/classe: `style={{ background: 'var(--gradient)' }} className="shadow-lg shadow-brand-500/20"`.
|
||||
- **Bordas**: Use `border-zinc-200` para light mode e `dark:border-zinc-800` para dark mode.
|
||||
- **Backgrounds**: Use `bg-white` (light) e `dark:bg-zinc-900` (dark) para componentes elevados.
|
||||
- **Cards & Containers (Flat Design)**:
|
||||
- **Cards:** Fundo branco/zinc-900, bordas sutis (`border-zinc-200` / `dark:border-zinc-800`), **SEM SOMBRAS**.
|
||||
- **Border Radius:** Usar `rounded-2xl` (16px) ou `rounded-[32px]` para containers grandes.
|
||||
- **StatsCards:** Texto de valor em `font-bold` ou `font-black`, ícones em boxes coloridos com opacidade 10% no dark mode.
|
||||
- **Hover:** Apenas transições de cor ou escalas sutis, evitar sombras no hover.
|
||||
@@ -60,6 +60,8 @@ func main() {
|
||||
subscriptionRepo := repository.NewSubscriptionRepository(db)
|
||||
crmRepo := repository.NewCRMRepository(db)
|
||||
solutionRepo := repository.NewSolutionRepository(db)
|
||||
erpRepo := repository.NewERPRepository(db)
|
||||
docRepo := repository.NewDocumentRepository(db)
|
||||
|
||||
// Initialize services
|
||||
authService := service.NewAuthService(userRepo, tenantRepo, crmRepo, cfg)
|
||||
@@ -83,6 +85,8 @@ func main() {
|
||||
agencyTemplateHandler := handlers.NewAgencyTemplateHandler(agencyTemplateRepo, agencyService, userRepo, tenantRepo)
|
||||
filesHandler := handlers.NewFilesHandler(cfg)
|
||||
customerPortalHandler := handlers.NewCustomerPortalHandler(crmRepo, authService, cfg)
|
||||
erpHandler := handlers.NewERPHandler(erpRepo)
|
||||
docHandler := handlers.NewDocumentHandler(docRepo)
|
||||
|
||||
// Initialize upload handler
|
||||
uploadHandler, err := handlers.NewUploadHandler(cfg)
|
||||
@@ -424,6 +428,133 @@ func main() {
|
||||
}
|
||||
}))).Methods("POST", "DELETE")
|
||||
|
||||
// ==================== ERP ROUTES (TENANT) ====================
|
||||
|
||||
// Finance
|
||||
router.Handle("/api/erp/finance/categories", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetFinancialCategories(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateFinancialCategory(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/finance/accounts", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetBankAccounts(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateBankAccount(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/finance/accounts/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
erpHandler.UpdateBankAccount(w, r)
|
||||
case http.MethodDelete:
|
||||
erpHandler.DeleteBankAccount(w, r)
|
||||
}
|
||||
}))).Methods("PUT", "DELETE")
|
||||
|
||||
router.Handle("/api/erp/finance/transactions", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetTransactions(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateTransaction(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/finance/transactions/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
erpHandler.UpdateTransaction(w, r)
|
||||
case http.MethodDelete:
|
||||
erpHandler.DeleteTransaction(w, r)
|
||||
}
|
||||
}))).Methods("PUT", "DELETE")
|
||||
|
||||
// Products
|
||||
router.Handle("/api/erp/products", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetProducts(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateProduct(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/products/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
erpHandler.UpdateProduct(w, r)
|
||||
case http.MethodDelete:
|
||||
erpHandler.DeleteProduct(w, r)
|
||||
}
|
||||
}))).Methods("PUT", "DELETE")
|
||||
|
||||
// Orders
|
||||
router.Handle("/api/erp/orders", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetOrders(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateOrder(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/orders/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodDelete:
|
||||
erpHandler.DeleteOrder(w, r)
|
||||
}
|
||||
}))).Methods("DELETE")
|
||||
|
||||
// Entities
|
||||
router.Handle("/api/erp/entities", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
erpHandler.GetEntities(w, r)
|
||||
case http.MethodPost:
|
||||
erpHandler.CreateEntity(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/erp/entities/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodPut, http.MethodPatch:
|
||||
erpHandler.UpdateEntity(w, r)
|
||||
case http.MethodDelete:
|
||||
erpHandler.DeleteEntity(w, r)
|
||||
}
|
||||
}))).Methods("PUT", "PATCH", "DELETE")
|
||||
|
||||
// Documents
|
||||
router.Handle("/api/documents", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
docHandler.List(w, r)
|
||||
case http.MethodPost:
|
||||
docHandler.Create(w, r)
|
||||
}
|
||||
}))).Methods("GET", "POST")
|
||||
|
||||
router.Handle("/api/documents/{id}", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
docHandler.Get(w, r)
|
||||
case http.MethodPut:
|
||||
docHandler.Update(w, r)
|
||||
case http.MethodDelete:
|
||||
docHandler.Delete(w, r)
|
||||
}
|
||||
}))).Methods("GET", "PUT", "DELETE")
|
||||
|
||||
router.Handle("/api/documents/{id}/subpages", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(docHandler.GetSubpages))).Methods("GET")
|
||||
router.Handle("/api/documents/{id}/activities", middleware.RequireAgencyUser(cfg)(http.HandlerFunc(docHandler.GetActivities))).Methods("GET")
|
||||
|
||||
// Apply global middlewares: tenant -> cors -> security -> rateLimit -> router
|
||||
handler := tenantDetector(corsMiddleware(securityMiddleware(rateLimitMiddleware(router))))
|
||||
|
||||
|
||||
@@ -5,8 +5,32 @@ go 1.23
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/minio/minio-go/v7 v7.0.63
|
||||
github.com/shopspring/decimal v1.3.1
|
||||
github.com/xuri/excelize/v2 v2.8.1
|
||||
golang.org/x/crypto v0.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.3 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
|
||||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,8 +1,76 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
|
||||
github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
|
||||
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0=
|
||||
github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ=
|
||||
github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE=
|
||||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4=
|
||||
github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
144
backend/internal/api/handlers/document_handler.go
Normal file
144
backend/internal/api/handlers/document_handler.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"aggios-app/backend/internal/api/middleware"
|
||||
"aggios-app/backend/internal/domain"
|
||||
"aggios-app/backend/internal/repository"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type DocumentHandler struct {
|
||||
repo *repository.DocumentRepository
|
||||
}
|
||||
|
||||
func NewDocumentHandler(repo *repository.DocumentRepository) *DocumentHandler {
|
||||
return &DocumentHandler{repo: repo}
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||||
|
||||
var doc domain.Document
|
||||
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
doc.ID = uuid.New()
|
||||
doc.TenantID, _ = uuid.Parse(tenantID)
|
||||
doc.CreatedBy, _ = uuid.Parse(userID)
|
||||
doc.LastUpdatedBy, _ = uuid.Parse(userID)
|
||||
if doc.Status == "" {
|
||||
doc.Status = "draft"
|
||||
}
|
||||
|
||||
if err := h.repo.Create(&doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(doc)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
|
||||
docs, err := h.repo.GetByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(docs)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
doc, err := h.repo.GetByID(id, tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if doc == nil {
|
||||
http.Error(w, "document not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(doc)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
var doc domain.Document
|
||||
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
doc.ID, _ = uuid.Parse(id)
|
||||
doc.TenantID, _ = uuid.Parse(tenantID)
|
||||
doc.LastUpdatedBy, _ = uuid.Parse(userID)
|
||||
|
||||
if err := h.repo.Update(&doc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(doc)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
if err := h.repo.Delete(id, tenantID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) GetSubpages(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
parentID := mux.Vars(r)["id"]
|
||||
|
||||
docs, err := h.repo.GetSubpages(parentID, tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(docs)
|
||||
}
|
||||
|
||||
func (h *DocumentHandler) GetActivities(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
activities, err := h.repo.GetActivities(id, tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(activities)
|
||||
}
|
||||
399
backend/internal/api/handlers/erp_handler.go
Normal file
399
backend/internal/api/handlers/erp_handler.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"aggios-app/backend/internal/api/middleware"
|
||||
"aggios-app/backend/internal/domain"
|
||||
"aggios-app/backend/internal/repository"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type ERPHandler struct {
|
||||
repo *repository.ERPRepository
|
||||
}
|
||||
|
||||
func NewERPHandler(repo *repository.ERPRepository) *ERPHandler {
|
||||
return &ERPHandler{repo: repo}
|
||||
}
|
||||
|
||||
// ==================== FINANCE ====================
|
||||
|
||||
func (h *ERPHandler) CreateFinancialCategory(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
var cat domain.FinancialCategory
|
||||
if err := json.NewDecoder(r.Body).Decode(&cat); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
cat.ID = uuid.New()
|
||||
cat.TenantID, _ = uuid.Parse(tenantID)
|
||||
cat.IsActive = true
|
||||
|
||||
if err := h.repo.CreateFinancialCategory(&cat); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(cat)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetFinancialCategories(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
cats, err := h.repo.GetFinancialCategoriesByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(cats)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) CreateBankAccount(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
var acc domain.BankAccount
|
||||
if err := json.NewDecoder(r.Body).Decode(&acc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
acc.ID = uuid.New()
|
||||
acc.TenantID, _ = uuid.Parse(tenantID)
|
||||
acc.IsActive = true
|
||||
|
||||
if err := h.repo.CreateBankAccount(&acc); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(acc)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetBankAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
accs, err := h.repo.GetBankAccountsByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(accs)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) CreateTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||||
var t domain.FinancialTransaction
|
||||
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t.ID = uuid.New()
|
||||
t.TenantID, _ = uuid.Parse(tenantID)
|
||||
t.CreatedBy, _ = uuid.Parse(userID)
|
||||
|
||||
if err := h.repo.CreateTransaction(&t); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(t)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetTransactions(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
txs, err := h.repo.GetTransactionsByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(txs)
|
||||
}
|
||||
|
||||
// ==================== PRODUCTS ====================
|
||||
|
||||
func (h *ERPHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
var p domain.Product
|
||||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.ID = uuid.New()
|
||||
p.TenantID, _ = uuid.Parse(tenantID)
|
||||
p.IsActive = true
|
||||
|
||||
if err := h.repo.CreateProduct(&p); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(p)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetProducts(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
products, err := h.repo.GetProductsByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(products)
|
||||
}
|
||||
|
||||
// ==================== ORDERS ====================
|
||||
|
||||
type createOrderRequest struct {
|
||||
Order domain.Order `json:"order"`
|
||||
Items []domain.OrderItem `json:"items"`
|
||||
}
|
||||
|
||||
func (h *ERPHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
userID, _ := r.Context().Value(middleware.UserIDKey).(string)
|
||||
var req createOrderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
req.Order.ID = uuid.New()
|
||||
req.Order.TenantID, _ = uuid.Parse(tenantID)
|
||||
req.Order.CreatedBy, _ = uuid.Parse(userID)
|
||||
if req.Order.Status == "" {
|
||||
req.Order.Status = "draft"
|
||||
}
|
||||
|
||||
for i := range req.Items {
|
||||
req.Items[i].ID = uuid.New()
|
||||
req.Items[i].OrderID = req.Order.ID
|
||||
req.Items[i].CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
if err := h.repo.CreateOrder(&req.Order, req.Items); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(req.Order)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetOrders(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
orders, err := h.repo.GetOrdersByTenant(tenantID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(orders)
|
||||
}
|
||||
|
||||
// ==================== ENTITIES ====================
|
||||
|
||||
func (h *ERPHandler) CreateEntity(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
var e domain.Entity
|
||||
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e.ID = uuid.New()
|
||||
e.TenantID, _ = uuid.Parse(tenantID)
|
||||
if e.Status == "" {
|
||||
e.Status = "active"
|
||||
}
|
||||
|
||||
if err := h.repo.CreateEntity(&e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(e)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) GetEntities(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
entityType := r.URL.Query().Get("type") // customer or supplier
|
||||
|
||||
entities, err := h.repo.GetEntitiesByTenant(tenantID, entityType)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(entities)
|
||||
}
|
||||
func (h *ERPHandler) UpdateTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var t domain.FinancialTransaction
|
||||
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t.ID = id
|
||||
t.TenantID, _ = uuid.Parse(tenantID)
|
||||
|
||||
if err := h.repo.UpdateTransaction(&t); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) DeleteTransaction(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
if err := h.repo.DeleteTransaction(idStr, tenantID); err != nil {
|
||||
log.Printf("❌ Error deleting transaction: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) UpdateEntity(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var e domain.Entity
|
||||
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e.ID = id
|
||||
e.TenantID, _ = uuid.Parse(tenantID)
|
||||
|
||||
if err := h.repo.UpdateEntity(&e); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) DeleteEntity(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
if err := h.repo.DeleteEntity(idStr, tenantID); err != nil {
|
||||
log.Printf("❌ Error deleting entity: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var p domain.Product
|
||||
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.ID = id
|
||||
p.TenantID, _ = uuid.Parse(tenantID)
|
||||
|
||||
if err := h.repo.UpdateProduct(&p); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
if err := h.repo.DeleteProduct(idStr, tenantID); err != nil {
|
||||
log.Printf("❌ Error deleting product: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) UpdateBankAccount(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var a domain.BankAccount
|
||||
if err := json.NewDecoder(r.Body).Decode(&a); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
a.ID = id
|
||||
a.TenantID, _ = uuid.Parse(tenantID)
|
||||
|
||||
if err := h.repo.UpdateBankAccount(&a); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) DeleteBankAccount(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
if err := h.repo.DeleteBankAccount(idStr, tenantID); err != nil {
|
||||
log.Printf("❌ Error deleting bank account: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ERPHandler) DeleteOrder(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, _ := r.Context().Value(middleware.TenantIDKey).(string)
|
||||
idStr := mux.Vars(r)["id"]
|
||||
if err := h.repo.DeleteOrder(idStr, tenantID); err != nil {
|
||||
log.Printf("❌ Error deleting order: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
-- Migration: 025_create_erp_tables.sql
|
||||
-- Description: Create tables for Finance, Inventory, and Order management
|
||||
|
||||
-- Financial Categories
|
||||
CREATE TABLE IF NOT EXISTS erp_financial_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('income', 'expense')),
|
||||
color VARCHAR(20),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Bank Accounts
|
||||
CREATE TABLE IF NOT EXISTS erp_bank_accounts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
bank_name VARCHAR(255),
|
||||
initial_balance DECIMAL(15,2) DEFAULT 0.00,
|
||||
current_balance DECIMAL(15,2) DEFAULT 0.00,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Financial Transactions
|
||||
CREATE TABLE IF NOT EXISTS erp_financial_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
account_id UUID REFERENCES erp_bank_accounts(id),
|
||||
category_id UUID REFERENCES erp_financial_categories(id),
|
||||
description TEXT,
|
||||
amount DECIMAL(15,2) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('income', 'expense')),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
|
||||
due_date DATE,
|
||||
payment_date TIMESTAMP WITH TIME ZONE,
|
||||
attachments TEXT[], -- URLs for proofs
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Products & Services
|
||||
CREATE TABLE IF NOT EXISTS erp_products (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
sku VARCHAR(100),
|
||||
description TEXT,
|
||||
price DECIMAL(15,2) NOT NULL,
|
||||
cost_price DECIMAL(15,2),
|
||||
type VARCHAR(20) DEFAULT 'product' CHECK (type IN ('product', 'service')),
|
||||
stock_quantity INT DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Orders
|
||||
CREATE TABLE IF NOT EXISTS erp_orders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
customer_id UUID REFERENCES companies(id), -- Linked to CRM (companies)
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'confirmed', 'completed', 'cancelled')),
|
||||
total_amount DECIMAL(15,2) DEFAULT 0.00,
|
||||
notes TEXT,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Order Items
|
||||
CREATE TABLE IF NOT EXISTS erp_order_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id UUID NOT NULL REFERENCES erp_orders(id) ON DELETE CASCADE,
|
||||
product_id UUID NOT NULL REFERENCES erp_products(id),
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
unit_price DECIMAL(15,2) NOT NULL,
|
||||
total_price DECIMAL(15,2) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Indexes for performance and multi-tenancy
|
||||
CREATE INDEX idx_erp_fin_cat_tenant ON erp_financial_categories(tenant_id);
|
||||
CREATE INDEX idx_erp_bank_acc_tenant ON erp_bank_accounts(tenant_id);
|
||||
CREATE INDEX idx_erp_fin_trans_tenant ON erp_financial_transactions(tenant_id);
|
||||
CREATE INDEX idx_erp_products_tenant ON erp_products(tenant_id);
|
||||
CREATE INDEX idx_erp_orders_tenant ON erp_orders(tenant_id);
|
||||
CREATE INDEX idx_erp_order_items_order ON erp_order_items(order_id);
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Migration: 026_create_erp_entities.sql
|
||||
-- Description: Create tables for Customers and Suppliers in ERP
|
||||
|
||||
-- ERP Entities (Customers and Suppliers)
|
||||
CREATE TABLE IF NOT EXISTS erp_entities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
document VARCHAR(20), -- CPF/CNPJ
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('customer', 'supplier', 'both')),
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(2),
|
||||
zip VARCHAR(10),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Update Financial Transactions to link with Entities
|
||||
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS entity_id UUID REFERENCES erp_entities(id);
|
||||
|
||||
-- Update Orders to link with Entities instead of companies (optional but more consistent for ERP)
|
||||
-- Keep customer_id for now to avoid breaking existing logic, but allow entity_id
|
||||
ALTER TABLE erp_orders ADD COLUMN IF NOT EXISTS entity_id UUID REFERENCES erp_entities(id);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_erp_entities_tenant ON erp_entities(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_erp_entities_type ON erp_entities(type);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Migration: 027_add_payment_method_to_transactions.sql
|
||||
-- Description: Add payment_method field to financial transactions
|
||||
|
||||
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS payment_method VARCHAR(50);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Migration: 028_add_crm_links_to_transactions.sql
|
||||
-- Description: Add fields to link financial transactions to CRM Customers and Companies
|
||||
|
||||
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS crm_customer_id UUID REFERENCES crm_customers(id) ON DELETE SET NULL;
|
||||
ALTER TABLE erp_financial_transactions ADD COLUMN IF NOT EXISTS company_id UUID REFERENCES companies(id) ON DELETE SET NULL;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Migration: 029_create_documents_table.sql
|
||||
-- Description: Create table for text documents (Google Docs style)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT,
|
||||
status VARCHAR(50) DEFAULT 'draft',
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_tenant_id ON documents(tenant_id);
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Migration: 030_add_subpages_and_activities_to_documents.sql
|
||||
-- Description: Add parent_id for subpages and tracking columns (Fixed)
|
||||
|
||||
ALTER TABLE documents
|
||||
ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES documents(id) ON DELETE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS last_updated_by UUID REFERENCES users(id),
|
||||
ADD COLUMN IF NOT EXISTS version INTEGER DEFAULT 1;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_parent_id ON documents(parent_id);
|
||||
|
||||
-- Simple activity log table
|
||||
CREATE TABLE IF NOT EXISTS document_activities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
action VARCHAR(50) NOT NULL, -- 'created', 'updated', 'deleted', 'status_change'
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_activities_doc_id ON document_activities(document_id);
|
||||
32
backend/internal/domain/document.go
Normal file
32
backend/internal/domain/document.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Document struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
ParentID *uuid.UUID `json:"parent_id" db:"parent_id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Content string `json:"content" db:"content"` // JSON for blocks
|
||||
Status string `json:"status" db:"status"` // draft, published
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
LastUpdatedBy uuid.UUID `json:"last_updated_by" db:"last_updated_by"`
|
||||
Version int `json:"version" db:"version"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type DocumentActivity struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
DocumentID uuid.UUID `json:"document_id" db:"document_id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
UserID uuid.UUID `json:"user_id" db:"user_id"`
|
||||
UserName string `json:"user_name" db:"user_name"` // For join
|
||||
Action string `json:"action" db:"action"`
|
||||
Description string `json:"description" db:"description"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
115
backend/internal/domain/erp.go
Normal file
115
backend/internal/domain/erp.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// FinancialCategory represents a category for income or expenses
|
||||
type FinancialCategory struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Type string `json:"type" db:"type"` // income, expense
|
||||
Color string `json:"color" db:"color"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// BankAccount represents a financial account in the agency
|
||||
type BankAccount struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
BankName string `json:"bank_name" db:"bank_name"`
|
||||
InitialBalance decimal.Decimal `json:"initial_balance" db:"initial_balance"`
|
||||
CurrentBalance decimal.Decimal `json:"current_balance" db:"current_balance"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Entity represents a customer or supplier in the ERP
|
||||
type Entity struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Document string `json:"document" db:"document"`
|
||||
Email string `json:"email" db:"email"`
|
||||
Phone string `json:"phone" db:"phone"`
|
||||
Type string `json:"type" db:"type"` // customer, supplier, both
|
||||
Status string `json:"status" db:"status"`
|
||||
Address string `json:"address" db:"address"`
|
||||
City string `json:"city" db:"city"`
|
||||
State string `json:"state" db:"state"`
|
||||
Zip string `json:"zip" db:"zip"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// FinancialTransaction represents a single financial movement
|
||||
type FinancialTransaction struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
AccountID *uuid.UUID `json:"account_id" db:"account_id"`
|
||||
CategoryID *uuid.UUID `json:"category_id" db:"category_id"`
|
||||
EntityID *uuid.UUID `json:"entity_id" db:"entity_id"`
|
||||
CRMCustomerID *uuid.UUID `json:"crm_customer_id" db:"crm_customer_id"`
|
||||
CompanyID *uuid.UUID `json:"company_id" db:"company_id"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Amount decimal.Decimal `json:"amount" db:"amount"`
|
||||
Type string `json:"type" db:"type"` // income, expense
|
||||
Status string `json:"status" db:"status"` // pending, paid, cancelled
|
||||
DueDate *time.Time `json:"due_date" db:"due_date"`
|
||||
PaymentDate *time.Time `json:"payment_date" db:"payment_date"`
|
||||
PaymentMethod string `json:"payment_method" db:"payment_method"`
|
||||
Attachments []string `json:"attachments" db:"attachments"`
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Product represents a product or service in the catalog
|
||||
type Product struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
SKU string `json:"sku" db:"sku"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Price decimal.Decimal `json:"price" db:"price"`
|
||||
CostPrice decimal.Decimal `json:"cost_price" db:"cost_price"`
|
||||
Type string `json:"type" db:"type"` // product, service
|
||||
StockQuantity int `json:"stock_quantity" db:"stock_quantity"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// Order represents a sales or service order
|
||||
type Order struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
|
||||
CustomerID *uuid.UUID `json:"customer_id" db:"customer_id"`
|
||||
EntityID *uuid.UUID `json:"entity_id" db:"entity_id"`
|
||||
Status string `json:"status" db:"status"` // draft, confirmed, completed, cancelled
|
||||
TotalAmount decimal.Decimal `json:"total_amount" db:"total_amount"`
|
||||
Notes string `json:"notes" db:"notes"`
|
||||
CreatedBy uuid.UUID `json:"created_by" db:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// OrderItem represents an item within an order
|
||||
type OrderItem struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
OrderID uuid.UUID `json:"order_id" db:"order_id"`
|
||||
ProductID uuid.UUID `json:"product_id" db:"product_id"`
|
||||
Quantity int `json:"quantity" db:"quantity"`
|
||||
UnitPrice decimal.Decimal `json:"unit_price" db:"unit_price"`
|
||||
TotalPrice decimal.Decimal `json:"total_price" db:"total_price"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
156
backend/internal/repository/document_repository.go
Normal file
156
backend/internal/repository/document_repository.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"aggios-app/backend/internal/domain"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type DocumentRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewDocumentRepository(db *sql.DB) *DocumentRepository {
|
||||
return &DocumentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) Create(doc *domain.Document) error {
|
||||
query := `
|
||||
INSERT INTO documents (id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $7, 1, NOW(), NOW())
|
||||
`
|
||||
_, err := r.db.Exec(query, doc.ID, doc.TenantID, doc.ParentID, doc.Title, doc.Content, doc.Status, doc.CreatedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.logActivity(doc.ID.String(), doc.TenantID.String(), doc.CreatedBy.String(), "created", "Criou o documento")
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) GetByTenant(tenantID string) ([]domain.Document, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
|
||||
FROM documents
|
||||
WHERE tenant_id = $1 AND parent_id IS NULL
|
||||
ORDER BY updated_at DESC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var docs []domain.Document
|
||||
for rows.Next() {
|
||||
var doc domain.Document
|
||||
if err := rows.Scan(&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) GetSubpages(parentID, tenantID string) ([]domain.Document, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
|
||||
FROM documents
|
||||
WHERE parent_id = $1 AND tenant_id = $2
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
rows, err := r.db.Query(query, parentID, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var docs []domain.Document
|
||||
for rows.Next() {
|
||||
var doc domain.Document
|
||||
if err := rows.Scan(&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) GetByID(id, tenantID string) (*domain.Document, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, parent_id, title, content, status, created_by, last_updated_by, version, created_at, updated_at
|
||||
FROM documents
|
||||
WHERE id = $1 AND tenant_id = $2
|
||||
`
|
||||
var doc domain.Document
|
||||
err := r.db.QueryRow(query, id, tenantID).Scan(
|
||||
&doc.ID, &doc.TenantID, &doc.ParentID, &doc.Title, &doc.Content, &doc.Status, &doc.CreatedBy, &doc.LastUpdatedBy, &doc.Version, &doc.CreatedAt, &doc.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) Update(doc *domain.Document) error {
|
||||
query := `
|
||||
UPDATE documents
|
||||
SET title = $1, content = $2, status = $3, last_updated_by = $4, version = version + 1, updated_at = NOW()
|
||||
WHERE id = $5 AND tenant_id = $6
|
||||
`
|
||||
_, err := r.db.Exec(query, doc.Title, doc.Content, doc.Status, doc.LastUpdatedBy, doc.ID, doc.TenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.logActivity(doc.ID.String(), doc.TenantID.String(), doc.LastUpdatedBy.String(), "updated", "Atualizou o conteúdo")
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) Delete(id, tenantID string) error {
|
||||
query := "DELETE FROM documents WHERE id = $1 AND tenant_id = $2"
|
||||
res, err := r.db.Exec(query, id, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, _ := res.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("document not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) logActivity(docID, tenantID, userID, action, description string) error {
|
||||
query := `
|
||||
INSERT INTO document_activities (document_id, tenant_id, user_id, action, description)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`
|
||||
_, err := r.db.Exec(query, docID, tenantID, userID, action, description)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DocumentRepository) GetActivities(docID, tenantID string) ([]domain.DocumentActivity, error) {
|
||||
query := `
|
||||
SELECT a.id, a.document_id, a.tenant_id, a.user_id, COALESCE(u.first_name, 'Usuário Removido') as user_name, a.action, a.description, a.created_at
|
||||
FROM document_activities a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
WHERE a.document_id = $1 AND a.tenant_id = $2
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 20
|
||||
`
|
||||
rows, err := r.db.Query(query, docID, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var activities []domain.DocumentActivity
|
||||
for rows.Next() {
|
||||
var a domain.DocumentActivity
|
||||
err := rows.Scan(&a.ID, &a.DocumentID, &a.TenantID, &a.UserID, &a.UserName, &a.Action, &a.Description, &a.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
activities = append(activities, a)
|
||||
}
|
||||
return activities, nil
|
||||
}
|
||||
493
backend/internal/repository/erp_repository.go
Normal file
493
backend/internal/repository/erp_repository.go
Normal file
@@ -0,0 +1,493 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"aggios-app/backend/internal/domain"
|
||||
"database/sql"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type ERPRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewERPRepository(db *sql.DB) *ERPRepository {
|
||||
return &ERPRepository{db: db}
|
||||
}
|
||||
|
||||
// ==================== FINANCE: CATEGORIES ====================
|
||||
|
||||
func (r *ERPRepository) CreateFinancialCategory(cat *domain.FinancialCategory) error {
|
||||
query := `
|
||||
INSERT INTO erp_financial_categories (id, tenant_id, name, type, color, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
return r.db.QueryRow(
|
||||
query,
|
||||
cat.ID, cat.TenantID, cat.Name, cat.Type, cat.Color, cat.IsActive,
|
||||
).Scan(&cat.CreatedAt, &cat.UpdatedAt)
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetFinancialCategoriesByTenant(tenantID string) ([]domain.FinancialCategory, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, name, type, color, is_active, created_at, updated_at
|
||||
FROM erp_financial_categories
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY name ASC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var categories []domain.FinancialCategory
|
||||
for rows.Next() {
|
||||
var c domain.FinancialCategory
|
||||
err := rows.Scan(&c.ID, &c.TenantID, &c.Name, &c.Type, &c.Color, &c.IsActive, &c.CreatedAt, &c.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
categories = append(categories, c)
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// ==================== FINANCE: BANK ACCOUNTS ====================
|
||||
|
||||
func (r *ERPRepository) CreateBankAccount(acc *domain.BankAccount) error {
|
||||
query := `
|
||||
INSERT INTO erp_bank_accounts (id, tenant_id, name, bank_name, initial_balance, current_balance, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
return r.db.QueryRow(
|
||||
query,
|
||||
acc.ID, acc.TenantID, acc.Name, acc.BankName, acc.InitialBalance, acc.InitialBalance, acc.IsActive,
|
||||
).Scan(&acc.CreatedAt, &acc.UpdatedAt)
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetBankAccountsByTenant(tenantID string) ([]domain.BankAccount, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, name, bank_name, initial_balance, current_balance, is_active, created_at, updated_at
|
||||
FROM erp_bank_accounts
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY name ASC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var accounts []domain.BankAccount
|
||||
for rows.Next() {
|
||||
var a domain.BankAccount
|
||||
err := rows.Scan(&a.ID, &a.TenantID, &a.Name, &a.BankName, &a.InitialBalance, &a.CurrentBalance, &a.IsActive, &a.CreatedAt, &a.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// ==================== ENTITIES: CUSTOMERS & SUPPLIERS ====================
|
||||
|
||||
func (r *ERPRepository) CreateEntity(e *domain.Entity) error {
|
||||
query := `
|
||||
INSERT INTO erp_entities (id, tenant_id, name, document, email, phone, type, status, address, city, state, zip, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
return r.db.QueryRow(
|
||||
query,
|
||||
e.ID, e.TenantID, e.Name, e.Document, e.Email, e.Phone, e.Type, e.Status, e.Address, e.City, e.State, e.Zip, e.Notes,
|
||||
).Scan(&e.CreatedAt, &e.UpdatedAt)
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetEntitiesByTenant(tenantID string, entityType string) ([]domain.Entity, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, name, document, email, phone, type, status, address, city, state, zip, notes, created_at, updated_at
|
||||
FROM erp_entities
|
||||
WHERE tenant_id = $1
|
||||
`
|
||||
var args []interface{}
|
||||
args = append(args, tenantID)
|
||||
|
||||
if entityType != "" {
|
||||
query += " AND (type = $2 OR type = 'both')"
|
||||
args = append(args, entityType)
|
||||
}
|
||||
|
||||
query += " ORDER BY name ASC"
|
||||
|
||||
rows, err := r.db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entities []domain.Entity
|
||||
for rows.Next() {
|
||||
var e domain.Entity
|
||||
err := rows.Scan(
|
||||
&e.ID, &e.TenantID, &e.Name, &e.Document, &e.Email, &e.Phone, &e.Type, &e.Status, &e.Address, &e.City, &e.State, &e.Zip, &e.Notes, &e.CreatedAt, &e.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entities = append(entities, e)
|
||||
}
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
// ==================== FINANCE: TRANSACTIONS ====================
|
||||
|
||||
func (r *ERPRepository) CreateTransaction(t *domain.FinancialTransaction) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
query := `
|
||||
INSERT INTO erp_financial_transactions (
|
||||
id, tenant_id, account_id, category_id, entity_id, crm_customer_id, company_id, description, amount, type, status, due_date, payment_date, payment_method, attachments, created_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
err = tx.QueryRow(
|
||||
query,
|
||||
t.ID, t.TenantID, t.AccountID, t.CategoryID, t.EntityID, t.CRMCustomerID, t.CompanyID, t.Description, t.Amount, t.Type, t.Status, t.DueDate, t.PaymentDate, t.PaymentMethod, pq.Array(t.Attachments), t.CreatedBy,
|
||||
).Scan(&t.CreatedAt, &t.UpdatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update balance if paid
|
||||
if t.Status == "paid" && t.AccountID != nil {
|
||||
balanceQuery := ""
|
||||
if t.Type == "income" {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
|
||||
} else {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
|
||||
}
|
||||
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetTransactionsByTenant(tenantID string) ([]domain.FinancialTransaction, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, account_id, category_id, entity_id, crm_customer_id, company_id, description, amount, type, status, due_date, payment_date, payment_method, attachments, created_by, created_at, updated_at
|
||||
FROM erp_financial_transactions
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var transactions []domain.FinancialTransaction
|
||||
for rows.Next() {
|
||||
var t domain.FinancialTransaction
|
||||
err := rows.Scan(
|
||||
&t.ID, &t.TenantID, &t.AccountID, &t.CategoryID, &t.EntityID, &t.CRMCustomerID, &t.CompanyID, &t.Description, &t.Amount, &t.Type, &t.Status, &t.DueDate, &t.PaymentDate, &t.PaymentMethod, pq.Array(&t.Attachments), &t.CreatedBy, &t.CreatedAt, &t.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transactions = append(transactions, t)
|
||||
}
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// ==================== PRODUCTS ====================
|
||||
|
||||
func (r *ERPRepository) CreateProduct(p *domain.Product) error {
|
||||
query := `
|
||||
INSERT INTO erp_products (id, tenant_id, name, sku, description, price, cost_price, type, stock_quantity, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
return r.db.QueryRow(
|
||||
query,
|
||||
p.ID, p.TenantID, p.Name, p.SKU, p.Description, p.Price, p.CostPrice, p.Type, p.StockQuantity, p.IsActive,
|
||||
).Scan(&p.CreatedAt, &p.UpdatedAt)
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetProductsByTenant(tenantID string) ([]domain.Product, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, name, sku, description, price, cost_price, type, stock_quantity, is_active, created_at, updated_at
|
||||
FROM erp_products
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY name ASC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var products []domain.Product
|
||||
for rows.Next() {
|
||||
var p domain.Product
|
||||
err := rows.Scan(&p.ID, &p.TenantID, &p.Name, &p.SKU, &p.Description, &p.Price, &p.CostPrice, &p.Type, &p.StockQuantity, &p.IsActive, &p.CreatedAt, &p.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
products = append(products, p)
|
||||
}
|
||||
return products, nil
|
||||
}
|
||||
|
||||
// ==================== ORDERS ====================
|
||||
|
||||
func (r *ERPRepository) CreateOrder(o *domain.Order, items []domain.OrderItem) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
orderQuery := `
|
||||
INSERT INTO erp_orders (id, tenant_id, customer_id, entity_id, status, total_amount, notes, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING created_at, updated_at
|
||||
`
|
||||
err = tx.QueryRow(
|
||||
orderQuery,
|
||||
o.ID, o.TenantID, o.CustomerID, o.EntityID, o.Status, o.TotalAmount, o.Notes, o.CreatedBy,
|
||||
).Scan(&o.CreatedAt, &o.UpdatedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
itemQuery := `
|
||||
INSERT INTO erp_order_items (id, order_id, product_id, quantity, unit_price, total_price)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
for _, item := range items {
|
||||
_, err = tx.Exec(itemQuery, item.ID, o.ID, item.ProductID, item.Quantity, item.UnitPrice, item.TotalPrice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update stock if product
|
||||
stockQuery := "UPDATE erp_products SET stock_quantity = stock_quantity - $1 WHERE id = $2 AND type = 'product'"
|
||||
_, err = tx.Exec(stockQuery, item.Quantity, item.ProductID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *ERPRepository) GetOrdersByTenant(tenantID string) ([]domain.Order, error) {
|
||||
query := `
|
||||
SELECT id, tenant_id, customer_id, status, total_amount, notes, created_by, created_at, updated_at
|
||||
FROM erp_orders
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := r.db.Query(query, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orders []domain.Order
|
||||
for rows.Next() {
|
||||
var o domain.Order
|
||||
err := rows.Scan(&o.ID, &o.TenantID, &o.CustomerID, &o.Status, &o.TotalAmount, &o.Notes, &o.CreatedBy, &o.CreatedAt, &o.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orders = append(orders, o)
|
||||
}
|
||||
return orders, nil
|
||||
}
|
||||
func (r *ERPRepository) UpdateTransaction(t *domain.FinancialTransaction) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Get old transaction to adjust balance
|
||||
var oldT domain.FinancialTransaction
|
||||
err = tx.QueryRow(`
|
||||
SELECT amount, type, status, account_id
|
||||
FROM erp_financial_transactions
|
||||
WHERE id = $1 AND tenant_id = $2`, t.ID, t.TenantID).
|
||||
Scan(&oldT.Amount, &oldT.Type, &oldT.Status, &oldT.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Falls back to old type if not provided in request
|
||||
if t.Type == "" {
|
||||
t.Type = oldT.Type
|
||||
}
|
||||
|
||||
// Reverse old balance impact
|
||||
if oldT.Status == "paid" && oldT.AccountID != nil {
|
||||
balanceQuery := ""
|
||||
if oldT.Type == "income" {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
|
||||
} else {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
|
||||
}
|
||||
_, err = tx.Exec(balanceQuery, oldT.Amount, oldT.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE erp_financial_transactions
|
||||
SET description = $1, amount = $2, type = $3, status = $4, due_date = $5, payment_date = $6,
|
||||
category_id = $7, entity_id = $8, crm_customer_id = $9, company_id = $10, account_id = $11, payment_method = $12, updated_at = NOW()
|
||||
WHERE id = $13 AND tenant_id = $14
|
||||
`
|
||||
_, err = tx.Exec(query,
|
||||
t.Description, t.Amount, t.Type, t.Status, t.DueDate, t.PaymentDate,
|
||||
t.CategoryID, t.EntityID, t.CRMCustomerID, t.CompanyID, t.AccountID, t.PaymentMethod,
|
||||
t.ID, t.TenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply new balance impact
|
||||
if t.Status == "paid" && t.AccountID != nil {
|
||||
balanceQuery := ""
|
||||
if t.Type == "income" {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
|
||||
} else {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
|
||||
}
|
||||
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *ERPRepository) DeleteTransaction(id, tenantID string) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Adjust balance before delete
|
||||
var t domain.FinancialTransaction
|
||||
err = tx.QueryRow(`
|
||||
SELECT amount, type, status, account_id
|
||||
FROM erp_financial_transactions
|
||||
WHERE id = $1 AND tenant_id = $2`, id, tenantID).
|
||||
Scan(&t.Amount, &t.Type, &t.Status, &t.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Status == "paid" && t.AccountID != nil {
|
||||
balanceQuery := ""
|
||||
if t.Type == "income" {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance - $1 WHERE id = $2"
|
||||
} else {
|
||||
balanceQuery = "UPDATE erp_bank_accounts SET current_balance = current_balance + $1 WHERE id = $2"
|
||||
}
|
||||
_, err = tx.Exec(balanceQuery, t.Amount, t.AccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.Exec("DELETE FROM erp_financial_transactions WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
|
||||
func (r *ERPRepository) UpdateEntity(e *domain.Entity) error {
|
||||
query := `
|
||||
UPDATE erp_entities
|
||||
SET name = $1, document = $2, email = $3, phone = $4, type = $5, status = $6,
|
||||
address = $7, city = $8, state = $9, zip = $10, notes = $11, updated_at = NOW()
|
||||
WHERE id = $12 AND tenant_id = $13
|
||||
`
|
||||
_, err := r.db.Exec(query, e.Name, e.Document, e.Email, e.Phone, e.Type, e.Status, e.Address, e.City, e.State, e.Zip, e.Notes, e.ID, e.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) DeleteEntity(id, tenantID string) error {
|
||||
_, err := r.db.Exec("DELETE FROM erp_entities WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) UpdateProduct(p *domain.Product) error {
|
||||
query := `
|
||||
UPDATE erp_products
|
||||
SET name = $1, sku = $2, description = $3, price = $4, cost_price = $5,
|
||||
type = $6, stock_quantity = $7, is_active = $8, updated_at = NOW()
|
||||
WHERE id = $9 AND tenant_id = $10
|
||||
`
|
||||
_, err := r.db.Exec(query, p.Name, p.SKU, p.Description, p.Price, p.CostPrice, p.Type, p.StockQuantity, p.IsActive, p.ID, p.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) DeleteProduct(id, tenantID string) error {
|
||||
_, err := r.db.Exec("DELETE FROM erp_products WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) UpdateBankAccount(a *domain.BankAccount) error {
|
||||
query := `
|
||||
UPDATE erp_bank_accounts
|
||||
SET name = $1, bank_name = $2, initial_balance = $3, is_active = $4, updated_at = NOW()
|
||||
WHERE id = $5 AND tenant_id = $6
|
||||
`
|
||||
_, err := r.db.Exec(query, a.Name, a.BankName, a.InitialBalance, a.IsActive, a.ID, a.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) DeleteBankAccount(id, tenantID string) error {
|
||||
_, err := r.db.Exec("DELETE FROM erp_bank_accounts WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ERPRepository) DeleteOrder(id, tenantID string) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Deleta os itens do pedido primeiro
|
||||
_, err = tx.Exec("DELETE FROM erp_order_items WHERE order_id = $1", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deleta o pedido
|
||||
_, err = tx.Exec("DELETE FROM erp_orders WHERE id = $1 AND tenant_id = $2", id, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
BIN
build_error.log
Normal file
BIN
build_error.log
Normal file
Binary file not shown.
@@ -5,6 +5,7 @@ import { AgencyBranding } from '@/components/layout/AgencyBranding';
|
||||
import AuthGuard from '@/components/auth/AuthGuard';
|
||||
import { CRMFilterProvider } from '@/contexts/CRMFilterContext';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getUser } from '@/lib/auth';
|
||||
import {
|
||||
HomeIcon,
|
||||
RocketLaunchIcon,
|
||||
@@ -12,10 +13,30 @@ import {
|
||||
RectangleStackIcon,
|
||||
UsersIcon,
|
||||
MegaphoneIcon,
|
||||
BanknotesIcon,
|
||||
CubeIcon,
|
||||
ShoppingCartIcon,
|
||||
ArrowDownCircleIcon,
|
||||
ChartBarIcon,
|
||||
WalletIcon,
|
||||
UserGroupIcon,
|
||||
ArchiveBoxIcon,
|
||||
AdjustmentsHorizontalIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
ArrowTrendingDownIcon,
|
||||
DocumentTextIcon,
|
||||
ShoppingBagIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const AGENCY_MENU_ITEMS = [
|
||||
{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: HomeIcon },
|
||||
{
|
||||
id: 'documentos',
|
||||
label: 'Documentos',
|
||||
href: '/documentos',
|
||||
icon: DocumentTextIcon,
|
||||
requiredSolution: 'documentos'
|
||||
},
|
||||
{
|
||||
id: 'crm',
|
||||
label: 'CRM',
|
||||
@@ -30,6 +51,21 @@ const AGENCY_MENU_ITEMS = [
|
||||
{ label: 'Leads', href: '/crm/leads', icon: UserPlusIcon },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'erp',
|
||||
label: 'ERP',
|
||||
href: '/erp',
|
||||
icon: BanknotesIcon,
|
||||
requiredSolution: 'erp',
|
||||
subItems: [
|
||||
{ label: 'Visão Geral', href: '/erp', icon: ChartBarIcon },
|
||||
{ label: 'Produtos e Estoque', href: '/erp/estoque', icon: ArchiveBoxIcon },
|
||||
{ label: 'Pedidos e Vendas', href: '/erp/pedidos', icon: ShoppingBagIcon },
|
||||
{ label: 'Caixa', href: '/erp/caixa', icon: WalletIcon },
|
||||
{ label: 'Contas a Receber', href: '/erp/receber', icon: ArrowTrendingUpIcon },
|
||||
{ label: 'Contas a Pagar', href: '/erp/pagar', icon: ArrowTrendingDownIcon },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
interface AgencyLayoutClientProps {
|
||||
@@ -67,10 +103,23 @@ export function AgencyLayoutClient({ children, colors }: AgencyLayoutClientProps
|
||||
console.log('🏷️ Slugs das soluções:', solutionSlugs);
|
||||
|
||||
// Sempre mostrar dashboard + soluções disponíveis
|
||||
// Segurança Máxima: ERP só para ADMIN_AGENCIA
|
||||
const user = getUser();
|
||||
const filtered = AGENCY_MENU_ITEMS.filter(item => {
|
||||
if (item.id === 'dashboard') return true;
|
||||
|
||||
// ERP restrito a administradores da agência
|
||||
if (item.id === 'erp' && user?.role !== 'ADMIN_AGENCIA') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requiredSolution = (item as any).requiredSolution;
|
||||
return solutionSlugs.includes((requiredSolution || item.id).toLowerCase());
|
||||
const hasSolution = solutionSlugs.includes((requiredSolution || item.id).toLowerCase());
|
||||
|
||||
// Temporariamente forçar a exibição de Documentos para debug
|
||||
if (item.id === 'documentos') return true;
|
||||
|
||||
return hasSolution;
|
||||
});
|
||||
|
||||
console.log('📋 Menu filtrado:', filtered.map(i => i.id));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { Menu, Transition, Tab } from '@headlessui/react';
|
||||
import ConfirmDialog from '@/components/layout/ConfirmDialog';
|
||||
import { ConfirmDialog, BulkActionBar } from "@/components/ui";
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import {
|
||||
UserIcon,
|
||||
@@ -83,12 +83,14 @@ export default function CustomersPage() {
|
||||
const [editingCustomer, setEditingCustomer] = useState<Customer | null>(null);
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
|
||||
const [customerToDelete, setCustomerToDelete] = useState<string | null>(null);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showPendingOnly, setShowPendingOnly] = useState(false);
|
||||
const [detailsCustomer, setDetailsCustomer] = useState<Customer | null>(null);
|
||||
const [isApprovingId, setIsApprovingId] = useState<string | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
|
||||
// Portal Access States
|
||||
const [isPortalModalOpen, setIsPortalModalOpen] = useState(false);
|
||||
@@ -179,6 +181,93 @@ export default function CustomersPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Clientes] Error fetching customers:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setBulkConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmBulkDelete = async () => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const results = await Promise.all(selectedIds.map(async (id) => {
|
||||
const response = await fetch(`/api/crm/customers/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
return { id, ok: response.ok };
|
||||
}));
|
||||
|
||||
const successful = results.filter(r => r.ok).length;
|
||||
if (successful > 0) {
|
||||
toast.success('Exclusão completa', `${successful} clientes excluídos com sucesso.`);
|
||||
}
|
||||
if (successful < selectedIds.length) {
|
||||
toast.error('Erro', 'Não foi possível excluir alguns clientes.');
|
||||
}
|
||||
await fetchCustomers();
|
||||
} catch (error) {
|
||||
toast.error('Erro', 'Ocorreu um erro na exclusão em lote.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setBulkConfirmOpen(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkApprove = async () => {
|
||||
const pendingToApprove = customers.filter(c => selectedIds.includes(c.id) && isPendingApproval(c));
|
||||
if (pendingToApprove.length === 0) {
|
||||
toast.error('Nenhum dos clientes selecionados está pendente de aprovação.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await Promise.all(pendingToApprove.map(async (customer) => {
|
||||
const updatedTags = (customer.tags || []).filter(tag => tag !== PENDING_APPROVAL_TAG);
|
||||
let logoUrl = customer.logo_url;
|
||||
const publicData = parsePublicNotes(customer.notes);
|
||||
if (publicData?.logo_path && !logoUrl) {
|
||||
logoUrl = publicData.logo_path;
|
||||
}
|
||||
|
||||
return fetch(`/api/crm/customers/${customer.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: customer.name,
|
||||
email: customer.email,
|
||||
phone: customer.phone,
|
||||
company: customer.company,
|
||||
position: customer.position,
|
||||
address: customer.address,
|
||||
city: customer.city,
|
||||
state: customer.state,
|
||||
zip_code: customer.zip_code,
|
||||
country: customer.country,
|
||||
notes: customer.notes,
|
||||
tags: updatedTags,
|
||||
is_active: customer.is_active ?? true,
|
||||
logo_url: logoUrl,
|
||||
}),
|
||||
});
|
||||
}));
|
||||
toast.success('Sucesso', `${pendingToApprove.length} clientes aprovados.`);
|
||||
await fetchCustomers();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao aprovar clientes');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -213,7 +302,7 @@ export default function CustomersPage() {
|
||||
editingCustomer ? 'Cliente atualizado' : 'Cliente criado',
|
||||
editingCustomer ? 'O cliente foi atualizado com sucesso.' : 'O novo cliente foi criado com sucesso.'
|
||||
);
|
||||
fetchCustomers();
|
||||
await fetchCustomers();
|
||||
handleCloseModal();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
@@ -262,7 +351,7 @@ export default function CustomersPage() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setCustomers(customers.filter(c => c.id !== customerToDelete));
|
||||
await fetchCustomers();
|
||||
toast.success('Cliente excluído', 'O cliente foi excluído com sucesso.');
|
||||
} else {
|
||||
toast.error('Erro ao excluir', 'Não foi possível excluir o cliente.');
|
||||
@@ -361,7 +450,7 @@ export default function CustomersPage() {
|
||||
|
||||
if (response.ok) {
|
||||
toast.success('Acesso liberado!', 'O cliente já pode fazer login no portal com a senha cadastrada.');
|
||||
fetchCustomers();
|
||||
await fetchCustomers();
|
||||
setDetailsCustomer(prev => prev && prev.id === customer.id ? { ...prev, tags: updatedTags } : prev);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
@@ -524,6 +613,22 @@ export default function CustomersPage() {
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800">
|
||||
<th className="px-6 py-4 w-10 text-left">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filteredCustomers.length > 0 && selectedIds.length === filteredCustomers.length}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedIds(filteredCustomers.map(c => c.id));
|
||||
} else {
|
||||
setSelectedIds([]);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Cliente</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Empresa</th>
|
||||
<th className="px-6 py-4 text-left text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Contato</th>
|
||||
@@ -540,11 +645,28 @@ export default function CustomersPage() {
|
||||
return (
|
||||
<tr
|
||||
key={customer.id}
|
||||
className={`group transition-colors ${pendingApproval
|
||||
onClick={() => openDetails(customer)}
|
||||
className={`group transition-colors cursor-pointer ${pendingApproval
|
||||
? 'bg-amber-50/40 dark:bg-amber-900/10 hover:bg-amber-50/70 dark:hover:bg-amber-900/20'
|
||||
: 'hover:bg-zinc-50 dark:hover:bg-zinc-800/50'
|
||||
}`}
|
||||
} ${selectedIds.includes(customer.id) ? 'bg-brand-50/30 dark:bg-brand-500/5' : ''}`}
|
||||
>
|
||||
<td className="px-6 py-4 w-10" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(customer.id)}
|
||||
onChange={() => {
|
||||
if (selectedIds.includes(customer.id)) {
|
||||
setSelectedIds(selectedIds.filter(id => id !== customer.id));
|
||||
} else {
|
||||
setSelectedIds([...selectedIds, customer.id]);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
{customer.logo_url ? (
|
||||
@@ -1010,20 +1132,38 @@ export default function CustomersPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog isOpen={confirmOpen} onClose={() => setConfirmOpen(false)} onConfirm={handleConfirmDelete} title="Excluir Cadastro" message="Tem certeza? Isso pode afetar lançamentos vinculados a esta entidade no ERP." confirmText="Excluir" cancelText="Cancelar" variant="danger" />
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={confirmOpen}
|
||||
onClose={() => {
|
||||
setConfirmOpen(false);
|
||||
setCustomerToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Excluir Cliente"
|
||||
message="Tem certeza que deseja excluir este cliente? Esta ação não pode ser desfeita."
|
||||
confirmText="Excluir"
|
||||
isOpen={bulkConfirmOpen}
|
||||
onClose={() => setBulkConfirmOpen(false)}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="Excluir Clientes Selecionados"
|
||||
message={`Tem certeza que deseja excluir os ${selectedIds.length} clientes selecionados? Esta ação não pode ser desfeita.`}
|
||||
confirmText="Excluir Tudo"
|
||||
cancelText="Cancelar"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<BulkActionBar
|
||||
selectedCount={selectedIds.length}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
actions={[
|
||||
...(customers.some(c => selectedIds.includes(c.id) && isPendingApproval(c)) ? [{
|
||||
label: "Aprovar Selecionados",
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
onClick: handleBulkApprove,
|
||||
variant: 'primary' as const
|
||||
}] : []),
|
||||
{
|
||||
label: "Excluir Selecionados",
|
||||
icon: <TrashIcon className="w-5 h-5" />,
|
||||
onClick: handleBulkDelete,
|
||||
variant: 'danger'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Modal de Acesso ao Portal */}
|
||||
{isPortalModalOpen && selectedCustomerForPortal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
|
||||
@@ -1,15 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import { PageHeader, DataTable, Card, Badge } from '@/components/ui';
|
||||
import {
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
DocumentTextIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
ArrowPathIcon,
|
||||
EyeIcon,
|
||||
ClockIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { docApi, Document } from '@/lib/api-docs';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import DocumentEditor from '@/components/documentos/DocumentEditor';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
export default function DocumentosPage() {
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentDoc, setCurrentDoc] = useState<Partial<Document> | null>(null);
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 8;
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments();
|
||||
}, []);
|
||||
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await docApi.getDocuments();
|
||||
setDocuments(data || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar documentos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const newDoc = await docApi.createDocument({
|
||||
title: 'Novo Documento',
|
||||
content: '{"type":"doc","content":[{"type":"paragraph"}]}',
|
||||
status: 'published',
|
||||
parent_id: null
|
||||
});
|
||||
setCurrentDoc(newDoc);
|
||||
setIsEditing(true);
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao iniciar novo documento');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (doc: Document) => {
|
||||
setCurrentDoc(doc);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async (docData: Partial<Document>) => {
|
||||
try {
|
||||
if (docData.id) {
|
||||
await docApi.updateDocument(docData.id, docData);
|
||||
// toast.success('Documento atualizado!'); // Auto-save já acontece
|
||||
} else {
|
||||
await docApi.createDocument(docData);
|
||||
toast.success('Documento criado!');
|
||||
}
|
||||
setIsEditing(false);
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao salvar documento');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Tem certeza que deseja excluir este documento e todas as suas subpáginas?')) return;
|
||||
try {
|
||||
await docApi.deleteDocument(id);
|
||||
toast.success('Documento excluído!');
|
||||
fetchDocuments();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao excluir documento');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredDocuments = useMemo(() => {
|
||||
return documents.filter(doc =>
|
||||
(doc.title || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(doc.content || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}, [documents, searchTerm]);
|
||||
|
||||
const paginatedDocuments = useMemo(() => {
|
||||
const start = (currentPage - 1) * itemsPerPage;
|
||||
return filteredDocuments.slice(start, start + itemsPerPage);
|
||||
}, [filteredDocuments, currentPage]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: 'Documento',
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="p-2.5 bg-zinc-50 dark:bg-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-700 shadow-sm">
|
||||
<DocumentTextIcon className="w-5 h-5 text-zinc-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-zinc-900 dark:text-white group-hover:text-brand-500 transition-colors uppercase tracking-tight text-sm">
|
||||
{doc.title || 'Sem título'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<Badge variant="info" className="text-[8px] px-1.5 font-black">v{doc.version || 1}</Badge>
|
||||
<span className="text-[10px] text-zinc-400 font-medium">#{doc.id.substring(0, 8)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Última Modificação',
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<ClockIcon className="w-4 h-4 text-zinc-300" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-zinc-600 dark:text-zinc-400">
|
||||
{format(parseISO(doc.updated_at), "dd 'de' MMM", { locale: ptBR })}
|
||||
</span>
|
||||
<span className="text-[9px] text-zinc-400 uppercase font-black tracking-tighter">
|
||||
às {format(parseISO(doc.updated_at), "HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Ações',
|
||||
align: 'right' as const,
|
||||
accessor: (doc: Document) => (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(doc)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-xs font-black uppercase tracking-widest text-zinc-600 dark:text-zinc-400 hover:text-brand-500 hover:bg-brand-50 dark:hover:bg-brand-500/10 rounded-xl transition-all"
|
||||
>
|
||||
<PencilSquareIcon className="w-4 h-4" />
|
||||
Abrir
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(doc.id)}
|
||||
className="p-2 text-zinc-300 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-all"
|
||||
title="Excluir"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<SolutionGuard requiredSolution="documentos">
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">Documentos</h1>
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-8 text-center">
|
||||
<p className="text-gray-500">Gestão Eletrônica de Documentos (GED) em breve</p>
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-8 animate-in fade-in duration-700">
|
||||
<PageHeader
|
||||
title="Wiki & Base de Conhecimento"
|
||||
description="Organize processos, manuais e documentação técnica da agência."
|
||||
primaryAction={{
|
||||
label: "Criar Novo",
|
||||
icon: <PlusIcon className="w-5 h-5" />,
|
||||
onClick: handleCreate
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center justify-between bg-white dark:bg-zinc-900/50 p-4 rounded-[28px] border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<div className="w-full md:w-96 relative">
|
||||
<MagnifyingGlassIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-zinc-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Pesquisar wiki..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-100 dark:border-zinc-800 rounded-2xl pl-12 pr-4 py-3 text-sm outline-none focus:ring-2 ring-brand-500/20 transition-all font-semibold placeholder:text-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={fetchDocuments}
|
||||
className="p-3 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ArrowPathIcon className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<div className="h-6 w-px bg-zinc-200 dark:border-zinc-800" />
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-2xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-zinc-200 dark:shadow-none">
|
||||
{filteredDocuments.length} Documentos
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card noPadding allowOverflow className="border-none shadow-2xl shadow-black/5 overflow-hidden rounded-[32px]">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={paginatedDocuments}
|
||||
isLoading={loading}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="p-6 border-t border-zinc-50 dark:border-zinc-800 flex items-center justify-between bg-zinc-50/50 dark:bg-zinc-900/50">
|
||||
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">
|
||||
{filteredDocuments.length} itens no total
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage(p => p - 1)}
|
||||
className="px-6 py-2.5 text-[10px] font-black uppercase tracking-widest bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl disabled:opacity-30 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-all shadow-sm"
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<button
|
||||
disabled={currentPage * itemsPerPage >= filteredDocuments.length}
|
||||
onClick={() => setCurrentPage(p => p + 1)}
|
||||
className="px-6 py-2.5 text-[10px] font-black uppercase tracking-widest bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 rounded-xl disabled:opacity-30 hover:bg-black dark:hover:bg-white transition-all shadow-lg active:scale-95"
|
||||
>
|
||||
Próximo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{isEditing && (
|
||||
<DocumentEditor
|
||||
initialDocument={currentDoc}
|
||||
onSave={handleSave}
|
||||
onCancel={() => {
|
||||
setIsEditing(false);
|
||||
fetchDocuments();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</SolutionGuard>
|
||||
);
|
||||
|
||||
211
front-end-agency/app/(agency)/erp/ERPSettingsPage.tsx
Normal file
211
front-end-agency/app/(agency)/erp/ERPSettingsPage.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
PlusIcon,
|
||||
BanknotesIcon,
|
||||
TagIcon,
|
||||
CheckIcon,
|
||||
XMarkIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
BuildingLibraryIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { erpApi, FinancialCategory, BankAccount } from '@/lib/api-erp';
|
||||
import { formatCurrency } from '@/lib/format';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {
|
||||
PageHeader,
|
||||
DataTable,
|
||||
Input,
|
||||
Card,
|
||||
Tabs
|
||||
} from "@/components/ui";
|
||||
|
||||
export default function ERPSettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Configurações do ERP"
|
||||
description="Gerencie categorias financeiras, contas bancárias e outras preferências do sistema."
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
variant="pills"
|
||||
items={[
|
||||
{
|
||||
label: 'Categorias Financeiras',
|
||||
icon: <TagIcon className="w-4 h-4" />,
|
||||
content: <CategorySettings />
|
||||
},
|
||||
{
|
||||
label: 'Contas Bancárias',
|
||||
icon: <BuildingLibraryIcon className="w-4 h-4" />,
|
||||
content: <AccountSettings />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategorySettings() {
|
||||
const [categories, setCategories] = useState<FinancialCategory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await erpApi.getFinancialCategories();
|
||||
setCategories(data || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar categorias');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Categorias</h3>
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 text-white rounded-xl font-bold shadow-lg hover:opacity-90 transition-all text-sm"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Nova Categoria
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Card noPadding className="overflow-hidden">
|
||||
<DataTable
|
||||
isLoading={loading}
|
||||
data={categories}
|
||||
columns={[
|
||||
{
|
||||
header: 'Nome',
|
||||
accessor: (row) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: row.color }} />
|
||||
<span className="font-bold text-zinc-900 dark:text-white">{row.name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Tipo',
|
||||
accessor: (row) => (
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${row.type === 'income' ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'}`}>
|
||||
{row.type === 'income' ? 'Receita' : 'Despesa'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessor: (row) => (
|
||||
<span className={`text-xs font-bold ${row.is_active ? 'text-emerald-500' : 'text-zinc-400'}`}>
|
||||
{row.is_active ? 'Ativo' : 'Inativo'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
className: 'text-right',
|
||||
accessor: () => (
|
||||
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<button className="p-2 text-zinc-400 hover:text-brand-600 dark:hover:text-brand-400">
|
||||
<PencilSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountSettings() {
|
||||
const [accounts, setAccounts] = useState<BankAccount[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await erpApi.getBankAccounts();
|
||||
setAccounts(data || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar contas');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Contas Bancárias</h3>
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 text-white rounded-xl font-bold shadow-lg hover:opacity-90 transition-all text-sm"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
Nova Conta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Card noPadding className="overflow-hidden">
|
||||
<DataTable
|
||||
isLoading={loading}
|
||||
data={accounts}
|
||||
columns={[
|
||||
{
|
||||
header: 'Nome da Conta',
|
||||
accessor: (row) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-zinc-900 dark:text-white">{row.name}</span>
|
||||
<span className="text-xs text-zinc-400 font-bold uppercase">{row.bank_name}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Saldo Atual',
|
||||
className: 'text-right',
|
||||
accessor: (row) => (
|
||||
<span className="font-black text-zinc-900 dark:text-white">
|
||||
{formatCurrency(row.current_balance)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessor: (row) => (
|
||||
<span className={`text-xs font-bold ${row.is_active ? 'text-emerald-500' : 'text-zinc-400'}`}>
|
||||
{row.is_active ? 'Ativo' : 'Inativo'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
className: 'text-right',
|
||||
accessor: () => (
|
||||
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<button className="p-2 text-zinc-400 hover:text-brand-600 dark:hover:text-brand-400">
|
||||
<PencilSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
front-end-agency/app/(agency)/erp/OrdersPage.tsx
Normal file
309
front-end-agency/app/(agency)/erp/OrdersPage.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import {
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
ShoppingBagIcon,
|
||||
CalendarIcon,
|
||||
CurrencyDollarIcon,
|
||||
UserIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
XMarkIcon,
|
||||
EyeIcon,
|
||||
TrashIcon,
|
||||
ExclamationTriangleIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { ConfirmDialog } from "@/components/ui";
|
||||
import { erpApi, Order, Entity } from '@/lib/api-erp';
|
||||
import { formatCurrency } from '@/lib/format';
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import {
|
||||
PageHeader,
|
||||
StatsCard,
|
||||
DataTable,
|
||||
Input,
|
||||
Card,
|
||||
BulkActionBar,
|
||||
} from "@/components/ui";
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default function OrdersPage() {
|
||||
const toast = useToast();
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [entities, setEntities] = useState<Entity[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
|
||||
const [orderToDelete, setOrderToDelete] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async (silent = false) => {
|
||||
try {
|
||||
if (!silent) setLoading(true);
|
||||
const [ordersData, entitiesData] = await Promise.all([
|
||||
erpApi.getOrders(),
|
||||
erpApi.getEntities()
|
||||
]);
|
||||
setOrders(ordersData || []);
|
||||
setEntities(entitiesData || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar', 'Não foi possível carregar os pedidos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setBulkConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const originalOrders = [...orders];
|
||||
const idsToDelete = selectedIds.map(String);
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setOrders(prev => prev.filter(o => !idsToDelete.includes(String(o.id))));
|
||||
const deletedCount = selectedIds.length;
|
||||
|
||||
try {
|
||||
await Promise.all(idsToDelete.map(id => erpApi.deleteOrder(id)));
|
||||
toast.success('Exclusão completa', `${deletedCount} pedidos excluídos com sucesso.`);
|
||||
setTimeout(() => fetchData(true), 500);
|
||||
} catch (error) {
|
||||
setOrders(originalOrders);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir alguns pedidos.');
|
||||
} finally {
|
||||
setBulkConfirmOpen(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setOrderToDelete(id);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!orderToDelete) return;
|
||||
|
||||
const originalOrders = [...orders];
|
||||
const idToDelete = String(orderToDelete);
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setOrders(prev => prev.filter(o => String(o.id) !== idToDelete));
|
||||
|
||||
try {
|
||||
await erpApi.deleteOrder(idToDelete);
|
||||
toast.success('Exclusão completa', 'O pedido foi removido com sucesso.');
|
||||
setTimeout(() => fetchData(true), 500);
|
||||
} catch (error) {
|
||||
setOrders(originalOrders);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir o pedido.');
|
||||
} finally {
|
||||
setConfirmOpen(false);
|
||||
setOrderToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredOrders = orders.filter(o => {
|
||||
const entityName = entities.find(e => e.id === o.entity_id)?.name || '';
|
||||
const searchStr = searchTerm.toLowerCase();
|
||||
return String(o.id).toLowerCase().includes(searchStr) ||
|
||||
entityName.toLowerCase().includes(searchStr);
|
||||
});
|
||||
|
||||
const totalRevenue = orders.filter(o => o.status !== 'cancelled').reduce((sum, o) => sum + Number(o.total_amount), 0);
|
||||
const pendingOrders = orders.filter(o => o.status === 'confirmed').length;
|
||||
const completedOrders = orders.filter(o => o.status === 'completed').length;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: 'Pedido / Data',
|
||||
accessor: (row: Order) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-zinc-900 dark:text-white uppercase text-xs">#{row.id.slice(0, 8)}</span>
|
||||
<div className="flex items-center gap-1 text-[10px] text-zinc-400 font-bold">
|
||||
<CalendarIcon className="w-3 h-3" />
|
||||
{row.created_at ? format(new Date(row.created_at), 'dd/MM/yyyy HH:mm') : '-'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Cliente',
|
||||
accessor: (row: Order) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-zinc-100 dark:bg-zinc-800 text-zinc-500">
|
||||
<UserIcon className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-zinc-900 dark:text-white">
|
||||
{entities.find(e => e.id === row.entity_id)?.name || 'Consumidor Final'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
accessor: (row: Order) => {
|
||||
const colors = {
|
||||
draft: 'bg-zinc-100 text-zinc-700',
|
||||
confirmed: 'bg-blue-100 text-blue-700',
|
||||
completed: 'bg-emerald-100 text-emerald-700',
|
||||
cancelled: 'bg-rose-100 text-rose-700'
|
||||
};
|
||||
const labels = {
|
||||
draft: 'Rascunho',
|
||||
confirmed: 'Confirmado',
|
||||
completed: 'Concluído',
|
||||
cancelled: 'Cancelado'
|
||||
};
|
||||
return (
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider ${colors[row.status as keyof typeof colors]}`}>
|
||||
{labels[row.status as keyof typeof labels]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'Total',
|
||||
className: 'text-right',
|
||||
accessor: (row: Order) => (
|
||||
<span className="font-black text-zinc-900 dark:text-white">
|
||||
{formatCurrency(row.total_amount)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
className: 'text-right',
|
||||
accessor: (row: Order) => (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button className="p-2 text-zinc-400 hover:text-brand-600 dark:hover:text-brand-400">
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(row.id); }}
|
||||
className="p-2 text-zinc-400 hover:text-rose-600 dark:hover:text-rose-400 transition-all"
|
||||
>
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Pedidos & Vendas"
|
||||
description="Acompanhe suas vendas, gerencie orçamentos e controle o fluxo de pedidos."
|
||||
primaryAction={{
|
||||
label: "Novo Pedido",
|
||||
icon: <PlusIcon className="w-5 h-5" />,
|
||||
onClick: () => toast.error('Funcionalidade em desenvolvimento')
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Receita de Vendas"
|
||||
value={formatCurrency(totalRevenue)}
|
||||
icon={<CurrencyDollarIcon className="w-6 h-6 text-emerald-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Pedidos Pendentes"
|
||||
value={pendingOrders}
|
||||
icon={<ClockIcon className="w-6 h-6 text-blue-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Pedidos Concluídos"
|
||||
value={completedOrders}
|
||||
icon={<CheckCircleIcon className="w-6 h-6 text-emerald-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Total de Pedidos"
|
||||
value={orders.length}
|
||||
icon={<ShoppingBagIcon className="w-6 h-6 text-indigo-500" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
placeholder="Buscar por cliente ou ID do pedido..."
|
||||
className="pl-10 h-10 border-zinc-200 dark:border-zinc-800"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full sm:w-auto">
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl text-sm font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">
|
||||
<FunnelIcon className="w-4 h-4" />
|
||||
Filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card noPadding className="overflow-hidden">
|
||||
<DataTable
|
||||
selectable
|
||||
isLoading={loading}
|
||||
selectedIds={selectedIds}
|
||||
onSelectionChange={setSelectedIds}
|
||||
columns={columns}
|
||||
data={filteredOrders}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={bulkConfirmOpen}
|
||||
onClose={() => setBulkConfirmOpen(false)}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="Excluir Pedidos Selecionados"
|
||||
message={`Tem certeza que deseja excluir os ${selectedIds.length} pedidos selecionados? Esta ação não pode ser desfeita.`}
|
||||
confirmText="Excluir Tudo"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={confirmOpen}
|
||||
onClose={() => {
|
||||
setConfirmOpen(false);
|
||||
setOrderToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Excluir Pedido"
|
||||
message="Tem certeza que deseja excluir este pedido? Esta ação não pode ser desfeita."
|
||||
confirmText="Excluir"
|
||||
cancelText="Cancelar"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<BulkActionBar
|
||||
selectedCount={selectedIds.length}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
actions={[
|
||||
{
|
||||
label: "Excluir Selecionados",
|
||||
icon: <TrashIcon className="w-5 h-5" />,
|
||||
onClick: handleBulkDelete,
|
||||
variant: 'danger'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
503
front-end-agency/app/(agency)/erp/ProductsPage.tsx
Normal file
503
front-end-agency/app/(agency)/erp/ProductsPage.tsx
Normal file
@@ -0,0 +1,503 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import {
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
Square3Stack3DIcon as PackageIcon,
|
||||
CurrencyDollarIcon,
|
||||
ExclamationTriangleIcon,
|
||||
TrashIcon,
|
||||
PencilSquareIcon,
|
||||
XMarkIcon,
|
||||
CheckIcon,
|
||||
TagIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { erpApi, Product } from '@/lib/api-erp';
|
||||
import { formatCurrency } from '@/lib/format';
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import {
|
||||
PageHeader,
|
||||
StatsCard,
|
||||
DataTable,
|
||||
Input,
|
||||
Card,
|
||||
BulkActionBar,
|
||||
ConfirmDialog,
|
||||
} from "@/components/ui";
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||
|
||||
export default function ProductsPage() {
|
||||
const toast = useToast();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
|
||||
const [productToDelete, setProductToDelete] = useState<string | null>(null);
|
||||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<Product>>({
|
||||
name: '',
|
||||
sku: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
cost_price: 0,
|
||||
type: 'product',
|
||||
stock_quantity: 0,
|
||||
is_active: true
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
const fetchProducts = async (silent = false) => {
|
||||
try {
|
||||
if (!silent) setLoading(true);
|
||||
const data = await erpApi.getProducts();
|
||||
setProducts(data || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar', 'Não foi possível carregar os produtos');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (editingProduct?.id) {
|
||||
await erpApi.updateProduct(editingProduct.id, formData);
|
||||
toast.success('Produto atualizado com sucesso!');
|
||||
} else {
|
||||
await erpApi.createProduct(formData);
|
||||
toast.success('Produto cadastrado com sucesso!');
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setEditingProduct(null);
|
||||
resetForm();
|
||||
await fetchProducts(true);
|
||||
} catch (error) {
|
||||
toast.error(editingProduct ? 'Erro ao atualizar produto' : 'Erro ao salvar produto');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
setProductToDelete(id);
|
||||
setConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!productToDelete) return;
|
||||
|
||||
const originalProducts = [...products];
|
||||
const idToDelete = String(productToDelete);
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setProducts(prev => prev.filter(p => String(p.id) !== idToDelete));
|
||||
|
||||
try {
|
||||
await erpApi.deleteProduct(idToDelete);
|
||||
toast.success('Exclusão completa', 'O item foi removido com sucesso.');
|
||||
setTimeout(() => fetchProducts(true), 500);
|
||||
} catch (error) {
|
||||
setProducts(originalProducts);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir o produto.');
|
||||
} finally {
|
||||
setConfirmOpen(false);
|
||||
setProductToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setBulkConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
|
||||
const originalProducts = [...products];
|
||||
const idsToDelete = selectedIds.map(String);
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setProducts(prev => prev.filter(p => !idsToDelete.includes(String(p.id))));
|
||||
const deletedCount = selectedIds.length;
|
||||
|
||||
try {
|
||||
await Promise.all(idsToDelete.map(id => erpApi.deleteProduct(id)));
|
||||
toast.success('Exclusão completa', `${deletedCount} produtos excluídos com sucesso.`);
|
||||
setTimeout(() => fetchProducts(true), 500);
|
||||
} catch (error) {
|
||||
setProducts(originalProducts);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir alguns produtos.');
|
||||
} finally {
|
||||
setBulkConfirmOpen(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (product: Product) => {
|
||||
setEditingProduct(product);
|
||||
setFormData({
|
||||
name: product.name,
|
||||
sku: product.sku,
|
||||
description: product.description,
|
||||
price: Number(product.price),
|
||||
cost_price: Number(product.cost_price),
|
||||
type: product.type,
|
||||
stock_quantity: Number(product.stock_quantity),
|
||||
is_active: product.is_active
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
sku: '',
|
||||
description: '',
|
||||
price: 0,
|
||||
cost_price: 0,
|
||||
type: 'product',
|
||||
stock_quantity: 0,
|
||||
is_active: true
|
||||
});
|
||||
};
|
||||
|
||||
const filteredProducts = products.filter(p =>
|
||||
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(p.sku || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const totalStockValue = products.reduce((sum, p) => sum + (Number(p.price) * Number(p.stock_quantity)), 0);
|
||||
const lowStockItems = products.filter(p => p.type === 'product' && p.stock_quantity < 5).length;
|
||||
const servicesCount = products.filter(p => p.type === 'service').length;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: 'Produto / SKU',
|
||||
accessor: (row: Product) => (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2 rounded-lg ${row.type === 'product' ? 'bg-indigo-50 text-indigo-600' : 'bg-amber-50 text-amber-600'}`}>
|
||||
{row.type === 'product' ? <PackageIcon className="w-5 h-5" /> : <TagIcon className="w-5 h-5" />}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-zinc-900 dark:text-white uppercase tracking-tight">{row.name}</span>
|
||||
<span className="text-xs text-zinc-400 font-black tracking-widest">{row.sku || 'SEM SKU'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Tipo',
|
||||
accessor: (row: Product) => (
|
||||
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider ${row.type === 'product' ? 'bg-indigo-100 text-indigo-700' : 'bg-amber-100 text-amber-700'}`}>
|
||||
{row.type === 'product' ? 'Produto' : 'Serviço'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Estoque',
|
||||
accessor: (row: Product) => (
|
||||
row.type === 'product' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-black text-sm ${row.stock_quantity < 5 ? 'text-rose-500' : 'text-zinc-900 dark:text-white'}`}>
|
||||
{row.stock_quantity}
|
||||
</span>
|
||||
{row.stock_quantity < 5 && (
|
||||
<ExclamationTriangleIcon className="w-4 h-4 text-rose-500" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-zinc-400 text-xs">N/A</span>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'Preço de Venda',
|
||||
className: 'text-right',
|
||||
accessor: (row: Product) => (
|
||||
<span className="font-black text-zinc-900 dark:text-white">
|
||||
{formatCurrency(row.price)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
className: 'text-right',
|
||||
accessor: (row: Product) => (
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(row); }}
|
||||
className="p-2 text-zinc-400 hover:text-brand-600 dark:hover:text-brand-400 transition-all"
|
||||
>
|
||||
<PencilSquareIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(row.id); }}
|
||||
className="p-2 text-zinc-400 hover:text-rose-600 dark:hover:text-rose-400 transition-all"
|
||||
>
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Produtos & Estoque"
|
||||
description="Controle seu inventário, gerencie preços e acompanhe a disponibilidade de itens."
|
||||
primaryAction={{
|
||||
label: "Novo Item",
|
||||
icon: <PlusIcon className="w-5 h-5" />,
|
||||
onClick: () => {
|
||||
setEditingProduct(null);
|
||||
resetForm();
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Total em Estoque"
|
||||
value={formatCurrency(totalStockValue)}
|
||||
icon={<CurrencyDollarIcon className="w-6 h-6 text-emerald-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Itens com Estoque Baixo"
|
||||
value={lowStockItems}
|
||||
icon={<ExclamationTriangleIcon className="w-6 h-6 text-rose-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Total de Produtos"
|
||||
value={products.filter(p => p.type === 'product').length}
|
||||
icon={<PackageIcon className="w-6 h-6 text-indigo-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Total de Serviços"
|
||||
value={servicesCount}
|
||||
icon={<TagIcon className="w-6 h-6 text-amber-500" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
placeholder="Buscar por nome ou SKU..."
|
||||
className="pl-10 h-10 border-zinc-200 dark:border-zinc-800"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 w-full sm:w-auto">
|
||||
<button className="flex items-center gap-2 px-4 py-2 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl text-sm font-bold text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-all">
|
||||
<FunnelIcon className="w-4 h-4" />
|
||||
Filtros
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card noPadding className="overflow-hidden">
|
||||
<DataTable
|
||||
selectable
|
||||
selectedIds={selectedIds}
|
||||
onSelectionChange={setSelectedIds}
|
||||
columns={columns}
|
||||
data={filteredProducts}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Transition show={isModalOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={() => setIsModalOpen(false)}>
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<TransitionChild
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95 translate-y-4"
|
||||
enterTo="opacity-100 scale-100 translate-y-0"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100 translate-y-0"
|
||||
leaveTo="opacity-0 scale-95 translate-y-4"
|
||||
>
|
||||
<DialogPanel className="w-full max-w-2xl transform overflow-hidden rounded-[32px] bg-white dark:bg-zinc-900 p-8 text-left align-middle shadow-2xl transition-all border border-gray-100 dark:border-zinc-800">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<DialogTitle as="h3" className="text-xl font-bold text-zinc-900 dark:text-white">
|
||||
{editingProduct ? 'Editar Item' : 'Novo Produto/Serviço'}
|
||||
</DialogTitle>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-all"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6 text-zinc-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, type: 'product' })}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl border transition-all font-bold text-sm ${formData.type === 'product' ? 'border-indigo-500 bg-indigo-50 text-indigo-600' : 'border-zinc-200 dark:border-zinc-700 text-zinc-400'}`}
|
||||
>
|
||||
<PackageIcon className="w-5 h-5" />
|
||||
Produto
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, type: 'service' })}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl border transition-all font-bold text-sm ${formData.type === 'service' ? 'border-amber-500 bg-amber-50 text-amber-600' : 'border-zinc-200 dark:border-zinc-700 text-zinc-400'}`}
|
||||
>
|
||||
<TagIcon className="w-5 h-5" />
|
||||
Serviço
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Input
|
||||
label="Nome do Item"
|
||||
required
|
||||
placeholder="Ex: Teclado Mecânico RGB"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="SKU / Código"
|
||||
placeholder="Ex: PROD-001"
|
||||
value={formData.sku}
|
||||
onChange={(e) => setFormData({ ...formData, sku: e.target.value })}
|
||||
className="bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Estoque Inicial"
|
||||
type="number"
|
||||
disabled={formData.type === 'service'}
|
||||
placeholder="0"
|
||||
value={formData.stock_quantity}
|
||||
onChange={(e) => setFormData({ ...formData, stock_quantity: Number(e.target.value) })}
|
||||
className="bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Preço de Venda"
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
placeholder="0,00"
|
||||
value={formData.price}
|
||||
onChange={(e) => setFormData({ ...formData, price: Number(e.target.value) })}
|
||||
className="bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Preço de Custo"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0,00"
|
||||
value={formData.cost_price}
|
||||
onChange={(e) => setFormData({ ...formData, cost_price: Number(e.target.value) })}
|
||||
className="bg-zinc-50 dark:bg-zinc-800 border-zinc-200 dark:border-zinc-700"
|
||||
/>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-black text-zinc-400 uppercase tracking-widest mb-2">Descrição</label>
|
||||
<textarea
|
||||
className="w-full px-4 py-3 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-2xl focus:ring-2 focus:ring-brand-500/20 outline-none transition-all placeholder:text-zinc-400 text-sm h-32 resize-none"
|
||||
placeholder="Detalhes sobre o produto ou serviço..."
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 pt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className="px-6 py-3 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 font-bold transition-all"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-8 py-3 text-white rounded-2xl font-bold shadow-lg hover:opacity-90 transition-all flex items-center gap-2"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
<CheckIcon className="w-5 h-5" />
|
||||
Salvar Item
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={bulkConfirmOpen}
|
||||
onClose={() => setBulkConfirmOpen(false)}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="Excluir Produtos Selecionados"
|
||||
message={`Tem certeza que deseja excluir os ${selectedIds.length} produtos selecionados? Esta ação não pode ser desfeita.`}
|
||||
confirmText="Excluir Tudo"
|
||||
variant="danger"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={confirmOpen}
|
||||
onClose={() => {
|
||||
setConfirmOpen(false);
|
||||
setProductToDelete(null);
|
||||
}}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Excluir Item"
|
||||
message="Tem certeza que deseja excluir este produto ou serviço? Esta ação não pode ser desfeita."
|
||||
confirmText="Excluir"
|
||||
cancelText="Cancelar"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<BulkActionBar
|
||||
selectedCount={selectedIds.length}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
actions={[
|
||||
{
|
||||
label: "Excluir Selecionados",
|
||||
icon: <TrashIcon className="w-5 h-5" />,
|
||||
onClick: handleBulkDelete,
|
||||
variant: 'danger'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
front-end-agency/app/(agency)/erp/caixa/page.tsx
Normal file
12
front-end-agency/app/(agency)/erp/caixa/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import FinanceContent from '@/components/erp/FinanceContent';
|
||||
|
||||
export default function CaixaPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<FinanceContent />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
23
front-end-agency/app/(agency)/erp/configuracoes/page.tsx
Normal file
23
front-end-agency/app/(agency)/erp/configuracoes/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/outline";
|
||||
import { PageHeader } from "@/components/ui";
|
||||
|
||||
export default function ConfiguracoesPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
<PageHeader
|
||||
title="Configurações do ERP"
|
||||
description="Personalize as categorias financeiras, contas e parâmetros do sistema."
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center py-20 bg-white dark:bg-zinc-900 rounded-[32px] border border-zinc-200 dark:border-zinc-800">
|
||||
<AdjustmentsHorizontalIcon className="w-16 h-16 text-zinc-300 mb-4" />
|
||||
<h3 className="text-xl font-bold text-zinc-900 dark:text-white">Módulo em Desenvolvimento</h3>
|
||||
<p className="text-zinc-500 max-w-sm text-center mt-2">Em breve você poderá configurar suas categorias, contas bancárias e fluxos operacionais aqui.</p>
|
||||
</div>
|
||||
</div>
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
358
front-end-agency/app/(agency)/erp/entidades/page.tsx
Normal file
358
front-end-agency/app/(agency)/erp/entidades/page.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, Fragment } from 'react';
|
||||
import {
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
UserIcon,
|
||||
BriefcaseIcon,
|
||||
TrashIcon,
|
||||
PencilSquareIcon,
|
||||
ArrowRightIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { erpApi, Entity } from '@/lib/api-erp';
|
||||
import { useToast } from '@/components/layout/ToastContext';
|
||||
import {
|
||||
StatsCard,
|
||||
DataTable,
|
||||
Input,
|
||||
Card,
|
||||
CustomSelect,
|
||||
PageHeader,
|
||||
BulkActionBar,
|
||||
ConfirmDialog,
|
||||
} from "@/components/ui";
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface CRMCustomer {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
function EntidadesContent() {
|
||||
const toast = useToast();
|
||||
const [entities, setEntities] = useState<Entity[]>([]);
|
||||
const [crmCustomers, setCrmCustomers] = useState<CRMCustomer[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
|
||||
const [entityToDelete, setEntityToDelete] = useState<string | null>(null);
|
||||
const [editingEntity, setEditingEntity] = useState<Partial<Entity> | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<Entity>>({
|
||||
name: '',
|
||||
type: 'supplier',
|
||||
document: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllData();
|
||||
}, []);
|
||||
|
||||
const fetchAllData = async (silent = false) => {
|
||||
try {
|
||||
if (!silent) setLoading(true);
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
||||
const [erpData, crmResp] = await Promise.all([
|
||||
erpApi.getEntities(),
|
||||
fetch('/api/crm/customers', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
}).then(res => res.ok ? res.json() : [])
|
||||
]);
|
||||
|
||||
setEntities(erpData || []);
|
||||
setCrmCustomers(crmResp?.customers || crmResp || []);
|
||||
} catch (error) {
|
||||
toast.error('Erro ao carregar', 'Não foi possível carregar os dados financeiros');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
if (editingEntity?.id) {
|
||||
await erpApi.updateEntity(editingEntity.id, formData);
|
||||
toast.success('Cadastro atualizado!');
|
||||
} else {
|
||||
await erpApi.createEntity(formData);
|
||||
toast.success('Entidade cadastrada!');
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setEditingEntity(null);
|
||||
await fetchAllData();
|
||||
} catch (error) {
|
||||
toast.error('Erro ao salvar');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!entityToDelete) return;
|
||||
|
||||
const originalEntities = [...entities];
|
||||
const idToDelete = String(entityToDelete);
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setEntities(prev => prev.filter(e => String(e.id) !== idToDelete));
|
||||
|
||||
try {
|
||||
await erpApi.deleteEntity(idToDelete);
|
||||
toast.success('Exclusão completa', 'A entidade foi removida com sucesso.');
|
||||
setTimeout(() => fetchAllData(true), 500);
|
||||
} catch (error) {
|
||||
setEntities(originalEntities);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir a entidade.');
|
||||
} finally {
|
||||
setConfirmOpen(false);
|
||||
setEntityToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkDelete = async () => {
|
||||
if (selectedIds.length === 0) return;
|
||||
setBulkConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmBulkDelete = async () => {
|
||||
const erpIds = selectedIds.filter(id => {
|
||||
const item = combinedData.find(d => d.id === id);
|
||||
return item?.source === 'ERP';
|
||||
}).map(String);
|
||||
|
||||
if (erpIds.length === 0) return;
|
||||
|
||||
const originalEntities = [...entities];
|
||||
|
||||
// Dynamic: remove instantly
|
||||
setEntities(prev => prev.filter(e => !erpIds.includes(String(e.id))));
|
||||
|
||||
try {
|
||||
await Promise.all(erpIds.map(id => erpApi.deleteEntity(id)));
|
||||
toast.success('Exclusão completa', `${erpIds.length} entidades excluídas com sucesso.`);
|
||||
setTimeout(() => fetchAllData(true), 500);
|
||||
} catch (error) {
|
||||
setEntities(originalEntities);
|
||||
toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir algumas entidades.');
|
||||
} finally {
|
||||
setBulkConfirmOpen(false);
|
||||
setSelectedIds([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Combine both for searching
|
||||
const combinedData = [
|
||||
...crmCustomers.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
email: c.email,
|
||||
phone: c.phone,
|
||||
source: 'CRM' as const,
|
||||
type: 'Cliente (CRM)',
|
||||
original: c
|
||||
})),
|
||||
...entities.map(e => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
email: e.email,
|
||||
phone: e.phone,
|
||||
source: 'ERP' as const,
|
||||
type: e.type === 'customer' ? 'Cliente (ERP)' : (e.type === 'supplier' ? 'Fornecedor (ERP)' : 'Ambos'),
|
||||
original: e
|
||||
}))
|
||||
];
|
||||
|
||||
const filteredData = combinedData.filter(d =>
|
||||
d.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(d.email || '').toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading && combinedData.length === 0) return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto">
|
||||
<div className="text-center py-20 text-zinc-500">Carregando parceiros de negócio...</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
<PageHeader
|
||||
title="Parceiros de Negócio"
|
||||
description="Gerencie seus Clientes (CRM) e Fornecedores (ERP) em um único lugar."
|
||||
primaryAction={{
|
||||
label: "Novo Fornecedor",
|
||||
icon: <PlusIcon className="w-5 h-5" />,
|
||||
onClick: () => {
|
||||
setEditingEntity(null);
|
||||
setFormData({ name: '', type: 'supplier', document: '', email: '', phone: '', address: '' });
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Ir para CRM Clientes",
|
||||
icon: <UserIcon className="w-5 h-5" />,
|
||||
onClick: () => window.location.href = '/crm/clientes'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<StatsCard
|
||||
title="Clientes no CRM"
|
||||
value={crmCustomers.length}
|
||||
icon={<UserIcon className="w-6 h-6 text-emerald-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Fornecedores no ERP"
|
||||
value={entities.filter(e => e.type === 'supplier' || e.type === 'both').length}
|
||||
icon={<BriefcaseIcon className="w-6 h-6 text-purple-500" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card noPadding>
|
||||
<div className="p-4 border-b border-zinc-100 dark:border-zinc-800">
|
||||
<div className="max-w-md">
|
||||
<Input
|
||||
placeholder="Pesquisar por nome ou e-mail em toda a base..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
leftIcon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable
|
||||
selectable
|
||||
isLoading={loading}
|
||||
selectedIds={selectedIds}
|
||||
onSelectionChange={setSelectedIds}
|
||||
data={filteredData}
|
||||
columns={[
|
||||
{
|
||||
header: 'Nome / Razão Social',
|
||||
accessor: (row) => (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-zinc-900 dark:text-white">{row.name}</span>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-black uppercase ${row.source === 'CRM' ? 'bg-emerald-50 text-emerald-700' : 'bg-purple-50 text-purple-700'}`}>
|
||||
{row.source}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-400 font-medium">{row.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: 'E-mail',
|
||||
accessor: (row) => row.email || '-'
|
||||
},
|
||||
{
|
||||
header: 'Telefone',
|
||||
accessor: (row) => row.phone || '-'
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
className: 'text-right',
|
||||
accessor: (row) => (
|
||||
<div className="flex justify-end gap-2">
|
||||
{row.source === 'ERP' ? (
|
||||
<>
|
||||
<button onClick={(e) => { e.stopPropagation(); setEditingEntity(row.original as Entity); setFormData(row.original as Entity); setIsModalOpen(true); }} className="p-2 text-zinc-400 hover:text-brand-500">
|
||||
<PencilSquareIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button onClick={(e) => { e.stopPropagation(); setEntityToDelete(row.id as string); setConfirmOpen(true); }} className="p-2 text-zinc-400 hover:text-rose-500">
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Link href={`/crm/clientes?id=${row.id}`} onClick={(e) => e.stopPropagation()} className="p-2 text-zinc-400 hover:text-brand-500 flex items-center gap-1 text-xs font-bold">
|
||||
Ver no CRM <ArrowRightIcon className="w-4 h-4" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Modal de Cadastro ERP (Fornecedores) */}
|
||||
<Transition show={isModalOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={() => setIsModalOpen(false)}>
|
||||
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
</TransitionChild>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
|
||||
<DialogPanel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
|
||||
<DialogTitle className="text-xl font-bold mb-6">{editingEntity ? 'Editar Fornecedor' : 'Novo Fornecedor / Outros'}</DialogTitle>
|
||||
<form onSubmit={handleSave} className="space-y-4">
|
||||
<Input label="Nome / Razão Social" value={formData.name} onChange={e => setFormData({ ...formData, name: e.target.value })} required />
|
||||
<CustomSelect label="Tipo" options={[{ label: 'Fornecedor', value: 'supplier' }, { label: 'Cliente (ERP Avulso)', value: 'customer' }, { label: 'Ambos', value: 'both' }]} value={formData.type || 'supplier'} onChange={val => setFormData({ ...formData, type: val as any })} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input label="Documento (CNPJ/CPF)" value={formData.document} onChange={e => setFormData({ ...formData, document: e.target.value })} />
|
||||
<Input label="Telefone" value={formData.phone} onChange={e => setFormData({ ...formData, phone: e.target.value })} />
|
||||
</div>
|
||||
<Input label="E-mail" type="email" value={formData.email} onChange={e => setFormData({ ...formData, email: e.target.value })} />
|
||||
<Input label="Endereço Completo" value={formData.address} onChange={e => setFormData({ ...formData, address: e.target.value })} />
|
||||
|
||||
<div className="flex justify-end gap-3 mt-8 pt-4 border-t border-zinc-100 dark:border-zinc-800">
|
||||
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
|
||||
<button type="submit" className="px-8 py-2 text-white rounded-xl font-bold" style={{ background: 'var(--gradient)' }}>Salvar Cadastro</button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
|
||||
<ConfirmDialog isOpen={confirmOpen} onClose={() => setConfirmOpen(false)} onConfirm={handleConfirmDelete} title="Excluir Cadastro" message="Tem certeza? Isso pode afetar lançamentos vinculados a esta entidade no ERP." confirmText="Excluir" />
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={bulkConfirmOpen}
|
||||
onClose={() => setBulkConfirmOpen(false)}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
title="Excluir Itens Selecionados"
|
||||
message={`Tem certeza que deseja excluir as entidades selecionadas? Esta ação não pode ser desfeita.`}
|
||||
confirmText="Excluir Tudo"
|
||||
variant="danger"
|
||||
/>
|
||||
|
||||
<BulkActionBar
|
||||
selectedCount={selectedIds.length}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
actions={[
|
||||
{
|
||||
label: "Excluir Selecionados",
|
||||
icon: <TrashIcon className="w-5 h-5" />,
|
||||
onClick: handleBulkDelete,
|
||||
variant: 'danger'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EntidadesPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<EntidadesContent />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
14
front-end-agency/app/(agency)/erp/estoque/page.tsx
Normal file
14
front-end-agency/app/(agency)/erp/estoque/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import ProductsPage from '../ProductsPage';
|
||||
|
||||
export default function EstoquePage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<div className="p-6 max-w-[1600px] mx-auto">
|
||||
<ProductsPage />
|
||||
</div>
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
12
front-end-agency/app/(agency)/erp/pagar/page.tsx
Normal file
12
front-end-agency/app/(agency)/erp/pagar/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import FinanceContent from '@/components/erp/FinanceContent';
|
||||
|
||||
export default function ContasPagarPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<FinanceContent type="pagar" />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,315 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
AreaChart, Area, PieChart, Pie, Cell, ResponsiveContainer, CartesianGrid, XAxis, YAxis, Tooltip, Legend
|
||||
} from 'recharts';
|
||||
import {
|
||||
ArrowTrendingUpIcon,
|
||||
ArrowTrendingDownIcon,
|
||||
CubeIcon,
|
||||
CurrencyDollarIcon,
|
||||
CreditCardIcon,
|
||||
ClockIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { erpApi, FinancialTransaction, Order, FinancialCategory, Entity } from '@/lib/api-erp';
|
||||
import { formatCurrency } from '@/lib/format';
|
||||
import { PageHeader, StatsCard, Card } from "@/components/ui";
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
|
||||
const COLORS = ['#8b5cf6', '#ec4899', '#f43f5e', '#f59e0b', '#10b981', '#3b82f6'];
|
||||
|
||||
function ERPDashboardContent() {
|
||||
const [transactions, setTransactions] = useState<FinancialTransaction[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [categories, setCategories] = useState<FinancialCategory[]>([]);
|
||||
const [entities, setEntities] = useState<Entity[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [txData, orderData, categoriesData, entitiesData] = await Promise.all([
|
||||
erpApi.getTransactions(),
|
||||
erpApi.getOrders(),
|
||||
erpApi.getFinancialCategories(),
|
||||
erpApi.getEntities()
|
||||
]);
|
||||
setTransactions(Array.isArray(txData) ? txData : []);
|
||||
setOrders(Array.isArray(orderData) ? orderData : []);
|
||||
setCategories(Array.isArray(categoriesData) ? categoriesData : []);
|
||||
setEntities(Array.isArray(entitiesData) ? entitiesData : []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const paidTransactions = (transactions || []).filter(t => t.status === 'paid');
|
||||
|
||||
const totalIncome = paidTransactions
|
||||
.filter(t => t.type === 'income')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
const totalExpense = paidTransactions
|
||||
.filter(t => t.type === 'expense')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
const pendingIncome = (transactions || [])
|
||||
.filter(t => t.type === 'income' && t.status === 'pending')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
const pendingExpense = (transactions || [])
|
||||
.filter(t => t.type === 'expense' && t.status === 'pending')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
const balance = totalIncome - totalExpense;
|
||||
|
||||
// Process chart data (Income vs Expense by Month)
|
||||
const getChartData = () => {
|
||||
const months = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const data = months.map((month, index) => {
|
||||
const monthTransactions = paidTransactions.filter(t => {
|
||||
const date = new Date(t.payment_date || t.due_date || '');
|
||||
return date.getMonth() === index && date.getFullYear() === currentYear;
|
||||
});
|
||||
|
||||
const income = monthTransactions
|
||||
.filter(t => t.type === 'income')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
const expense = monthTransactions
|
||||
.filter(t => t.type === 'expense')
|
||||
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
|
||||
|
||||
return { name: month, income, expense };
|
||||
});
|
||||
|
||||
const currentMonthIndex = new Date().getMonth();
|
||||
// Mostrar pelo menos os últimos 6 meses ou o ano todo se for o caso
|
||||
return data.slice(Math.max(0, currentMonthIndex - 5), currentMonthIndex + 1);
|
||||
};
|
||||
|
||||
// Process category data (Expenses by Category)
|
||||
const getCategoryData = () => {
|
||||
const expenseTransactions = paidTransactions.filter(t => t.type === 'expense');
|
||||
const breakdown: Record<string, number> = {};
|
||||
|
||||
expenseTransactions.forEach(t => {
|
||||
const category = categories.find(c => c.id === t.category_id)?.name || 'Outros';
|
||||
breakdown[category] = (breakdown[category] || 0) + Number(t.amount || 0);
|
||||
});
|
||||
|
||||
return Object.entries(breakdown)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 6);
|
||||
};
|
||||
|
||||
const chartData = getChartData();
|
||||
const categoryData = getCategoryData();
|
||||
|
||||
if (loading) return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto">
|
||||
<div className="flex items-center justify-center h-[600px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
<PageHeader
|
||||
title="Dashboard ERP"
|
||||
description="Visão geral financeira e operacional em tempo real"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Receitas pagas"
|
||||
value={formatCurrency(totalIncome)}
|
||||
icon={<ArrowTrendingUpIcon className="w-6 h-6 text-emerald-500" />}
|
||||
trend={{ value: formatCurrency(pendingIncome), label: 'pendente', type: 'up' }}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Despesas pagas"
|
||||
value={formatCurrency(totalExpense)}
|
||||
icon={<ArrowTrendingDownIcon className="w-6 h-6 text-rose-500" />}
|
||||
trend={{ value: formatCurrency(pendingExpense), label: 'pendente', type: 'down' }}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Saldo em Caixa"
|
||||
value={formatCurrency(balance)}
|
||||
icon={<CurrencyDollarIcon className="w-6 h-6 text-brand-500" />}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Pedidos (Mês)"
|
||||
value={(orders?.length || 0).toString()}
|
||||
icon={<CubeIcon className="w-6 h-6 text-purple-500" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<Card title="Evolução Financeira" description="Diferença entre entradas e saídas pagas nos últimos meses.">
|
||||
<div className="h-[350px] w-full mt-4">
|
||||
{chartData.some(d => d.income > 0 || d.expense > 0) ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="colorIncome" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.1} />
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorExpense" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.1} />
|
||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#88888820" />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#888' }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#888' }} tickFormatter={(val) => `R$${val}`} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)'
|
||||
}}
|
||||
formatter={(value: any) => formatCurrency(value || 0)}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36} />
|
||||
<Area
|
||||
name="Receitas"
|
||||
type="monotone"
|
||||
dataKey="income"
|
||||
stroke="#10b981"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorIncome)"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<Area
|
||||
name="Despesas"
|
||||
type="monotone"
|
||||
dataKey="expense"
|
||||
stroke="#ef4444"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorExpense)"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-zinc-400 text-sm italic">
|
||||
Ainda não há dados financeiros suficientes para exibir o gráfico.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
<Card title="Despesas por Categoria" description="Distribuição dos gastos pagos.">
|
||||
<div className="h-[350px] w-full mt-4">
|
||||
{categoryData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={categoryData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={5}
|
||||
dataKey="value"
|
||||
>
|
||||
{categoryData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
borderRadius: '16px',
|
||||
border: 'none',
|
||||
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
|
||||
}}
|
||||
formatter={(value: any) => formatCurrency(value || 0)}
|
||||
/>
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-zinc-400 text-sm italic">
|
||||
Ainda não há despesas pagas registradas.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3">
|
||||
<Card title="Transações Recentes" description="Últimos lançamentos financeiros registrados no sistema.">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-100 dark:border-zinc-800">
|
||||
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Descrição</th>
|
||||
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Categoria</th>
|
||||
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Data</th>
|
||||
<th className="py-4 font-semibold text-zinc-900 dark:text-white text-right">Valor</th>
|
||||
<th className="py-4 font-semibold text-zinc-900 dark:text-white text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-50 dark:divide-zinc-900">
|
||||
{transactions.slice(0, 5).map((t) => (
|
||||
<tr key={t.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors">
|
||||
<td className="py-4 text-zinc-600 dark:text-zinc-400">{t.description}</td>
|
||||
<td className="py-4 text-zinc-600 dark:text-zinc-400">
|
||||
{categories.find(c => c.id === t.category_id)?.name || 'Outros'}
|
||||
</td>
|
||||
<td className="py-4 text-zinc-600 dark:text-zinc-400">
|
||||
{new Date(t.payment_date || t.due_date || '').toLocaleDateString('pt-BR')}
|
||||
</td>
|
||||
<td className={`py-4 text-right font-medium ${t.type === 'income' ? 'text-emerald-600' : 'text-rose-600'}`}>
|
||||
{t.type === 'income' ? '+' : '-'} {formatCurrency(t.amount)}
|
||||
</td>
|
||||
<td className="py-4 text-right">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${t.status === 'paid' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400' :
|
||||
t.status === 'pending' ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400' :
|
||||
'bg-zinc-100 text-zinc-700 dark:bg-zinc-900/20 dark:text-zinc-400'
|
||||
}`}>
|
||||
{t.status === 'paid' ? 'Pago' : t.status === 'pending' ? 'Pendente' : 'Cancelado'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{transactions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-zinc-400 italic">
|
||||
Nenhuma transação encontrada.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ERPPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">ERP</h1>
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-8 text-center">
|
||||
<p className="text-gray-500">Sistema Integrado de Gestão Empresarial em breve</p>
|
||||
</div>
|
||||
</div>
|
||||
<ERPDashboardContent />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
14
front-end-agency/app/(agency)/erp/pedidos/page.tsx
Normal file
14
front-end-agency/app/(agency)/erp/pedidos/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import OrdersPage from '../OrdersPage';
|
||||
|
||||
export default function PedidosPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<div className="p-6 max-w-[1600px] mx-auto">
|
||||
<OrdersPage />
|
||||
</div>
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
12
front-end-agency/app/(agency)/erp/receber/page.tsx
Normal file
12
front-end-agency/app/(agency)/erp/receber/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { SolutionGuard } from '@/components/auth/SolutionGuard';
|
||||
import FinanceContent from '@/components/erp/FinanceContent';
|
||||
|
||||
export default function ContasReceberPage() {
|
||||
return (
|
||||
<SolutionGuard requiredSolution="erp">
|
||||
<FinanceContent type="receber" />
|
||||
</SolutionGuard>
|
||||
);
|
||||
}
|
||||
198
front-end-agency/app/(agency)/teste/page.tsx
Normal file
198
front-end-agency/app/(agency)/teste/page.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
MagnifyingGlassIcon,
|
||||
PlusIcon,
|
||||
FunnelIcon,
|
||||
ArrowPathIcon,
|
||||
EllipsisVerticalIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Button, Input, Select, PageHeader, Card, StatsCard, Tabs, DatePicker, CustomSelect } from "@/components/ui";
|
||||
import {
|
||||
UsersIcon,
|
||||
CurrencyDollarIcon,
|
||||
BriefcaseIcon as BriefcaseSolidIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
TableCellsIcon,
|
||||
ChartPieIcon,
|
||||
Cog6ToothIcon as CogIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
export default function TestPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [dateRange, setDateRange] = useState<{ start: Date | null; end: Date | null }>({ start: null, end: null });
|
||||
const [status, setStatus] = useState('all');
|
||||
|
||||
// Dados fictícios para a lista
|
||||
const items = [
|
||||
{ id: 1, name: 'Projeto Alpha', client: 'Empresa A', date: '2023-10-01', status: 'Ativo', amount: 'R$ 1.500,00' },
|
||||
{ id: 2, name: 'Serviço Beta', client: 'Empresa B', date: '2023-10-05', status: 'Pendente', amount: 'R$ 2.300,00' },
|
||||
{ id: 3, name: 'Consultoria Gamma', client: 'Empresa C', date: '2023-10-10', status: 'Concluído', amount: 'R$ 800,00' },
|
||||
{ id: 4, name: 'Design Delta', client: 'Empresa D', date: '2023-10-12', status: 'Ativo', amount: 'R$ 4.200,00' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
||||
<PageHeader
|
||||
title="Página de Teste"
|
||||
description="Área de desenvolvimento e homologação de novos componentes do padrão Aggios."
|
||||
primaryAction={{
|
||||
label: "Novo Item",
|
||||
icon: <PlusIcon className="w-4 h-4" />,
|
||||
onClick: () => console.log('Novo Item')
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatsCard
|
||||
title="Total de Clientes"
|
||||
value="1.240"
|
||||
icon={<UsersIcon className="w-6 h-6" />}
|
||||
trend={{ value: '12%', label: 'vs mês passado', type: 'up' }}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Receita Mensal"
|
||||
value="R$ 45.200"
|
||||
icon={<CurrencyDollarIcon className="w-6 h-6" />}
|
||||
trend={{ value: '8.4%', label: 'vs mês passado', type: 'up' }}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Projetos Ativos"
|
||||
value="42"
|
||||
icon={<BriefcaseSolidIcon className="w-6 h-6" />}
|
||||
trend={{ value: '2', label: 'novos esta semana', type: 'neutral' }}
|
||||
/>
|
||||
<StatsCard
|
||||
title="Taxa de Conversão"
|
||||
value="18.5%"
|
||||
icon={<ArrowTrendingUpIcon className="w-6 h-6" />}
|
||||
trend={{ value: '2.1%', label: 'vs mês passado', type: 'down' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters Area: Clean Visual (Solid contrast) */}
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="flex-1 w-full">
|
||||
<Input
|
||||
placeholder="Pesquisar registros..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
leftIcon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
||||
className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-80">
|
||||
<DatePicker
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
buttonClassName="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-56">
|
||||
<CustomSelect
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
options={[
|
||||
{ label: 'Todos os Status', value: 'all' },
|
||||
{ label: 'Ativo', value: 'active', color: 'bg-emerald-500' },
|
||||
{ label: 'Pendente', value: 'pending', color: 'bg-amber-500' },
|
||||
{ label: 'Concluído', value: 'done', color: 'bg-blue-500' },
|
||||
]}
|
||||
buttonClassName="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 hover:border-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Tabs */}
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
label: 'Visão Geral',
|
||||
icon: <TableCellsIcon />,
|
||||
content: (
|
||||
<Card noPadding title="Itens Recentes" description="Lista de últimos itens cadastrados no sistema.">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800 text-left">
|
||||
<th className="px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Item</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Cliente</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Data</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Valor</th>
|
||||
<th className="px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider text-right">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{items.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors group">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-medium text-zinc-900 dark:text-white">{item.name}</div>
|
||||
<div className="text-xs text-zinc-500">ID: #{item.id}</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-zinc-600 dark:text-zinc-300">{item.client}</td>
|
||||
<td className="px-6 py-4 text-sm text-zinc-600 dark:text-zinc-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<CalendarIcon className="w-4 h-4 text-zinc-400" />
|
||||
{new Date(item.date).toLocaleDateString('pt-BR')}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm font-semibold text-zinc-900 dark:text-white">{item.amount}</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-400 hover:text-zinc-600 transition-colors">
|
||||
<EllipsisVerticalIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-zinc-50/30 dark:bg-zinc-900/30 border-t border-zinc-200 dark:border-zinc-800 flex items-center justify-between">
|
||||
<span className="text-xs text-zinc-500 italic">Exibindo {items.length} resultados encontrados.</span>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">Anterior</Button>
|
||||
<Button variant="outline" size="sm">Próximo</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Relatórios',
|
||||
icon: <ChartPieIcon />,
|
||||
content: (
|
||||
<Card title="Analytics" description="Visualize o desempenho dos seus itens em tempo real.">
|
||||
<div className="flex items-center justify-center h-48 border-2 border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl">
|
||||
<p className="text-zinc-400 text-sm font-medium">Gráficos e métricas detalhadas serão exibidos aqui.</p>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: 'Configurações',
|
||||
icon: <CogIcon />,
|
||||
content: (
|
||||
<Card title="Preferências" description="Ajuste as configurações deste módulo de teste.">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl">
|
||||
<div>
|
||||
<p className="text-sm font-bold text-zinc-900 dark:text-white">Notificações por E-mail</p>
|
||||
<p className="text-xs text-zinc-500">Receba alertas automáticos sobre novos itens.</p>
|
||||
</div>
|
||||
<div className="w-10 h-6 bg-brand-500 rounded-full relative">
|
||||
<div className="absolute right-1 top-1 w-4 h-4 bg-white rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ path
|
||||
try {
|
||||
const response = await fetch(`http://backend:8080/api/${path}${req.nextUrl.search}`, {
|
||||
method: "GET",
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
"Authorization": token || "",
|
||||
"Content-Type": "application/json",
|
||||
@@ -77,4 +78,33 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ pat
|
||||
console.error("API proxy error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: pathArray } = await params;
|
||||
const path = pathArray?.join("/") || "";
|
||||
const token = req.headers.get("authorization");
|
||||
const host = req.headers.get("host");
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://backend:8080/api/${path}${req.nextUrl.search}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Authorization": token || "",
|
||||
"Content-Type": "application/json",
|
||||
"X-Forwarded-Host": host || "",
|
||||
"X-Original-Host": host || "",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error("API proxy error:", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export async function GET(
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/api/crm/customers/${id}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
'X-Tenant-Subdomain': subdomain,
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: NextRequest) {
|
||||
console.log('[API Route] GET /api/crm/customers - subdomain:', subdomain);
|
||||
|
||||
const response = await fetch(`${API_URL}/api/crm/customers`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
'Authorization': token,
|
||||
'X-Tenant-Subdomain': subdomain,
|
||||
|
||||
BIN
front-end-agency/build_output.txt
Normal file
BIN
front-end-agency/build_output.txt
Normal file
Binary file not shown.
199
front-end-agency/components/documentos/DocumentEditor.tsx
Normal file
199
front-end-agency/components/documentos/DocumentEditor.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Document, docApi } from '@/lib/api-docs';
|
||||
import {
|
||||
XMarkIcon,
|
||||
CheckIcon,
|
||||
ArrowLeftIcon,
|
||||
Bars3BottomLeftIcon,
|
||||
HashtagIcon,
|
||||
CloudArrowUpIcon,
|
||||
SparklesIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import NotionEditor from './NotionEditor';
|
||||
import DocumentSidebar from './DocumentSidebar';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface DocumentEditorProps {
|
||||
initialDocument: Partial<Document> | null;
|
||||
onSave: (doc: Partial<Document>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function DocumentEditor({ initialDocument, onSave, onCancel }: DocumentEditorProps) {
|
||||
const [document, setDocument] = useState<Partial<Document> | null>(initialDocument);
|
||||
const [title, setTitle] = useState(initialDocument?.title || '');
|
||||
const [content, setContent] = useState(initialDocument?.content || '[]');
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Refs para controle fino de salvamento
|
||||
const saveTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastSaved = useRef({ title: initialDocument?.title || '', content: initialDocument?.content || '[]' });
|
||||
|
||||
useEffect(() => {
|
||||
if (initialDocument) {
|
||||
setDocument(initialDocument);
|
||||
setTitle(initialDocument.title || '');
|
||||
setContent(initialDocument.content || '[]');
|
||||
lastSaved.current = {
|
||||
title: initialDocument.title || '',
|
||||
content: initialDocument.content || '[]'
|
||||
};
|
||||
}
|
||||
}, [initialDocument]);
|
||||
|
||||
// Função de Auto-Save Robusta
|
||||
const autoSave = useCallback(async (newTitle: string, newContent: string) => {
|
||||
if (!document?.id) return;
|
||||
|
||||
setSaving(true);
|
||||
console.log('💾 Inactivity detected. Saving document...', document.id);
|
||||
|
||||
try {
|
||||
await docApi.updateDocument(document.id, {
|
||||
title: newTitle,
|
||||
content: newContent,
|
||||
status: 'published'
|
||||
});
|
||||
// Atualiza o ref do último salvo para evitar loop
|
||||
lastSaved.current = { title: newTitle, content: newContent };
|
||||
console.log('✅ Document saved successfully');
|
||||
} catch (e) {
|
||||
console.error('❌ Auto-save failed', e);
|
||||
toast.error('Erro ao salvar automaticamente');
|
||||
} finally {
|
||||
// Delay visual para o feedback de "Salvo"
|
||||
setTimeout(() => setSaving(false), 800);
|
||||
}
|
||||
}, [document?.id]);
|
||||
|
||||
// Trigger de auto-save com debounce de 1 segundo
|
||||
useEffect(() => {
|
||||
if (!document?.id) return;
|
||||
|
||||
// Verifica se houve mudança real em relação ao último salvo
|
||||
if (title === lastSaved.current.title && content === lastSaved.current.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
||||
|
||||
saveTimeout.current = setTimeout(() => {
|
||||
autoSave(title, content);
|
||||
}, 1000); // Salva após 1 segundo de inatividade
|
||||
|
||||
return () => {
|
||||
if (saveTimeout.current) clearTimeout(saveTimeout.current);
|
||||
};
|
||||
}, [title, content, document?.id, autoSave]);
|
||||
|
||||
const navigateToDoc = async (doc: Document) => {
|
||||
// Antes de navegar, salva o atual se necessário
|
||||
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
|
||||
await autoSave(title, content);
|
||||
}
|
||||
|
||||
setDocument(doc);
|
||||
setTitle(doc.title);
|
||||
setContent(doc.content);
|
||||
lastSaved.current = { title: doc.title, content: doc.content };
|
||||
toast.success(`Abrindo: ${doc.title || 'Untitled'}`, { duration: 1000, position: 'bottom-center' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] bg-white dark:bg-zinc-950 flex flex-col">
|
||||
{/* Header Clean */}
|
||||
<header className="h-16 border-b border-zinc-200 dark:border-zinc-800 px-6 flex items-center justify-between bg-white dark:bg-zinc-950 z-20 shrink-0">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (title !== lastSaved.current.title || content !== lastSaved.current.content) {
|
||||
await autoSave(title, content);
|
||||
}
|
||||
onCancel();
|
||||
}}
|
||||
className="p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white rounded-xl hover:bg-zinc-100 dark:hover:bg-zinc-900 transition-all"
|
||||
title="Back to list"
|
||||
>
|
||||
<ArrowLeftIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowSidebar(!showSidebar)}
|
||||
className={`p-2 rounded-xl transition-all ${showSidebar ? 'text-brand-500 bg-brand-50/50 dark:bg-brand-500/10' : 'text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-900'}`}
|
||||
title={showSidebar ? "Hide Navigation" : "Show Navigation"}
|
||||
>
|
||||
<Bars3BottomLeftIcon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="h-4 w-px bg-zinc-200 dark:bg-zinc-800 mx-2" />
|
||||
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<HashtagIcon className="w-4 h-4 text-zinc-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Untitled Document"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="text-lg font-bold bg-transparent border-none outline-none text-zinc-900 dark:text-white placeholder:text-zinc-300 dark:placeholder:text-zinc-600 w-full max-w-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-zinc-50 dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800">
|
||||
{saving ? (
|
||||
<div className="flex items-center gap-2 text-brand-500">
|
||||
<div className="w-1.5 h-1.5 bg-brand-500 rounded-full animate-pulse" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest leading-none">Saving...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-emerald-500">
|
||||
<CheckIcon className="w-3.5 h-3.5" />
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-500 dark:text-zinc-400 leading-none">Sync Saved</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Lateral Sidebar (Navegação) */}
|
||||
{showSidebar && document?.id && (
|
||||
<DocumentSidebar
|
||||
key={document.id} // Re-render sidebar on doc change to ensure fresh data
|
||||
documentId={document.id}
|
||||
onNavigate={navigateToDoc}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Área do Editor */}
|
||||
<main className="flex-1 overflow-y-auto bg-white dark:bg-zinc-950 flex flex-col items-center custom-scrollbar">
|
||||
<div className="w-full max-w-[850px] px-12 md:px-24 py-16 animate-in slide-in-from-bottom-2 duration-500">
|
||||
{/* Title Hero Display */}
|
||||
<div className="mb-14 group">
|
||||
<h1 className="text-5xl font-black text-zinc-900 dark:text-white tracking-tighter leading-tight">
|
||||
{title || 'Untitled'}
|
||||
</h1>
|
||||
<div className="mt-6 flex items-center gap-4 text-zinc-400">
|
||||
<SparklesIcon className="w-4 h-4 text-brand-500" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 rounded-full bg-brand-500 flex items-center justify-center text-[10px] font-bold text-white uppercase">Me</div>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Editing Mode • Real-time Sync</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotionEditor
|
||||
documentId={document?.id}
|
||||
initialContent={content}
|
||||
onChange={setContent}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
86
front-end-agency/components/documentos/DocumentList.tsx
Normal file
86
front-end-agency/components/documentos/DocumentList.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Document } from '@/lib/api-docs';
|
||||
import {
|
||||
DocumentTextIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
CalendarIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { Card } from "@/components/ui";
|
||||
|
||||
interface DocumentListProps {
|
||||
documents: Document[];
|
||||
onEdit: (doc: Document) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function DocumentList({ documents, onEdit, onDelete }: DocumentListProps) {
|
||||
if (documents.length === 0) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-12 text-center">
|
||||
<DocumentTextIcon className="w-12 h-12 text-zinc-300 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-bold text-zinc-900 dark:text-white">Nenhum documento ainda</h3>
|
||||
<p className="text-zinc-500 max-w-xs mx-auto mt-2">
|
||||
Comece criando seu primeiro documento de texto para sua agência.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{documents.map((doc) => (
|
||||
<Card
|
||||
key={doc.id}
|
||||
className="group hover:shadow-xl transition-all border-2 border-transparent hover:border-brand-500/20"
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="p-3 bg-brand-50 dark:bg-brand-500/10 rounded-xl">
|
||||
<DocumentTextIcon className="w-6 h-6 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<button
|
||||
onClick={() => onEdit(doc)}
|
||||
className="p-2 text-zinc-400 hover:text-brand-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
|
||||
>
|
||||
<PencilSquareIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(doc.id)}
|
||||
className="p-2 text-zinc-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-lg transition-colors"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-base font-bold text-zinc-900 dark:text-white mb-2 line-clamp-1">
|
||||
{doc.title || 'Documento sem título'}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-zinc-500 line-clamp-3 mb-6 flex-1">
|
||||
{doc.content ? doc.content.replace(/<[^>]*>/g, '').substring(0, 150) : 'Sem conteúdo...'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 pt-4 border-t border-zinc-100 dark:border-zinc-800">
|
||||
<CalendarIcon className="w-3.5 h-3.5 text-zinc-400" />
|
||||
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">
|
||||
{format(parseISO(doc.updated_at), "dd 'de' MMMM", { locale: ptBR })}
|
||||
</span>
|
||||
{doc.status === 'draft' && (
|
||||
<span className="ml-auto text-[10px] font-black uppercase tracking-widest text-amber-500 bg-amber-50 dark:bg-amber-500/10 px-2 py-0.5 rounded-full">
|
||||
Rascunho
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
225
front-end-agency/components/documentos/DocumentSidebar.tsx
Normal file
225
front-end-agency/components/documentos/DocumentSidebar.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Document, DocumentActivity, docApi } from '@/lib/api-docs';
|
||||
import {
|
||||
ClockIcon,
|
||||
DocumentIcon,
|
||||
PlusIcon,
|
||||
MagnifyingGlassIcon,
|
||||
ChevronRightIcon,
|
||||
HomeIcon,
|
||||
ArrowUpIcon,
|
||||
FolderIcon,
|
||||
FolderOpenIcon
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface DocumentSidebarProps {
|
||||
documentId: string;
|
||||
onNavigate: (doc: Document) => void;
|
||||
}
|
||||
|
||||
export default function DocumentSidebar({ documentId, onNavigate }: DocumentSidebarProps) {
|
||||
const [currentDoc, setCurrentDoc] = useState<Document | null>(null);
|
||||
const [children, setChildren] = useState<Document[]>([]);
|
||||
const [parentDoc, setParentDoc] = useState<Document | null>(null);
|
||||
const [rootPages, setRootPages] = useState<Document[]>([]);
|
||||
const [activities, setActivities] = useState<DocumentActivity[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'subpages' | 'activities'>('subpages');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentId) {
|
||||
fetchData();
|
||||
}
|
||||
}, [documentId]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
console.log('🔍 Sidebar fetching data for:', documentId);
|
||||
|
||||
const fetchSafely = async (promise: Promise<any>) => {
|
||||
try { return await promise; }
|
||||
catch (e) { console.warn('Sidebar part failed:', e); return null; }
|
||||
};
|
||||
|
||||
try {
|
||||
const [doc, roots, logs, subpages] = await Promise.all([
|
||||
docApi.getDocument(documentId),
|
||||
fetchSafely(docApi.getDocuments()),
|
||||
fetchSafely(docApi.getActivities(documentId)),
|
||||
fetchSafely(docApi.getSubpages(documentId))
|
||||
]);
|
||||
|
||||
if (doc) {
|
||||
setCurrentDoc(doc);
|
||||
setChildren(subpages || []);
|
||||
if (doc.parent_id) {
|
||||
const parent = await fetchSafely(docApi.getDocument(doc.parent_id));
|
||||
setParentDoc(parent);
|
||||
} else {
|
||||
setParentDoc(null);
|
||||
}
|
||||
}
|
||||
|
||||
setRootPages(roots || []);
|
||||
setActivities(logs || []);
|
||||
} catch (e) {
|
||||
console.error('❌ Sidebar critical error:', e);
|
||||
toast.error('Erro ao carregar estrutura');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateSubpage = async () => {
|
||||
try {
|
||||
const newDoc = await docApi.createDocument({
|
||||
title: 'Nova Subpágina',
|
||||
parent_id: documentId as any,
|
||||
content: '{"type":"doc","content":[{"type":"paragraph"}]}',
|
||||
status: 'published'
|
||||
});
|
||||
toast.success('Página criada!');
|
||||
// Refresh local para aparecer imediatamente
|
||||
setChildren(prev => [...prev, newDoc]);
|
||||
onNavigate(newDoc);
|
||||
} catch (e) {
|
||||
toast.error('Erro ao criar subpágina');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredChildren = children.filter(p => p.title.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
const rootWikis = rootPages.filter(p => !p.parent_id);
|
||||
|
||||
return (
|
||||
<div className="w-72 border-r border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 flex flex-col h-full overflow-hidden shrink-0">
|
||||
{/* Tabs de Navegação */}
|
||||
<div className="flex border-b border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950">
|
||||
<button
|
||||
onClick={() => setActiveTab('subpages')}
|
||||
className={`flex-1 px-4 py-4 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'subpages' ? 'text-brand-500 bg-brand-50/10 border-b-2 border-brand-500' : 'text-zinc-500'}`}
|
||||
>
|
||||
Estrutura
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('activities')}
|
||||
className={`flex-1 px-4 py-4 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab === 'activities' ? 'text-brand-500 bg-brand-50/10 border-b-2 border-brand-500' : 'text-zinc-500'}`}
|
||||
>
|
||||
Histórico
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-6">
|
||||
{activeTab === 'subpages' ? (
|
||||
<>
|
||||
{/* Status do Nível Atual */}
|
||||
<div className="space-y-3">
|
||||
{parentDoc && (
|
||||
<button
|
||||
onClick={() => onNavigate(parentDoc)}
|
||||
className="w-full flex items-center gap-2 p-2 px-3 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-900 text-zinc-400 hover:text-brand-500 transition-all group border border-dashed border-zinc-200 dark:border-zinc-800"
|
||||
>
|
||||
<ArrowUpIcon className="w-3 h-3 transition-transform group-hover:-translate-y-0.5" />
|
||||
<span className="text-[9px] font-black uppercase tracking-widest truncate">Pai: {parentDoc.title}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="p-3 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 shadow-sm flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-brand-50 dark:bg-brand-500/10 flex items-center justify-center text-brand-500">
|
||||
<FolderOpenIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-tighter">Você está em</p>
|
||||
<p className="text-xs font-bold text-zinc-900 dark:text-white truncate">{currentDoc?.title || 'Sem título'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<MagnifyingGlassIcon className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 transition-colors ${loading ? 'text-brand-500 animate-pulse' : 'text-zinc-400'}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filtrar subpáginas..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl pl-10 pr-4 py-2 text-xs outline-none focus:ring-2 ring-brand-500/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subpages (Children) */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between px-2 mb-2">
|
||||
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Subpáginas ({children.length})</p>
|
||||
<button
|
||||
onClick={handleCreateSubpage}
|
||||
className="p-1 text-zinc-400 hover:text-brand-500 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md shadow-sm transition-all"
|
||||
>
|
||||
<PlusIcon className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filteredChildren.length === 0 && (
|
||||
<div className="text-center py-6 border border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl">
|
||||
<p className="text-[10px] text-zinc-400 uppercase font-black">Nenhuma subpágina</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredChildren.map(page => (
|
||||
<button
|
||||
key={page.id}
|
||||
onClick={() => onNavigate(page)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all group border border-transparent hover:border-zinc-100 dark:hover:border-zinc-800 ${documentId === page.id ? 'bg-brand-500 text-white shadow-lg' : 'hover:bg-white dark:hover:bg-zinc-900 text-zinc-600 dark:text-zinc-400'}`}
|
||||
>
|
||||
<DocumentIcon className={`w-4 h-4 ${documentId === page.id ? 'text-white' : 'text-zinc-300 group-hover:text-brand-500'}`} />
|
||||
<span className="text-xs font-bold truncate flex-1 text-left">{page.title || 'Subpágina'}</span>
|
||||
<ChevronRightIcon className={`w-3 h-3 opacity-0 group-hover:opacity-100 ${documentId === page.id ? 'text-white' : ''}`} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Root wikis */}
|
||||
{!searchTerm && (
|
||||
<div className="pt-4 border-t border-zinc-100 dark:border-zinc-800 space-y-1">
|
||||
<p className="px-3 text-[10px] font-black text-zinc-400 uppercase tracking-widest mb-3">Todas as Wikis</p>
|
||||
{rootWikis.map(page => (
|
||||
<button
|
||||
key={page.id}
|
||||
onClick={() => onNavigate(page)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-xl transition-all group ${documentId === page.id ? 'text-brand-500 font-bold' : 'text-zinc-500 hover:bg-white dark:hover:bg-zinc-900'}`}
|
||||
>
|
||||
<HomeIcon className="w-3.5 h-3.5" />
|
||||
<span className="text-[11px] font-semibold truncate flex-1 text-left">{page.title || 'Início'}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{activities.map(log => (
|
||||
<div key={log.id} className="flex gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center text-[10px] font-black text-zinc-400 shrink-0">
|
||||
{log.user_name?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0 pt-0.5">
|
||||
<p className="text-[11px] text-zinc-900 dark:text-zinc-100 leading-tight">
|
||||
<span className="font-bold">{log.user_name}</span> {log.description}
|
||||
</p>
|
||||
<span className="text-[9px] text-zinc-400 uppercase font-black block mt-1">
|
||||
{format(parseISO(log.created_at), "dd MMM, HH:mm", { locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
233
front-end-agency/components/documentos/NotionEditor.tsx
Normal file
233
front-end-agency/components/documentos/NotionEditor.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import { useEditor, EditorContent, BubbleMenu, FloatingMenu } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import TaskList from '@tiptap/extension-task-list';
|
||||
import TaskItem from '@tiptap/extension-task-item';
|
||||
import Heading from '@tiptap/extension-heading';
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
ListBulletIcon,
|
||||
HashtagIcon,
|
||||
CodeBracketIcon,
|
||||
PlusIcon,
|
||||
Bars2Icon
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
interface NotionEditorProps {
|
||||
initialContent: string;
|
||||
onChange: (jsonContent: string) => void;
|
||||
documentId?: string;
|
||||
}
|
||||
|
||||
export default function NotionEditor({ initialContent, onChange, documentId }: NotionEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
heading: false,
|
||||
}),
|
||||
Heading.configure({
|
||||
levels: [1, 2, 3],
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: "Comece a digitar ou aperte '/' para comandos...",
|
||||
emptyEditorClass: 'is-editor-empty',
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
}),
|
||||
],
|
||||
content: '',
|
||||
onUpdate: ({ editor }) => {
|
||||
const json = editor.getJSON();
|
||||
onChange(JSON.stringify(json));
|
||||
},
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-zinc dark:prose-invert max-w-none focus:outline-none min-h-[600px] py-10 px-2 editor-content',
|
||||
},
|
||||
},
|
||||
}, [documentId]);
|
||||
|
||||
// Sincronizar apenas na primeira carga ou troca de documento
|
||||
useEffect(() => {
|
||||
if (!editor || !initialContent) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(initialContent);
|
||||
// Evita resetar se o editor já tem conteúdo (para não quebrar a digitação)
|
||||
if (editor.isEmpty && initialContent !== '{"type":"doc","content":[{"type":"paragraph"}]}') {
|
||||
editor.commands.setContent(parsed, false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (editor.isEmpty && initialContent !== '[]') {
|
||||
editor.commands.setContent(initialContent, false);
|
||||
}
|
||||
}
|
||||
}, [editor, documentId]); // Só roda quando o editor é criado ou o documento muda
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full relative min-h-[600px] group/editor selection:bg-brand-100 dark:selection:bg-brand-500/30">
|
||||
{/* Bubble Menu Customizado */}
|
||||
<BubbleMenu editor={editor} tippyOptions={{ duration: 150 }} className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl p-1 gap-0.5 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
active={editor.isActive('bold')}
|
||||
icon={<BoldIcon className="w-4 h-4" />}
|
||||
/>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
active={editor.isActive('italic')}
|
||||
icon={<ItalicIcon className="w-4 h-4" />}
|
||||
/>
|
||||
<div className="w-px h-4 bg-zinc-200 dark:bg-zinc-800 mx-1 self-center" />
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
active={editor.isActive('heading', { level: 1 })}
|
||||
label="H1"
|
||||
/>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
active={editor.isActive('heading', { level: 2 })}
|
||||
label="H2"
|
||||
/>
|
||||
<MenuButton
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
active={editor.isActive('bulletList')}
|
||||
icon={<ListBulletIcon className="w-4 h-4" />}
|
||||
/>
|
||||
</BubbleMenu>
|
||||
|
||||
{/* Menu Flutuante Estilo Notion */}
|
||||
<FloatingMenu editor={editor} tippyOptions={{ duration: 150 }} className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-2xl p-2 gap-1 overflow-hidden min-w-[240px] flex-col animate-in slide-in-from-left-4 duration-300">
|
||||
<p className="px-3 py-1 text-[10px] font-black uppercase tracking-widest text-zinc-400">Blocos Básicos</p>
|
||||
<FloatingItem
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
icon={<HashtagIcon className="w-4 h-4 text-brand-500" />}
|
||||
title="Título 1"
|
||||
desc="Título de seção grande"
|
||||
/>
|
||||
<FloatingItem
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
icon={<HashtagIcon className="w-4 h-4 text-brand-600" />}
|
||||
title="Título 2"
|
||||
desc="Título médio"
|
||||
/>
|
||||
<FloatingItem
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
icon={<ListBulletIcon className="w-4 h-4 text-emerald-500" />}
|
||||
title="Lista"
|
||||
desc="Lista simples com marcadores"
|
||||
/>
|
||||
<FloatingItem
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
icon={<CodeBracketIcon className="w-4 h-4 text-zinc-600" />}
|
||||
title="Código"
|
||||
desc="Bloco para trechos de código"
|
||||
/>
|
||||
</FloatingMenu>
|
||||
|
||||
{/* Área do Editor */}
|
||||
<div className="relative editor-shell">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.tiptap p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #adb5bd;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
.tiptap {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.8;
|
||||
color: #374151;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
.dark .tiptap { color: #d1d5db; }
|
||||
|
||||
.tiptap h1 { font-size: 3.5rem; font-weight: 950; letter-spacing: -0.06em; margin-top: 3rem; margin-bottom: 1.5rem; color: #111827; line-height: 1.1; }
|
||||
.tiptap h2 { font-size: 2.2rem; font-weight: 800; letter-spacing: -0.04em; margin-top: 2.5rem; margin-bottom: 1rem; color: #1f2937; line-height: 1.2; }
|
||||
.tiptap h3 { font-size: 1.6rem; font-weight: 700; letter-spacing: -0.02em; margin-top: 2rem; margin-bottom: 0.75rem; color: #374151; }
|
||||
|
||||
.dark .tiptap h1 { color: #f9fafb; }
|
||||
.dark .tiptap h2 { color: #f3f4f6; }
|
||||
.dark .tiptap h3 { color: #e5e7eb; }
|
||||
|
||||
.tiptap p { margin: 0.5rem 0; }
|
||||
.tiptap ul { list-style-type: disc; padding-left: 1.5rem; margin: 1rem 0; }
|
||||
.tiptap ol { list-style-type: decimal; padding-left: 1.5rem; margin: 1rem 0; }
|
||||
.tiptap li { margin: 0.25rem 0; }
|
||||
|
||||
.tiptap pre {
|
||||
background: #f8fafc;
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 2rem 0;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.dark .tiptap pre { background: #0a0a0a; border-color: #1a1a1a; box-shadow: none; }
|
||||
|
||||
/* Efeito de Bloco Notion no Hover */
|
||||
.tiptap > * {
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
.tiptap > *:hover::before {
|
||||
content: ':::';
|
||||
position: absolute;
|
||||
left: -2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #d1d5db;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.dark .tiptap > *:hover::before { color: #334155; }
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuButton({ onClick, active, icon, label }: { onClick: () => void, active: boolean, icon?: React.ReactNode, label?: string }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`p-2 rounded-lg transition-all flex items-center justify-center min-w-[32px] ${active ? 'bg-brand-500 text-white shadow-lg' : 'hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-500'}`}
|
||||
>
|
||||
{icon || <span className="text-xs font-black uppercase tracking-widest">{label}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function FloatingItem({ onClick, icon, title, desc }: { onClick: () => void, icon: React.ReactNode, title: string, desc: string }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="flex items-center gap-4 p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-xl text-left transition-all group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 flex items-center justify-center shadow-sm group-hover:scale-110 transition-transform">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-black text-zinc-900 dark:text-white uppercase tracking-widest">{title}</p>
|
||||
<p className="text-[10px] text-zinc-400 font-medium">{desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
1064
front-end-agency/components/erp/FinanceContent.tsx
Normal file
1064
front-end-agency/components/erp/FinanceContent.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, Suspense } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { SidebarRail, MenuItem } from './SidebarRail';
|
||||
import { TopBar } from './TopBar';
|
||||
@@ -20,11 +20,13 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children, menu
|
||||
<div className="flex h-screen w-full bg-gray-100 dark:bg-zinc-950 text-slate-900 dark:text-slate-100 overflow-hidden md:p-3 md:gap-3 transition-colors duration-300">
|
||||
{/* Sidebar controla seu próprio estado visual via props - Desktop Only */}
|
||||
<div className="hidden md:flex">
|
||||
<SidebarRail
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
menuItems={menuItems}
|
||||
/>
|
||||
<Suspense fallback={<div className="w-[80px] bg-white dark:bg-zinc-900" />}>
|
||||
<SidebarRail
|
||||
isExpanded={isExpanded}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
menuItems={menuItems}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Área de Conteúdo (Children) */}
|
||||
@@ -46,7 +48,9 @@ export const DashboardLayout: React.FC<DashboardLayoutProps> = ({ children, menu
|
||||
</main>
|
||||
|
||||
{/* Mobile Bottom Bar */}
|
||||
<MobileBottomBar menuItems={menuItems} />
|
||||
<Suspense fallback={null}>
|
||||
<MobileBottomBar menuItems={menuItems} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { getUser, User, getToken, saveAuth } from '@/lib/auth';
|
||||
@@ -42,6 +42,7 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
menuItems
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -340,23 +341,33 @@ export const SidebarRail: React.FC<SidebarRailProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2 flex-1 overflow-y-auto">
|
||||
{activeMenuItem.subItems?.map((sub) => (
|
||||
<Link
|
||||
key={sub.href}
|
||||
href={sub.href}
|
||||
// onClick={() => setOpenSubmenu(null)} // Removido para manter fixo
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs font-medium transition-colors mb-1
|
||||
${pathname === sub.href
|
||||
? 'bg-brand-50 dark:bg-brand-900/10 text-brand-600 dark:text-brand-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-zinc-800 hover:text-gray-900 dark:hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${pathname === sub.href ? 'bg-brand-500' : 'bg-gray-300 dark:bg-zinc-600'}`} />
|
||||
{sub.label}
|
||||
</Link>
|
||||
))}
|
||||
{activeMenuItem.subItems?.map((sub) => {
|
||||
// Lógica aprimorada de ativo para suportar query params (ex: ?tab=finance)
|
||||
const fullCurrentPath = searchParams.toString()
|
||||
? `${pathname}?${searchParams.toString()}`
|
||||
: pathname;
|
||||
|
||||
const isSubActive = sub.href.includes('?')
|
||||
? fullCurrentPath.includes(sub.href)
|
||||
: pathname === sub.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={sub.href}
|
||||
href={sub.href}
|
||||
className={`
|
||||
flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs font-medium transition-colors mb-1
|
||||
${isSubActive
|
||||
? 'bg-brand-50 dark:bg-brand-900/10 text-brand-600 dark:text-brand-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-zinc-800 hover:text-gray-900 dark:hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${isSubActive ? 'bg-brand-500' : 'bg-gray-300 dark:bg-zinc-600'}`} />
|
||||
{sub.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
29
front-end-agency/components/ui/Badge.tsx
Normal file
29
front-end-agency/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Badge({ children, variant = 'default', className = '' }: BadgeProps) {
|
||||
const variants = {
|
||||
default: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400',
|
||||
success: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400',
|
||||
warning: 'bg-amber-50 text-amber-600 dark:bg-amber-500/10 dark:text-amber-400',
|
||||
error: 'bg-rose-50 text-rose-600 dark:bg-rose-500/10 dark:text-rose-400',
|
||||
info: 'bg-brand-50 text-brand-600 dark:bg-brand-500/10 dark:text-brand-400',
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`
|
||||
px-2.5 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest inline-flex items-center
|
||||
${variants[variant]}
|
||||
${className}
|
||||
`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
64
front-end-agency/components/ui/BulkActionBar.tsx
Normal file
64
front-end-agency/components/ui/BulkActionBar.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
export interface BulkAction {
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
onClick: () => void;
|
||||
variant?: 'danger' | 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
interface BulkActionBarProps {
|
||||
selectedCount: number;
|
||||
actions: BulkAction[];
|
||||
onClearSelection: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* BulkActionBar Component
|
||||
* A floating bar that appears when items are selected in a list/table.
|
||||
* Supports light/dark modes and custom actions.
|
||||
*/
|
||||
export default function BulkActionBar({ selectedCount, actions, onClearSelection }: BulkActionBarProps) {
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-bottom-10 duration-300">
|
||||
<div className="bg-white dark:bg-black border border-zinc-200 dark:border-zinc-900 text-zinc-900 dark:text-white rounded-[32px] px-8 py-4 shadow-[0_20px_50px_rgba(0,0,0,0.1)] dark:shadow-[0_20px_50px_rgba(0,0,0,0.5)] flex items-center gap-8 backdrop-blur-xl">
|
||||
<div className="flex items-center gap-3 pr-8 border-r border-zinc-200 dark:border-zinc-900">
|
||||
<span className="flex items-center justify-center w-8 h-8 bg-brand-500 rounded-full text-xs font-black text-white">
|
||||
{selectedCount}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-zinc-500 dark:text-zinc-400">Selecionados</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{actions.map((action, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={action.onClick}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl transition-all text-sm font-bold
|
||||
${action.variant === 'danger'
|
||||
? 'text-rose-500 hover:bg-rose-500/10'
|
||||
: action.variant === 'primary'
|
||||
? 'text-brand-600 dark:text-brand-400 hover:bg-brand-500/10'
|
||||
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800'
|
||||
}`}
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClearSelection}
|
||||
className="ml-4 p-2 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-colors"
|
||||
title="Limpar seleção"
|
||||
>
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
front-end-agency/components/ui/Card.tsx
Normal file
55
front-end-agency/components/ui/Card.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
headerAction?: ReactNode;
|
||||
noPadding?: boolean;
|
||||
onClick?: () => void;
|
||||
allowOverflow?: boolean;
|
||||
}
|
||||
|
||||
export default function Card({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
className = "",
|
||||
headerAction,
|
||||
noPadding = false,
|
||||
onClick,
|
||||
allowOverflow = false
|
||||
}: CardProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 transition-all ${allowOverflow ? '' : 'overflow-hidden'} ${className}`}
|
||||
>
|
||||
{(title || description || headerAction) && (
|
||||
<div className="px-6 py-4 border-b border-zinc-100 dark:border-zinc-800 flex items-center justify-between">
|
||||
<div>
|
||||
{title && (
|
||||
<h3 className="text-base font-bold text-zinc-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-400 mt-0.5">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{headerAction && (
|
||||
<div>{headerAction}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={noPadding ? "" : "p-6"}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
front-end-agency/components/ui/CustomSelect.tsx
Normal file
103
front-end-agency/components/ui/CustomSelect.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, useState } from "react";
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string | number;
|
||||
icon?: React.ReactNode;
|
||||
color?: string; // Cor para badge/ponto
|
||||
}
|
||||
|
||||
interface CustomSelectProps {
|
||||
options: SelectOption[];
|
||||
value: string | number;
|
||||
onChange: (value: any) => void;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
}
|
||||
|
||||
export default function CustomSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
placeholder = "Selecione...",
|
||||
className = "",
|
||||
buttonClassName = ""
|
||||
}: CustomSelectProps) {
|
||||
const selected = options.find((opt) => opt.value === value) || null;
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
{label && (
|
||||
<label className="block text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider mb-2">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
<div className="relative">
|
||||
<ListboxButton
|
||||
className={`
|
||||
relative w-full cursor-pointer rounded-xl bg-white dark:bg-zinc-900 py-2.5 pl-4 pr-10 text-left text-sm font-semibold transition-all border
|
||||
${buttonClassName || 'border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400'}
|
||||
focus:outline-none focus:border-zinc-400 dark:focus:border-zinc-500
|
||||
`}
|
||||
>
|
||||
<span className="flex items-center gap-2 truncate">
|
||||
{selected?.color && (
|
||||
<span className={`w-2 h-2 rounded-full ${selected.color}`} />
|
||||
)}
|
||||
{selected?.icon && <span>{selected.icon}</span>}
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-zinc-400" aria-hidden="true" />
|
||||
</span>
|
||||
</ListboxButton>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<ListboxOptions className="absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 py-1 focus:outline-none sm:text-sm">
|
||||
{options.map((option, idx) => (
|
||||
<ListboxOption
|
||||
key={idx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2.5 pl-10 pr-4 transition-colors ${active ? "bg-zinc-50 dark:bg-zinc-800 text-brand-600 dark:text-brand-400" : "text-zinc-700 dark:text-zinc-300"
|
||||
}`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
{({ selected: isSelected }) => (
|
||||
<>
|
||||
<span className={`flex items-center gap-2 truncate ${isSelected ? "font-bold" : "font-medium"}`}>
|
||||
{option.color && (
|
||||
<span className={`w-2 h-2 rounded-full ${option.color}`} />
|
||||
)}
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
{option.label}
|
||||
</span>
|
||||
{isSelected ? (
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-brand-600 dark:text-brand-400">
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
front-end-agency/components/ui/DataTable.tsx
Normal file
178
front-end-agency/components/ui/DataTable.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { Button } from "./index";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
interface Column<T> {
|
||||
header: string;
|
||||
accessor: keyof T | ((item: T) => ReactNode);
|
||||
className?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
isLoading?: boolean;
|
||||
emptyMessage?: string;
|
||||
pagination?: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
totalItems: number;
|
||||
};
|
||||
onRowClick?: (item: T) => void;
|
||||
selectable?: boolean;
|
||||
selectedIds?: (string | number)[];
|
||||
onSelectionChange?: (ids: (string | number)[]) => void;
|
||||
}
|
||||
|
||||
export default function DataTable<T extends { id: string | number }>({
|
||||
columns,
|
||||
data,
|
||||
isLoading = false,
|
||||
emptyMessage = "Nenhum resultado encontrado.",
|
||||
pagination,
|
||||
onRowClick,
|
||||
selectable = false,
|
||||
selectedIds = [],
|
||||
onSelectionChange
|
||||
}: DataTableProps<T>) {
|
||||
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!onSelectionChange) return;
|
||||
if (e.target.checked) {
|
||||
onSelectionChange(data.map(item => item.id));
|
||||
} else {
|
||||
onSelectionChange([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectItem = (id: string | number) => {
|
||||
if (!onSelectionChange) return;
|
||||
if (selectedIds.includes(id)) {
|
||||
onSelectionChange(selectedIds.filter(i => i !== id));
|
||||
} else {
|
||||
onSelectionChange([...selectedIds, id]);
|
||||
}
|
||||
};
|
||||
|
||||
const isAllSelected = data.length > 0 && selectedIds.length === data.length;
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-zinc-50/50 dark:bg-zinc-800/50 border-b border-zinc-200 dark:border-zinc-800">
|
||||
{selectable && (
|
||||
<th className="px-6 py-4 w-10 text-left">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAllSelected}
|
||||
onChange={handleSelectAll}
|
||||
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<th
|
||||
key={index}
|
||||
className={`px-6 py-4 text-xs font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider ${column.align === 'right' ? 'text-right' :
|
||||
column.align === 'center' ? 'text-center' : 'text-left'
|
||||
} ${column.className || ''}`}
|
||||
>
|
||||
{column.header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800">
|
||||
{isLoading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<tr key={i} className="animate-pulse">
|
||||
{columns.map((_, j) => (
|
||||
<td key={j} className="px-6 py-4">
|
||||
<div className="h-4 bg-zinc-100 dark:bg-zinc-800 rounded w-full"></div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length + (selectable ? 1 : 0)} className="px-6 py-12 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((item) => {
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
return (
|
||||
<tr
|
||||
key={item.id}
|
||||
onClick={() => onRowClick?.(item)}
|
||||
className={`transition-colors group ${isSelected ? 'bg-brand-50/30 dark:bg-brand-500/5' : ''} ${onRowClick ? 'cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50' : 'hover:bg-zinc-50/50 dark:hover:bg-zinc-800/30'}`}
|
||||
>
|
||||
{selectable && (
|
||||
<td className="px-6 py-4 w-10" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => handleSelectItem(item.id)}
|
||||
className="w-4 h-4 rounded border-zinc-300 text-brand-600 focus:ring-brand-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
{columns.map((column, index) => (
|
||||
<td
|
||||
key={index}
|
||||
className={`px-6 py-4 text-sm text-zinc-600 dark:text-zinc-300 ${column.align === 'right' ? 'text-right' :
|
||||
column.align === 'center' ? 'text-center' : 'text-left'
|
||||
} ${column.className || ''}`}
|
||||
>
|
||||
{typeof column.accessor === 'function'
|
||||
? column.accessor(item)
|
||||
: (item[column.accessor] as ReactNode)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div className="p-4 bg-zinc-50/30 dark:bg-zinc-900/30 border-t border-zinc-200 dark:border-zinc-800 flex items-center justify-between">
|
||||
<span className="text-xs text-zinc-500 italic">
|
||||
Mostrando {data.length} de {pagination.totalItems} resultados
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.currentPage <= 1 || isLoading}
|
||||
onClick={() => pagination.onPageChange(pagination.currentPage - 1)}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 mr-1" />
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={pagination.currentPage >= pagination.totalPages || isLoading}
|
||||
onClick={() => pagination.onPageChange(pagination.currentPage + 1)}
|
||||
>
|
||||
Próximo
|
||||
<ChevronRightIcon className="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
242
front-end-agency/components/ui/DatePicker.tsx
Normal file
242
front-end-agency/components/ui/DatePicker.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Fragment } from "react";
|
||||
import { Popover, PopoverButton, PopoverPanel, Transition } from "@headlessui/react";
|
||||
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, ClockIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
format,
|
||||
addMonths,
|
||||
subMonths,
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
startOfWeek,
|
||||
endOfWeek,
|
||||
isSameMonth,
|
||||
isSameDay,
|
||||
addDays,
|
||||
eachDayOfInterval,
|
||||
isWithinInterval,
|
||||
isBefore,
|
||||
subDays
|
||||
} from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
|
||||
interface DatePickerProps {
|
||||
value?: { start: Date | null; end: Date | null } | Date | null;
|
||||
onChange: (val: any) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
buttonClassName?: string;
|
||||
mode?: 'single' | 'range';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default function DatePicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
mode = 'range',
|
||||
label
|
||||
}: DatePickerProps) {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
|
||||
// Helper to normalize value
|
||||
const getRange = () => {
|
||||
if (mode === 'single') {
|
||||
const date = value instanceof Date ? value : (value as any)?.start || null;
|
||||
return { start: date, end: date };
|
||||
}
|
||||
const range = (value as { start: Date | null; end: Date | null }) || { start: null, end: null };
|
||||
return range;
|
||||
};
|
||||
|
||||
const range = getRange();
|
||||
|
||||
const quickRanges = [
|
||||
{ label: 'Hoje', getValue: () => ({ start: new Date(), end: new Date() }) },
|
||||
{ label: 'Últimos 7 dias', getValue: () => ({ start: subDays(new Date(), 7), end: new Date() }) },
|
||||
{ label: 'Últimos 14 dias', getValue: () => ({ start: subDays(new Date(), 14), end: new Date() }) },
|
||||
{ label: 'Últimos 30 dias', getValue: () => ({ start: subDays(new Date(), 30), end: new Date() }) },
|
||||
{ label: 'Este Mês', getValue: () => ({ start: startOfMonth(new Date()), end: endOfMonth(new Date()) }) },
|
||||
];
|
||||
|
||||
const handleDateClick = (day: Date) => {
|
||||
if (mode === 'single') {
|
||||
onChange(day);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!range.start || (range.start && range.end)) {
|
||||
onChange({ start: day, end: null });
|
||||
} else {
|
||||
if (isBefore(day, range.start)) {
|
||||
onChange({ start: day, end: range.start });
|
||||
} else {
|
||||
onChange({ start: range.start, end: day });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isInRange = (day: Date) => {
|
||||
if (range.start && range.end) {
|
||||
return isWithinInterval(day, { start: range.start, end: range.end });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const displayValue = () => {
|
||||
if (!range.start) return placeholder || (mode === 'single' ? "Selecionar data" : "Selecionar período");
|
||||
if (mode === 'single') return format(range.start, "dd/MM/yyyy", { locale: ptBR });
|
||||
if (!range.end || isSameDay(range.start, range.end)) return format(range.start, "dd MMM yyyy", { locale: ptBR });
|
||||
return `${format(range.start, "dd MMM", { locale: ptBR })} - ${format(range.end, "dd MMM yyyy", { locale: ptBR })}`;
|
||||
};
|
||||
|
||||
const handleClear = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onChange(mode === 'single' ? null : { start: null, end: null });
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className={`relative ${className}`}>
|
||||
{label && (
|
||||
<label className="block text-sm font-bold text-zinc-700 dark:text-zinc-300 mb-2">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<PopoverButton
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-2.5 text-sm font-bold transition-all outline-none border rounded-xl group
|
||||
${buttonClassName || 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400'}
|
||||
focus:border-zinc-400 dark:focus:border-zinc-500
|
||||
`}
|
||||
>
|
||||
<CalendarIcon className="w-4.5 h-4.5 text-zinc-400 group-hover:text-brand-500 transition-colors shrink-0" />
|
||||
<span className="flex-1 text-left truncate">
|
||||
{displayValue()}
|
||||
</span>
|
||||
{range.start && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg text-zinc-400 hover:text-rose-500 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.6} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</PopoverButton>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<PopoverPanel
|
||||
anchor="bottom end"
|
||||
className="flex bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl shadow-xl overflow-hidden outline-none [--anchor-gap:8px] min-w-[480px]"
|
||||
>
|
||||
{/* Filtros Rápidos (Apenas para Range) */}
|
||||
{mode === 'range' && (
|
||||
<div className="w-48 bg-zinc-50/80 dark:bg-zinc-900/80 border-r border-zinc-100 dark:border-zinc-800 p-4 space-y-1.5">
|
||||
<div className="flex items-center gap-2 px-1 py-1 mb-2">
|
||||
<ClockIcon className="w-3.5 h-3.5 text-zinc-400" />
|
||||
<p className="text-[10px] font-black text-zinc-400 uppercase tracking-[0.1em]">Atalhos</p>
|
||||
</div>
|
||||
{quickRanges.map((r) => (
|
||||
<button
|
||||
key={r.label}
|
||||
type="button"
|
||||
onClick={() => onChange(r.getValue())}
|
||||
className="w-full px-3 py-2 text-left text-[11px] font-bold text-zinc-600 dark:text-zinc-400 hover:bg-white dark:hover:bg-zinc-800 hover:text-brand-600 dark:hover:text-brand-400 rounded-xl transition-all border border-transparent hover:border-zinc-100 dark:hover:border-zinc-700 hover:shadow-sm"
|
||||
>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="pt-4 mt-4 border-t border-zinc-100 dark:border-zinc-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ start: null, end: null })}
|
||||
className="w-full px-3 py-2 text-left text-[11px] font-bold text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-all"
|
||||
>
|
||||
Limpar Filtro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Calendário */}
|
||||
<div className="flex-1 p-4">
|
||||
<div className="flex items-center justify-between mb-4 px-1">
|
||||
<span className="text-sm font-bold text-zinc-900 dark:text-white capitalize">
|
||||
{format(currentMonth, "MMMM yyyy", { locale: ptBR })}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
|
||||
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors border border-zinc-100 dark:border-zinc-800"
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4 text-zinc-500" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
|
||||
className="p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors border border-zinc-100 dark:border-zinc-800"
|
||||
>
|
||||
<ChevronRightIcon className="w-4 h-4 text-zinc-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 mb-2">
|
||||
{["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => (
|
||||
<div key={idx} className="text-center text-[10px] font-bold text-zinc-400 py-1">
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{eachDayOfInterval({
|
||||
start: startOfWeek(startOfMonth(currentMonth)),
|
||||
end: endOfWeek(endOfMonth(currentMonth))
|
||||
}).map((day, idx) => {
|
||||
const isStart = range.start && isSameDay(day, range.start);
|
||||
const isEnd = range.end && isSameDay(day, range.end);
|
||||
const isSelected = isStart || isEnd;
|
||||
const isRange = isInRange(day);
|
||||
const isCurrentMonth = isSameMonth(day, startOfMonth(currentMonth));
|
||||
const isTodayDate = isSameDay(day, new Date());
|
||||
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => handleDateClick(day)}
|
||||
className={`
|
||||
relative py-2.5 text-[11px] transition-all outline-none flex items-center justify-center font-bold h-10 w-10
|
||||
${!isCurrentMonth ? 'text-zinc-300 dark:text-zinc-600' : 'text-zinc-900 dark:text-zinc-100'}
|
||||
${isSelected ? 'bg-brand-500 text-white rounded-xl z-10 scale-105 shadow-lg shadow-brand-500/20' : ''}
|
||||
${isRange && !isSelected && mode === 'range' ? 'bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400' : ''}
|
||||
${!isSelected ? 'hover:bg-zinc-100 dark:hover:bg-zinc-800 hover:rounded-xl' : ''}
|
||||
${isTodayDate && !isSelected ? 'text-brand-600 ring-1 ring-brand-100 dark:ring-brand-500/30 rounded-xl' : ''}
|
||||
`}
|
||||
>
|
||||
<span>{format(day, "d")}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
${isPassword || rightIcon ? "pr-11" : ""}
|
||||
${error
|
||||
? "border-red-500 focus:border-red-500 focus:ring-4 focus:ring-red-500/10"
|
||||
: "border-gray-200 dark:border-gray-700 focus:border-brand-500 focus:ring-4 focus:ring-brand-500/10"
|
||||
: "border-zinc-200 dark:border-zinc-700 focus:border-zinc-400 dark:focus:border-zinc-500 focus:ring-0"
|
||||
}
|
||||
outline-none
|
||||
disabled:bg-gray-50 disabled:text-gray-500 disabled:cursor-not-allowed
|
||||
|
||||
76
front-end-agency/components/ui/PageHeader.tsx
Normal file
76
front-end-agency/components/ui/PageHeader.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import Button from "./Button";
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
primaryAction?: {
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
icon?: ReactNode;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
secondaryAction?: {
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
title,
|
||||
description,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
children
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-white tracking-tight truncate">
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{secondaryAction && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={secondaryAction.onClick}
|
||||
className="bg-white dark:bg-zinc-900"
|
||||
>
|
||||
{secondaryAction.icon && (
|
||||
<span className="mr-2">{secondaryAction.icon}</span>
|
||||
)}
|
||||
{secondaryAction.label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{primaryAction && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={primaryAction.onClick}
|
||||
isLoading={primaryAction.isLoading}
|
||||
className="shadow-lg shadow-brand-500/20"
|
||||
style={{ background: 'var(--gradient)' }}
|
||||
>
|
||||
{primaryAction.icon && !primaryAction.isLoading && (
|
||||
<span className="mr-2">{primaryAction.icon}</span>
|
||||
)}
|
||||
{primaryAction.label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
front-end-agency/components/ui/StatsCard.tsx
Normal file
67
front-end-agency/components/ui/StatsCard.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { ArrowTrendingUpIcon as TrendingUpIcon, ArrowTrendingDownIcon as TrendingDownIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
interface StatsCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: ReactNode;
|
||||
trend?: {
|
||||
value: string | number;
|
||||
label: string;
|
||||
type: "up" | "down" | "neutral";
|
||||
};
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function StatsCard({
|
||||
title,
|
||||
value,
|
||||
icon,
|
||||
trend,
|
||||
description
|
||||
}: StatsCardProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 p-6 transition-all group">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{title}
|
||||
</p>
|
||||
<h3 className="mt-1 text-2xl font-bold text-zinc-900 dark:text-white group-hover:text-brand-500 transition-colors">
|
||||
{value}
|
||||
</h3>
|
||||
|
||||
{trend && (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<div className={`flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold ${trend.type === 'up'
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400'
|
||||
: trend.type === 'down'
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/20 dark:text-red-400'
|
||||
: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400'
|
||||
}`}>
|
||||
{trend.type === 'up' && <TrendingUpIcon className="w-3 h-3" />}
|
||||
{trend.type === 'down' && <TrendingDownIcon className="w-3 h-3" />}
|
||||
{trend.value}
|
||||
</div>
|
||||
<span className="text-[10px] text-zinc-500 dark:text-zinc-500">
|
||||
{trend.label}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{description && !trend && (
|
||||
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-zinc-50 dark:bg-zinc-800 rounded-xl text-zinc-500 dark:text-zinc-400 group-hover:bg-brand-50 dark:group-hover:bg-brand-900/10 group-hover:text-brand-500 transition-all">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
front-end-agency/components/ui/Tabs.tsx
Normal file
69
front-end-agency/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, ReactNode } from 'react';
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react';
|
||||
|
||||
interface TabItem {
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface TabsProps {
|
||||
items: TabItem[];
|
||||
defaultIndex?: number;
|
||||
onChange?: (index: number) => void;
|
||||
className?: string;
|
||||
variant?: 'pills' | 'underline';
|
||||
}
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function Tabs({
|
||||
items,
|
||||
defaultIndex = 0,
|
||||
onChange,
|
||||
className = "",
|
||||
variant = 'pills'
|
||||
}: TabsProps) {
|
||||
return (
|
||||
<TabGroup defaultIndex={defaultIndex} onChange={onChange} className={className}>
|
||||
<TabList className={classNames(
|
||||
'flex space-x-1 p-1 mb-6',
|
||||
variant === 'pills' ? 'bg-zinc-100 dark:bg-zinc-800/50 rounded-xl' : 'border-b border-zinc-200 dark:border-zinc-800 bg-transparent'
|
||||
)}>
|
||||
{items.map((item, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
disabled={item.disabled}
|
||||
className={({ selected }) =>
|
||||
classNames(
|
||||
'flex items-center justify-center gap-2 py-2.5 text-sm font-bold transition-all outline-none rounded-lg flex-1 cursor-pointer',
|
||||
variant === 'pills'
|
||||
? selected
|
||||
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm ring-1 ring-black/5'
|
||||
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:bg-white/50 dark:hover:bg-zinc-700/30'
|
||||
: selected
|
||||
? 'border-b-2 border-brand-500 text-brand-500 rounded-none'
|
||||
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 border-b-2 border-transparent rounded-none'
|
||||
)
|
||||
}
|
||||
>
|
||||
{item.icon && <span className="w-4 h-4">{item.icon}</span>}
|
||||
{item.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{items.map((item, index) => (
|
||||
<TabPanel key={index} className="outline-none">
|
||||
{item.content}
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
}
|
||||
@@ -4,3 +4,13 @@ export { default as Checkbox } from "./Checkbox";
|
||||
export { default as Select } from "./Select";
|
||||
export { default as SearchableSelect } from "./SearchableSelect";
|
||||
export { default as Dialog } from "./Dialog";
|
||||
export { default as PageHeader } from "./PageHeader";
|
||||
export { default as Card } from "./Card";
|
||||
export { default as StatsCard } from "./StatsCard";
|
||||
export { default as Tabs } from "./Tabs";
|
||||
export { default as DataTable } from "./DataTable";
|
||||
export { default as DatePicker } from "./DatePicker";
|
||||
export { default as CustomSelect } from "./CustomSelect";
|
||||
export { default as Badge } from "./Badge";
|
||||
export { default as BulkActionBar } from "./BulkActionBar";
|
||||
export { default as ConfirmDialog } from "../layout/ConfirmDialog";
|
||||
|
||||
509
front-end-agency/front-end-agency/node_modules/.package-lock.json
generated
vendored
Normal file
509
front-end-agency/front-end-agency/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,509 @@
|
||||
{
|
||||
"name": "front-end-agency",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz",
|
||||
"integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts": {
|
||||
"version": "1.8.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/recharts/-/recharts-1.8.29.tgz",
|
||||
"integrity": "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-shape": "^1",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/recharts/node_modules/@types/d3-path": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz",
|
||||
"integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/recharts/node_modules/@types/d3-shape": {
|
||||
"version": "1.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz",
|
||||
"integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "^1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
|
||||
"integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.562.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
|
||||
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz",
|
||||
"integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
|
||||
"integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"www"
|
||||
],
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/LICENSE
generated
vendored
Normal file
21
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Mark Erikson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
110
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/README.md
generated
vendored
Normal file
110
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/README.md
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Redux Toolkit
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/@reduxjs/toolkit)
|
||||
[](https://www.npmjs.com/package/@reduxjs/toolkit)
|
||||
|
||||
**The official, opinionated, batteries-included toolset for efficient Redux development**
|
||||
|
||||
## Installation
|
||||
|
||||
### Create a React Redux App
|
||||
|
||||
The recommended way to start new apps with React and Redux Toolkit is by using [our official Redux Toolkit + TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux).
|
||||
|
||||
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
|
||||
|
||||
```bash
|
||||
# Vite with our Redux+TS template
|
||||
# (using the `degit` tool to clone and extract the template)
|
||||
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
|
||||
|
||||
# Next.js using the `with-redux` template
|
||||
npx create-next-app --example with-redux my-app
|
||||
```
|
||||
|
||||
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
|
||||
|
||||
- https://github.com/rahsheen/react-native-template-redux-typescript
|
||||
- https://github.com/rahsheen/expo-template-redux-typescript
|
||||
|
||||
### An Existing App
|
||||
|
||||
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
|
||||
|
||||
```bash
|
||||
# NPM
|
||||
npm install @reduxjs/toolkit
|
||||
|
||||
# Yarn
|
||||
yarn add @reduxjs/toolkit
|
||||
```
|
||||
|
||||
The package includes a precompiled ESM build that can be used as a [`<script type="module">` tag](https://unpkg.com/@reduxjs/toolkit/dist/redux-toolkit.browser.mjs) directly in the browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The Redux Toolkit docs are available at **https://redux-toolkit.js.org**, including API references and usage guides for all of the APIs included in Redux Toolkit.
|
||||
|
||||
The Redux core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.
|
||||
|
||||
## Purpose
|
||||
|
||||
The **Redux Toolkit** package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
|
||||
|
||||
- "Configuring a Redux store is too complicated"
|
||||
- "I have to add a lot of packages to get Redux to do anything useful"
|
||||
- "Redux requires too much boilerplate code"
|
||||
|
||||
We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
|
||||
|
||||
Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.
|
||||
|
||||
Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
|
||||
|
||||
## What's Included
|
||||
|
||||
Redux Toolkit includes these APIs:
|
||||
|
||||
- `configureStore()`: wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
|
||||
- `createReducer()`: lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
|
||||
- `createAction()`: generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
|
||||
- `createSlice()`: combines `createReducer()` + `createAction()`. Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
|
||||
- `combineSlices()`: combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.
|
||||
- `createListenerMiddleware()`: lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.
|
||||
- `createAsyncThunk()`: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
|
||||
- `createEntityAdapter()`: generates a set of reusable reducers and selectors to manage normalized data in the store
|
||||
- The `createSelector()` utility from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
|
||||
|
||||
For details, see [the Redux Toolkit API Reference section in the docs](https://redux-toolkit.js.org/api/configureStore).
|
||||
|
||||
## RTK Query
|
||||
|
||||
**RTK Query** is provided as an optional addon within the `@reduxjs/toolkit` package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
|
||||
|
||||
RTK Query is built on top of the Redux Toolkit core for its implementation, using [Redux](https://redux.js.org/) internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the [Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools), which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.
|
||||
|
||||
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
|
||||
|
||||
```ts no-transpile
|
||||
import { createApi } from '@reduxjs/toolkit/query'
|
||||
|
||||
/* React-specific entry point that automatically generates
|
||||
hooks corresponding to the defined endpoints */
|
||||
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||
```
|
||||
|
||||
### What's included
|
||||
|
||||
RTK Query includes these APIs:
|
||||
|
||||
- `createApi()`: The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.
|
||||
- `fetchBaseQuery()`: A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
|
||||
- `<ApiProvider />`: Can be used as a Provider if you do not already have a Redux store.
|
||||
- `setupListeners()`: A utility used to enable refetchOnMount and refetchOnReconnect behaviors.
|
||||
|
||||
See the [**RTK Query Overview**](https://redux-toolkit.js.org/rtk-query/overview) page for more details on what RTK Query is, what problems it solves, and how to use it.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to our [contributing guide](/CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.
|
||||
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/index.js
generated
vendored
Normal file
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/index.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./redux-toolkit.production.min.cjs')
|
||||
} else {
|
||||
module.exports = require('./redux-toolkit.development.cjs')
|
||||
}
|
||||
2387
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs
generated
vendored
Normal file
2387
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.production.min.cjs
generated
vendored
Normal file
3
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.production.min.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.production.min.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.production.min.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2662
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/index.d.mts
generated
vendored
Normal file
2662
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/index.d.mts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2662
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/index.d.ts
generated
vendored
Normal file
2662
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/index.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/index.js
generated
vendored
Normal file
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/index.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./rtk-query.production.min.cjs')
|
||||
} else {
|
||||
module.exports = require('./rtk-query.development.cjs')
|
||||
}
|
||||
3087
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.development.cjs
generated
vendored
Normal file
3087
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.development.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.development.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.development.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.production.min.cjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.production.min.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.production.min.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/cjs/rtk-query.production.min.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2954
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/index.d.mts
generated
vendored
Normal file
2954
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/index.d.mts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2954
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/index.d.ts
generated
vendored
Normal file
2954
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/index.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/index.js
generated
vendored
Normal file
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/index.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./rtk-query-react.production.min.cjs')
|
||||
} else {
|
||||
module.exports = require('./rtk-query-react.development.cjs')
|
||||
}
|
||||
748
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.development.cjs
generated
vendored
Normal file
748
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.development.cjs
generated
vendored
Normal file
@@ -0,0 +1,748 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/query/react/index.ts
|
||||
var react_exports = {};
|
||||
__export(react_exports, {
|
||||
ApiProvider: () => ApiProvider,
|
||||
UNINITIALIZED_VALUE: () => UNINITIALIZED_VALUE,
|
||||
createApi: () => createApi,
|
||||
reactHooksModule: () => reactHooksModule,
|
||||
reactHooksModuleName: () => reactHooksModuleName
|
||||
});
|
||||
module.exports = __toCommonJS(react_exports);
|
||||
|
||||
// src/query/react/rtkqImports.ts
|
||||
var import_query = require("@reduxjs/toolkit/query");
|
||||
|
||||
// src/query/react/module.ts
|
||||
var import_toolkit2 = require("@reduxjs/toolkit");
|
||||
var import_react_redux2 = require("react-redux");
|
||||
var import_reselect = require("reselect");
|
||||
|
||||
// src/query/utils/capitalize.ts
|
||||
function capitalize(str) {
|
||||
return str.replace(str[0], str[0].toUpperCase());
|
||||
}
|
||||
|
||||
// src/query/utils/countObjectKeys.ts
|
||||
function countObjectKeys(obj) {
|
||||
let count = 0;
|
||||
for (const _key in obj) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// src/query/endpointDefinitions.ts
|
||||
var ENDPOINT_QUERY = "query" /* query */;
|
||||
var ENDPOINT_MUTATION = "mutation" /* mutation */;
|
||||
var ENDPOINT_INFINITEQUERY = "infinitequery" /* infinitequery */;
|
||||
function isQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_QUERY;
|
||||
}
|
||||
function isMutationDefinition(e) {
|
||||
return e.type === ENDPOINT_MUTATION;
|
||||
}
|
||||
function isInfiniteQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_INFINITEQUERY;
|
||||
}
|
||||
|
||||
// src/query/tsHelpers.ts
|
||||
function safeAssign(target, ...args) {
|
||||
return Object.assign(target, ...args);
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
var import_toolkit = require("@reduxjs/toolkit");
|
||||
|
||||
// src/query/react/reactImports.ts
|
||||
var import_react = require("react");
|
||||
|
||||
// src/query/react/reactReduxImports.ts
|
||||
var import_react_redux = require("react-redux");
|
||||
|
||||
// src/query/react/constants.ts
|
||||
var UNINITIALIZED_VALUE = Symbol();
|
||||
|
||||
// src/query/react/useSerializedStableValue.ts
|
||||
function useStableQueryArgs(queryArgs) {
|
||||
const cache = (0, import_react.useRef)(queryArgs);
|
||||
const copy = (0, import_react.useMemo)(() => (0, import_query.copyWithStructuralSharing)(cache.current, queryArgs), [queryArgs]);
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (cache.current !== copy) {
|
||||
cache.current = copy;
|
||||
}
|
||||
}, [copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// src/query/react/useShallowStableValue.ts
|
||||
function useShallowStableValue(value) {
|
||||
const cache = (0, import_react.useRef)(value);
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (!(0, import_react_redux.shallowEqual)(cache.current, value)) {
|
||||
cache.current = value;
|
||||
}
|
||||
}, [value]);
|
||||
return (0, import_react_redux.shallowEqual)(cache.current, value) ? cache.current : value;
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
var canUseDOM = () => !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
|
||||
var isDOM = /* @__PURE__ */ canUseDOM();
|
||||
var isRunningInReactNative = () => typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
||||
var isReactNative = /* @__PURE__ */ isRunningInReactNative();
|
||||
var getUseIsomorphicLayoutEffect = () => isDOM || isReactNative ? import_react.useLayoutEffect : import_react.useEffect;
|
||||
var useIsomorphicLayoutEffect = /* @__PURE__ */ getUseIsomorphicLayoutEffect();
|
||||
var noPendingQueryStateSelector = (selected) => {
|
||||
if (selected.isUninitialized) {
|
||||
return {
|
||||
...selected,
|
||||
isUninitialized: false,
|
||||
isFetching: true,
|
||||
isLoading: selected.data !== void 0 ? false : true,
|
||||
// This is the one place where we still have to use `QueryStatus` as an enum,
|
||||
// since it's the only reference in the React package and not in the core.
|
||||
status: import_query.QueryStatus.pending
|
||||
};
|
||||
}
|
||||
return selected;
|
||||
};
|
||||
function pick(obj, ...keys) {
|
||||
const ret = {};
|
||||
keys.forEach((key) => {
|
||||
ret[key] = obj[key];
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
var COMMON_HOOK_DEBUG_FIELDS = ["data", "status", "isLoading", "isSuccess", "isError", "error"];
|
||||
function buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks: {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
useStore
|
||||
},
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
}) {
|
||||
const usePossiblyImmediateEffect = unstable__sideEffectsInRender ? (cb) => cb() : import_react.useEffect;
|
||||
const unsubscribePromiseRef = (ref) => ref.current?.unsubscribe?.();
|
||||
const endpointDefinitions = context.endpointDefinitions;
|
||||
return {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
};
|
||||
function queryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if (lastResult?.endpointName && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== import_query.skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || hasData && (isFetching && !lastResult?.isError || currentState.isUninitialized);
|
||||
return {
|
||||
...currentState,
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
};
|
||||
}
|
||||
function infiniteQueryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if (lastResult?.endpointName && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== import_query.skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || isFetching && hasData;
|
||||
return {
|
||||
...currentState,
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
};
|
||||
}
|
||||
function usePrefetch(endpointName, defaultOptions) {
|
||||
const dispatch = useDispatch();
|
||||
const stableDefaultOptions = useShallowStableValue(defaultOptions);
|
||||
return (0, import_react.useCallback)((arg, options) => dispatch(api.util.prefetch(endpointName, arg, {
|
||||
...stableDefaultOptions,
|
||||
...options
|
||||
})), [endpointName, dispatch, stableDefaultOptions]);
|
||||
}
|
||||
function useQuerySubscriptionCommonImpl(endpointName, arg, {
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
refetchOnMountOrArgChange,
|
||||
skip = false,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false,
|
||||
...rest
|
||||
} = {}) {
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const subscriptionSelectorsRef = (0, import_react.useRef)(void 0);
|
||||
if (!subscriptionSelectorsRef.current) {
|
||||
const returnedValue = dispatch(api.internalActions.internal_getRTKQSubscriptions());
|
||||
if (true) {
|
||||
if (typeof returnedValue !== "object" || typeof returnedValue?.type === "string") {
|
||||
throw new Error(false ? _formatProdErrorMessage(37) : `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
|
||||
You must add the middleware for RTK-Query to function correctly!`);
|
||||
}
|
||||
}
|
||||
subscriptionSelectorsRef.current = returnedValue;
|
||||
}
|
||||
const stableArg = useStableQueryArgs(skip ? import_query.skipToken : arg);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
const initialPageParam = rest.initialPageParam;
|
||||
const stableInitialPageParam = useShallowStableValue(initialPageParam);
|
||||
const refetchCachedPages = rest.refetchCachedPages;
|
||||
const stableRefetchCachedPages = useShallowStableValue(refetchCachedPages);
|
||||
const promiseRef = (0, import_react.useRef)(void 0);
|
||||
let {
|
||||
queryCacheKey,
|
||||
requestId
|
||||
} = promiseRef.current || {};
|
||||
let currentRenderHasSubscription = false;
|
||||
if (queryCacheKey && requestId) {
|
||||
currentRenderHasSubscription = subscriptionSelectorsRef.current.isRequestSubscribed(queryCacheKey, requestId);
|
||||
}
|
||||
const subscriptionRemoved = !currentRenderHasSubscription && promiseRef.current !== void 0;
|
||||
usePossiblyImmediateEffect(() => {
|
||||
if (subscriptionRemoved) {
|
||||
promiseRef.current = void 0;
|
||||
}
|
||||
}, [subscriptionRemoved]);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
const lastPromise = promiseRef.current;
|
||||
if (typeof process !== "undefined" && false) {
|
||||
console.log(subscriptionRemoved);
|
||||
}
|
||||
if (stableArg === import_query.skipToken) {
|
||||
lastPromise?.unsubscribe();
|
||||
promiseRef.current = void 0;
|
||||
return;
|
||||
}
|
||||
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
|
||||
if (!lastPromise || lastPromise.arg !== stableArg) {
|
||||
lastPromise?.unsubscribe();
|
||||
const promise = dispatch(initiate(stableArg, {
|
||||
subscriptionOptions: stableSubscriptionOptions,
|
||||
forceRefetch: refetchOnMountOrArgChange,
|
||||
...isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? {
|
||||
initialPageParam: stableInitialPageParam,
|
||||
refetchCachedPages: stableRefetchCachedPages
|
||||
} : {}
|
||||
}));
|
||||
promiseRef.current = promise;
|
||||
} else if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
lastPromise.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [dispatch, initiate, refetchOnMountOrArgChange, stableArg, stableSubscriptionOptions, subscriptionRemoved, stableInitialPageParam, stableRefetchCachedPages, endpointName]);
|
||||
return [promiseRef, dispatch, initiate, stableSubscriptionOptions];
|
||||
}
|
||||
function buildUseQueryState(endpointName, preSelector) {
|
||||
const useQueryState = (arg, {
|
||||
skip = false,
|
||||
selectFromResult
|
||||
} = {}) => {
|
||||
const {
|
||||
select
|
||||
} = api.endpoints[endpointName];
|
||||
const stableArg = useStableQueryArgs(skip ? import_query.skipToken : arg);
|
||||
const lastValue = (0, import_react.useRef)(void 0);
|
||||
const selectDefaultResult = (0, import_react.useMemo)(() => (
|
||||
// Normally ts-ignores are bad and should be avoided, but we're
|
||||
// already casting this selector to be `Selector<any>` anyway,
|
||||
// so the inconsistencies don't matter here
|
||||
// @ts-ignore
|
||||
createSelector([
|
||||
// @ts-ignore
|
||||
select(stableArg),
|
||||
(_, lastResult) => lastResult,
|
||||
(_) => stableArg
|
||||
], preSelector, {
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: import_react_redux.shallowEqual
|
||||
}
|
||||
})
|
||||
), [select, stableArg]);
|
||||
const querySelector = (0, import_react.useMemo)(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult, {
|
||||
devModeChecks: {
|
||||
identityFunctionCheck: "never"
|
||||
}
|
||||
}) : selectDefaultResult, [selectDefaultResult, selectFromResult]);
|
||||
const currentState = useSelector((state) => querySelector(state, lastValue.current), import_react_redux.shallowEqual);
|
||||
const store = useStore();
|
||||
const newLastValue = selectDefaultResult(store.getState(), lastValue.current);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
lastValue.current = newLastValue;
|
||||
}, [newLastValue]);
|
||||
return currentState;
|
||||
};
|
||||
return useQueryState;
|
||||
}
|
||||
function usePromiseRefUnsubscribeOnUnmount(promiseRef) {
|
||||
(0, import_react.useEffect)(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = void 0;
|
||||
};
|
||||
}, [promiseRef]);
|
||||
}
|
||||
function refetchOrErrorIfUnmounted(promiseRef) {
|
||||
if (!promiseRef.current) throw new Error(false ? _formatProdErrorMessage2(38) : "Cannot refetch a query that has not been started yet.");
|
||||
return promiseRef.current.refetch();
|
||||
}
|
||||
function buildQueryHooks(endpointName) {
|
||||
const useQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
return (0, import_react.useMemo)(() => ({
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch: () => refetchOrErrorIfUnmounted(promiseRef)
|
||||
}), [promiseRef]);
|
||||
};
|
||||
const useLazyQuerySubscription = ({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false
|
||||
} = {}) => {
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const [arg, setArg] = (0, import_react.useState)(UNINITIALIZED_VALUE);
|
||||
const promiseRef = (0, import_react.useRef)(void 0);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
usePossiblyImmediateEffect(() => {
|
||||
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
|
||||
if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
promiseRef.current?.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [stableSubscriptionOptions]);
|
||||
const subscriptionOptionsRef = (0, import_react.useRef)(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const trigger = (0, import_react.useCallback)(function(arg2, preferCacheValue = false) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
forceRefetch: !preferCacheValue
|
||||
}));
|
||||
setArg(arg2);
|
||||
});
|
||||
return promise;
|
||||
}, [dispatch, initiate]);
|
||||
const reset = (0, import_react.useCallback)(() => {
|
||||
if (promiseRef.current?.queryCacheKey) {
|
||||
dispatch(api.internalActions.removeQueryResult({
|
||||
queryCacheKey: promiseRef.current?.queryCacheKey
|
||||
}));
|
||||
}
|
||||
}, [dispatch]);
|
||||
(0, import_react.useEffect)(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
};
|
||||
}, []);
|
||||
(0, import_react.useEffect)(() => {
|
||||
if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) {
|
||||
trigger(arg, true);
|
||||
}
|
||||
}, [arg, trigger]);
|
||||
return (0, import_react.useMemo)(() => [trigger, arg, {
|
||||
reset
|
||||
}], [trigger, arg, reset]);
|
||||
};
|
||||
const useQueryState = buildUseQueryState(endpointName, queryStatePreSelector);
|
||||
return {
|
||||
useQueryState,
|
||||
useQuerySubscription,
|
||||
useLazyQuerySubscription,
|
||||
useLazyQuery(options) {
|
||||
const [trigger, arg, {
|
||||
reset
|
||||
}] = useLazyQuerySubscription(options);
|
||||
const queryStateResults = useQueryState(arg, {
|
||||
...options,
|
||||
skip: arg === UNINITIALIZED_VALUE
|
||||
});
|
||||
const info = (0, import_react.useMemo)(() => ({
|
||||
lastArg: arg
|
||||
}), [arg]);
|
||||
return (0, import_react.useMemo)(() => [trigger, {
|
||||
...queryStateResults,
|
||||
reset
|
||||
}, info], [trigger, queryStateResults, reset, info]);
|
||||
},
|
||||
useQuery(arg, options) {
|
||||
const querySubscriptionResults = useQuerySubscription(arg, options);
|
||||
const queryStateResults = useQueryState(arg, {
|
||||
selectFromResult: arg === import_query.skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
|
||||
...options
|
||||
});
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS);
|
||||
(0, import_react.useDebugValue)(debugValue);
|
||||
return (0, import_react.useMemo)(() => ({
|
||||
...queryStateResults,
|
||||
...querySubscriptionResults
|
||||
}), [queryStateResults, querySubscriptionResults]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildInfiniteQueryHooks(endpointName) {
|
||||
const useInfiniteQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef, dispatch, initiate, stableSubscriptionOptions] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
const subscriptionOptionsRef = (0, import_react.useRef)(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const hookRefetchCachedPages = options.refetchCachedPages;
|
||||
const stableHookRefetchCachedPages = useShallowStableValue(hookRefetchCachedPages);
|
||||
const trigger = (0, import_react.useCallback)(function(arg2, direction) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
direction
|
||||
}));
|
||||
});
|
||||
return promise;
|
||||
}, [promiseRef, dispatch, initiate]);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
const stableArg = useStableQueryArgs(options.skip ? import_query.skipToken : arg);
|
||||
const refetch = (0, import_react.useCallback)((options2) => {
|
||||
if (!promiseRef.current) throw new Error(false ? _formatProdErrorMessage3(38) : "Cannot refetch a query that has not been started yet.");
|
||||
const mergedOptions = {
|
||||
refetchCachedPages: options2?.refetchCachedPages ?? stableHookRefetchCachedPages
|
||||
};
|
||||
return promiseRef.current.refetch(mergedOptions);
|
||||
}, [promiseRef, stableHookRefetchCachedPages]);
|
||||
return (0, import_react.useMemo)(() => {
|
||||
const fetchNextPage = () => {
|
||||
return trigger(stableArg, "forward");
|
||||
};
|
||||
const fetchPreviousPage = () => {
|
||||
return trigger(stableArg, "backward");
|
||||
};
|
||||
return {
|
||||
trigger,
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
};
|
||||
}, [refetch, trigger, stableArg]);
|
||||
};
|
||||
const useInfiniteQueryState = buildUseQueryState(endpointName, infiniteQueryStatePreSelector);
|
||||
return {
|
||||
useInfiniteQueryState,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQuery(arg, options) {
|
||||
const {
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
} = useInfiniteQuerySubscription(arg, options);
|
||||
const queryStateResults = useInfiniteQueryState(arg, {
|
||||
selectFromResult: arg === import_query.skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
|
||||
...options
|
||||
});
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS, "hasNextPage", "hasPreviousPage");
|
||||
(0, import_react.useDebugValue)(debugValue);
|
||||
return (0, import_react.useMemo)(() => ({
|
||||
...queryStateResults,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage,
|
||||
refetch
|
||||
}), [queryStateResults, fetchNextPage, fetchPreviousPage, refetch]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildMutationHook(name) {
|
||||
return ({
|
||||
selectFromResult,
|
||||
fixedCacheKey
|
||||
} = {}) => {
|
||||
const {
|
||||
select,
|
||||
initiate
|
||||
} = api.endpoints[name];
|
||||
const dispatch = useDispatch();
|
||||
const [promise, setPromise] = (0, import_react.useState)();
|
||||
(0, import_react.useEffect)(() => () => {
|
||||
if (!promise?.arg.fixedCacheKey) {
|
||||
promise?.reset();
|
||||
}
|
||||
}, [promise]);
|
||||
const triggerMutation = (0, import_react.useCallback)(function(arg) {
|
||||
const promise2 = dispatch(initiate(arg, {
|
||||
fixedCacheKey
|
||||
}));
|
||||
setPromise(promise2);
|
||||
return promise2;
|
||||
}, [dispatch, initiate, fixedCacheKey]);
|
||||
const {
|
||||
requestId
|
||||
} = promise || {};
|
||||
const selectDefaultResult = (0, import_react.useMemo)(() => select({
|
||||
fixedCacheKey,
|
||||
requestId: promise?.requestId
|
||||
}), [fixedCacheKey, promise, select]);
|
||||
const mutationSelector = (0, import_react.useMemo)(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult) : selectDefaultResult, [selectFromResult, selectDefaultResult]);
|
||||
const currentState = useSelector(mutationSelector, import_react_redux.shallowEqual);
|
||||
const originalArgs = fixedCacheKey == null ? promise?.arg.originalArgs : void 0;
|
||||
const reset = (0, import_react.useCallback)(() => {
|
||||
batch(() => {
|
||||
if (promise) {
|
||||
setPromise(void 0);
|
||||
}
|
||||
if (fixedCacheKey) {
|
||||
dispatch(api.internalActions.removeMutationResult({
|
||||
requestId,
|
||||
fixedCacheKey
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [dispatch, fixedCacheKey, promise, requestId]);
|
||||
const debugValue = pick(currentState, ...COMMON_HOOK_DEBUG_FIELDS, "endpointName");
|
||||
(0, import_react.useDebugValue)(debugValue);
|
||||
const finalState = (0, import_react.useMemo)(() => ({
|
||||
...currentState,
|
||||
originalArgs,
|
||||
reset
|
||||
}), [currentState, originalArgs, reset]);
|
||||
return (0, import_react.useMemo)(() => [triggerMutation, finalState], [triggerMutation, finalState]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/query/react/module.ts
|
||||
var reactHooksModuleName = /* @__PURE__ */ Symbol();
|
||||
var reactHooksModule = ({
|
||||
batch = import_react_redux2.batch,
|
||||
hooks = {
|
||||
useDispatch: import_react_redux2.useDispatch,
|
||||
useSelector: import_react_redux2.useSelector,
|
||||
useStore: import_react_redux2.useStore
|
||||
},
|
||||
createSelector = import_reselect.createSelector,
|
||||
unstable__sideEffectsInRender = false,
|
||||
...rest
|
||||
} = {}) => {
|
||||
if (true) {
|
||||
const hookNames = ["useDispatch", "useSelector", "useStore"];
|
||||
let warned = false;
|
||||
for (const hookName of hookNames) {
|
||||
if (countObjectKeys(rest) > 0) {
|
||||
if (rest[hookName]) {
|
||||
if (!warned) {
|
||||
console.warn("As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`");
|
||||
warned = true;
|
||||
}
|
||||
}
|
||||
hooks[hookName] = rest[hookName];
|
||||
}
|
||||
if (typeof hooks[hookName] !== "function") {
|
||||
throw new Error(false ? _formatProdErrorMessage4(36) : `When using custom hooks for context, all ${hookNames.length} hooks need to be provided: ${hookNames.join(", ")}.
|
||||
Hook ${hookName} was either not provided or not a function.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: reactHooksModuleName,
|
||||
init(api, {
|
||||
serializeQueryArgs
|
||||
}, context) {
|
||||
const anyApi = api;
|
||||
const {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
} = buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks,
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
});
|
||||
safeAssign(anyApi, {
|
||||
usePrefetch
|
||||
});
|
||||
safeAssign(context, {
|
||||
batch
|
||||
});
|
||||
return {
|
||||
injectEndpoint(endpointName, definition) {
|
||||
if (isQueryDefinition(definition)) {
|
||||
const {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
} = buildQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Query`] = useQuery;
|
||||
api[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery;
|
||||
}
|
||||
if (isMutationDefinition(definition)) {
|
||||
const useMutation = buildMutationHook(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useMutation
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Mutation`] = useMutation;
|
||||
} else if (isInfiniteQueryDefinition(definition)) {
|
||||
const {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
} = buildInfiniteQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
});
|
||||
api[`use${capitalize(endpointName)}InfiniteQuery`] = useInfiniteQuery;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// src/query/react/index.ts
|
||||
__reExport(react_exports, require("@reduxjs/toolkit/query"), module.exports);
|
||||
|
||||
// src/query/react/ApiProvider.tsx
|
||||
var import_toolkit3 = require("@reduxjs/toolkit");
|
||||
var React = __toESM(require("react"));
|
||||
function ApiProvider(props) {
|
||||
const context = props.context || import_react_redux.ReactReduxContext;
|
||||
const existingContext = (0, import_react.useContext)(context);
|
||||
if (existingContext) {
|
||||
throw new Error(false ? _formatProdErrorMessage5(35) : "Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.");
|
||||
}
|
||||
const [store] = React.useState(() => (0, import_toolkit3.configureStore)({
|
||||
reducer: {
|
||||
[props.api.reducerPath]: props.api.reducer
|
||||
},
|
||||
middleware: (gDM) => gDM().concat(props.api.middleware)
|
||||
}));
|
||||
(0, import_react.useEffect)(() => props.setupListeners === false ? void 0 : (0, import_query.setupListeners)(store.dispatch, props.setupListeners), [props.setupListeners, store.dispatch]);
|
||||
return /* @__PURE__ */ React.createElement(import_react_redux.Provider, { store, context }, props.children);
|
||||
}
|
||||
|
||||
// src/query/react/index.ts
|
||||
var createApi = /* @__PURE__ */ (0, import_query.buildCreateApi)((0, import_query.coreModule)(), reactHooksModule());
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ApiProvider,
|
||||
UNINITIALIZED_VALUE,
|
||||
createApi,
|
||||
reactHooksModule,
|
||||
reactHooksModuleName,
|
||||
...require("@reduxjs/toolkit/query")
|
||||
});
|
||||
//# sourceMappingURL=rtk-query-react.development.cjs.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.development.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.development.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.production.min.cjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.production.min.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.production.min.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/cjs/rtk-query-react.production.min.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
980
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/index.d.mts
generated
vendored
Normal file
980
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/index.d.mts
generated
vendored
Normal file
@@ -0,0 +1,980 @@
|
||||
import * as _reduxjs_toolkit_query from '@reduxjs/toolkit/query';
|
||||
import { QueryDefinition, TSHelpersId, TSHelpersOverride, QuerySubState, ResultTypeFrom, QueryStatus, QueryArgFrom, SkipToken, SubscriptionOptions, TSHelpersNoInfer, QueryActionCreatorResult, MutationDefinition, MutationResultSelectorResult, MutationActionCreatorResult, InfiniteQueryDefinition, InfiniteQuerySubState, PageParamFrom, InfiniteQueryArgFrom, InfiniteQueryActionCreatorResult, BaseQueryFn, EndpointDefinitions, DefinitionType, QueryKeys, PrefetchOptions, Module, Api, setupListeners } from '@reduxjs/toolkit/query';
|
||||
export * from '@reduxjs/toolkit/query';
|
||||
import * as react_redux from 'react-redux';
|
||||
import { ReactReduxContextValue } from 'react-redux';
|
||||
import { CreateSelectorFunction } from 'reselect';
|
||||
import * as React from 'react';
|
||||
import { Context } from 'react';
|
||||
|
||||
type InfiniteData<DataType, PageParam> = {
|
||||
pages: Array<DataType>;
|
||||
pageParams: Array<PageParam>;
|
||||
};
|
||||
type InfiniteQueryDirection = 'forward' | 'backward';
|
||||
|
||||
export declare const UNINITIALIZED_VALUE: unique symbol;
|
||||
type UninitializedValue = typeof UNINITIALIZED_VALUE;
|
||||
|
||||
type QueryHooks<Definition extends QueryDefinition<any, any, any, any, any>> = {
|
||||
useQuery: UseQuery<Definition>;
|
||||
useLazyQuery: UseLazyQuery<Definition>;
|
||||
useQuerySubscription: UseQuerySubscription<Definition>;
|
||||
useLazyQuerySubscription: UseLazyQuerySubscription<Definition>;
|
||||
useQueryState: UseQueryState<Definition>;
|
||||
};
|
||||
type InfiniteQueryHooks<Definition extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
useInfiniteQuery: UseInfiniteQuery<Definition>;
|
||||
useInfiniteQuerySubscription: UseInfiniteQuerySubscription<Definition>;
|
||||
useInfiniteQueryState: UseInfiniteQueryState<Definition>;
|
||||
};
|
||||
type MutationHooks<Definition extends MutationDefinition<any, any, any, any, any>> = {
|
||||
useMutation: UseMutation<Definition>;
|
||||
};
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already, and the hook will return the data for that query arg once it's available.
|
||||
*
|
||||
* This hook combines the functionality of both [`useQueryState`](#usequerystate) and [`useQuerySubscription`](#usequerysubscription) together, and is intended to be used in the majority of situations.
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseQuery<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(arg: QueryArgFrom<D> | SkipToken, options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>) => UseQueryHookResult<D, R>;
|
||||
type TypedUseQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseQueryHookResult<D extends QueryDefinition<any, any, any, any>, R = UseQueryStateDefaultResult<D>> = UseQueryStateResult<D, R> & UseQuerySubscriptionResult<D>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuery` hook in userland code.
|
||||
*/
|
||||
type TypedUseQueryHookResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = TypedUseQueryStateResult<ResultType, QueryArg, BaseQuery, R> & TypedUseQuerySubscriptionResult<ResultType, QueryArg, BaseQuery>;
|
||||
type UseQuerySubscriptionOptions = SubscriptionOptions & {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When `skip` is true (or `skipToken` is passed in as `arg`):
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* Defaults to `false`. This setting allows you to control whether if a cached result is already available, RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
|
||||
* - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
|
||||
* - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
|
||||
* - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
|
||||
*
|
||||
* If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
|
||||
*/
|
||||
refetchOnMountOrArgChange?: boolean | number;
|
||||
};
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, and 'subscribes' the component to the cached data.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useQuery`](#usequery) or [`useQueryState`](#usequerystate).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
*/
|
||||
type UseQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (arg: QueryArgFrom<D> | SkipToken, options?: UseQuerySubscriptionOptions) => UseQuerySubscriptionResult<D>;
|
||||
type TypedUseQuerySubscription<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuerySubscription<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseQuerySubscriptionResult<D extends QueryDefinition<any, any, any, any>> = Pick<QueryActionCreatorResult<D>, 'refetch'>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuerySubscription` hook in userland code.
|
||||
*/
|
||||
type TypedUseQuerySubscriptionResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuerySubscriptionResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseLazyQueryLastPromiseInfo<D extends QueryDefinition<any, any, any, any>> = {
|
||||
lastArg: QueryArgFrom<D>;
|
||||
};
|
||||
/**
|
||||
* A React hook similar to [`useQuery`](#usequery), but with manual control over when the data fetching occurs.
|
||||
*
|
||||
* This hook includes the functionality of [`useLazyQuerySubscription`](#uselazyquerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to retrieve data
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
|
||||
*
|
||||
* #### Note
|
||||
*
|
||||
* When the trigger function returned from a LazyQuery is called, it always initiates a new request to the server even if there is cached data. Set `preferCacheValue`(the second argument to the function) as `true` if you want it to immediately return a cached value if one exists.
|
||||
*/
|
||||
type UseLazyQuery<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(options?: SubscriptionOptions & Omit<UseQueryStateOptions<D, R>, 'skip'>) => [
|
||||
LazyQueryTrigger<D>,
|
||||
UseLazyQueryStateResult<D, R>,
|
||||
UseLazyQueryLastPromiseInfo<D>
|
||||
];
|
||||
type TypedUseLazyQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseLazyQueryStateResult<D extends QueryDefinition<any, any, any, any>, R = UseQueryStateDefaultResult<D>> = UseQueryStateResult<D, R> & {
|
||||
/**
|
||||
* Resets the hook state to its initial `uninitialized` state.
|
||||
* This will also remove the last result from the cache.
|
||||
*/
|
||||
reset: () => void;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useLazyQuery` hook in userland code.
|
||||
*/
|
||||
type TypedUseLazyQueryStateResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseLazyQueryStateResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, R>;
|
||||
type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers a lazy query.
|
||||
*
|
||||
* By default, this will start a new request even if there is already a value in the cache.
|
||||
* If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
|
||||
*
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await getUserById(1).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>, preferCacheValue?: boolean): QueryActionCreatorResult<D>;
|
||||
};
|
||||
type TypedLazyQueryTrigger<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = LazyQueryTrigger<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook similar to [`useQuerySubscription`](#usequerysubscription), but with manual control over when the data fetching occurs.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useLazyQuery`](#uselazyquery).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to retrieve data
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
|
||||
*/
|
||||
type UseLazyQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (options?: SubscriptionOptions) => readonly [
|
||||
LazyQueryTrigger<D>,
|
||||
QueryArgFrom<D> | UninitializedValue,
|
||||
{
|
||||
reset: () => void;
|
||||
}
|
||||
];
|
||||
type TypedUseLazyQuerySubscription<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuerySubscription<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type QueryStateSelector<R extends Record<string, any>, D extends QueryDefinition<any, any, any, any>> = (state: UseQueryStateDefaultResult<D>) => R;
|
||||
/**
|
||||
* Provides a way to define a strongly-typed version of
|
||||
* {@linkcode QueryStateSelector} for use with a specific query.
|
||||
* This is useful for scenarios where you want to create a "pre-typed"
|
||||
* {@linkcode UseQueryStateOptions.selectFromResult | selectFromResult}
|
||||
* function.
|
||||
*
|
||||
* @example
|
||||
* <caption>#### __Create a strongly-typed `selectFromResult` selector function__</caption>
|
||||
*
|
||||
* ```tsx
|
||||
* import type { TypedQueryStateSelector } from '@reduxjs/toolkit/query/react'
|
||||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* type Post = {
|
||||
* id: number
|
||||
* title: string
|
||||
* }
|
||||
*
|
||||
* type PostsApiResponse = {
|
||||
* posts: Post[]
|
||||
* total: number
|
||||
* skip: number
|
||||
* limit: number
|
||||
* }
|
||||
*
|
||||
* type QueryArgument = number | undefined
|
||||
*
|
||||
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
|
||||
*
|
||||
* type SelectedResult = Pick<PostsApiResponse, 'posts'>
|
||||
*
|
||||
* const postsApiSlice = createApi({
|
||||
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
|
||||
* reducerPath: 'postsApi',
|
||||
* tagTypes: ['Posts'],
|
||||
* endpoints: (build) => ({
|
||||
* getPosts: build.query<PostsApiResponse, QueryArgument>({
|
||||
* query: (limit = 5) => `?limit=${limit}&select=title`,
|
||||
* }),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* const { useGetPostsQuery } = postsApiSlice
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* const { post } = useGetPostsQuery(undefined, {
|
||||
* selectFromResult: (state) => ({
|
||||
* post: state.data?.posts.find((post) => post.id === id),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* return <li>{post?.title}</li>
|
||||
* }
|
||||
*
|
||||
* const EMPTY_ARRAY: Post[] = []
|
||||
*
|
||||
* const typedSelectFromResult: TypedQueryStateSelector<
|
||||
* PostsApiResponse,
|
||||
* QueryArgument,
|
||||
* BaseQueryFunction,
|
||||
* SelectedResult
|
||||
* > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
|
||||
*
|
||||
* function PostsList() {
|
||||
* const { posts } = useGetPostsQuery(undefined, {
|
||||
* selectFromResult: typedSelectFromResult,
|
||||
* })
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <ul>
|
||||
* {posts.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template ResultType - The type of the result `data` returned by the query.
|
||||
* @template QueryArgumentType - The type of the argument passed into the query.
|
||||
* @template BaseQueryFunctionType - The type of the base query function being used.
|
||||
* @template SelectedResultType - The type of the selected result returned by the __`selectFromResult`__ function.
|
||||
*
|
||||
* @since 2.3.0
|
||||
* @public
|
||||
*/
|
||||
type TypedQueryStateSelector<ResultType, QueryArgumentType, BaseQueryFunctionType extends BaseQueryFn, SelectedResultType extends Record<string, any> = UseQueryStateDefaultResult<QueryDefinition<QueryArgumentType, BaseQueryFunctionType, string, ResultType, string>>> = QueryStateSelector<SelectedResultType, QueryDefinition<QueryArgumentType, BaseQueryFunctionType, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* Note that this hook does not trigger fetching new data. For that use-case, see [`useQuery`](#usequery) or [`useQuerySubscription`](#usequerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseQueryState<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(arg: QueryArgFrom<D> | SkipToken, options?: UseQueryStateOptions<D, R>) => UseQueryStateResult<D, R>;
|
||||
type TypedUseQueryState<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQueryState<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type UseQueryStateOptions<D extends QueryDefinition<any, any, any, any>, R extends Record<string, any>> = {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When skip is true:
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after skipping the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* `selectFromResult` allows you to get a specific segment from a query result in a performant manner.
|
||||
* When using this feature, the component will not rerender unless the underlying data of the selected item has changed.
|
||||
* If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using selectFromResult to extract a single result"
|
||||
* function PostsList() {
|
||||
* const { data: posts } = api.useGetPostsQuery();
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {posts?.data?.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* // Will select the post with the given id, and will only rerender if the given posts data changes
|
||||
* const { post } = api.useGetPostsQuery(undefined, {
|
||||
* selectFromResult: ({ data }) => ({ post: data?.find((post) => post.id === id) }),
|
||||
* });
|
||||
*
|
||||
* return <li>{post?.name}</li>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
selectFromResult?: QueryStateSelector<R, D>;
|
||||
};
|
||||
/**
|
||||
* Provides a way to define a "pre-typed" version of
|
||||
* {@linkcode UseQueryStateOptions} with specific options for a given query.
|
||||
* This is particularly useful for setting default query behaviors such as
|
||||
* refetching strategies, which can be overridden as needed.
|
||||
*
|
||||
* @example
|
||||
* <caption>#### __Create a `useQuery` hook with default options__</caption>
|
||||
*
|
||||
* ```ts
|
||||
* import type {
|
||||
* SubscriptionOptions,
|
||||
* TypedUseQueryStateOptions,
|
||||
* } from '@reduxjs/toolkit/query/react'
|
||||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* type Post = {
|
||||
* id: number
|
||||
* name: string
|
||||
* }
|
||||
*
|
||||
* const api = createApi({
|
||||
* baseQuery: fetchBaseQuery({ baseUrl: '/' }),
|
||||
* tagTypes: ['Post'],
|
||||
* endpoints: (build) => ({
|
||||
* getPosts: build.query<Post[], void>({
|
||||
* query: () => 'posts',
|
||||
* }),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* const { useGetPostsQuery } = api
|
||||
*
|
||||
* export const useGetPostsQueryWithDefaults = <
|
||||
* SelectedResult extends Record<string, any>,
|
||||
* >(
|
||||
* overrideOptions: TypedUseQueryStateOptions<
|
||||
* Post[],
|
||||
* void,
|
||||
* ReturnType<typeof fetchBaseQuery>,
|
||||
* SelectedResult
|
||||
* > &
|
||||
* SubscriptionOptions,
|
||||
* ) =>
|
||||
* useGetPostsQuery(undefined, {
|
||||
* // Insert default options here
|
||||
*
|
||||
* refetchOnMountOrArgChange: true,
|
||||
* refetchOnFocus: true,
|
||||
* ...overrideOptions,
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @template ResultType - The type of the result `data` returned by the query.
|
||||
* @template QueryArg - The type of the argument passed into the query.
|
||||
* @template BaseQuery - The type of the base query function being used.
|
||||
* @template SelectedResult - The type of the selected result returned by the __`selectFromResult`__ function.
|
||||
*
|
||||
* @since 2.2.8
|
||||
* @public
|
||||
*/
|
||||
type TypedUseQueryStateOptions<ResultType, QueryArg, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseQueryStateOptions<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, SelectedResult>;
|
||||
type UseQueryStateResult<_ extends QueryDefinition<any, any, any, any>, R> = TSHelpersNoInfer<R>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQueryState` hook in userland code.
|
||||
*/
|
||||
type TypedUseQueryStateResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = TSHelpersNoInfer<R>;
|
||||
type UseQueryStateBaseResult<D extends QueryDefinition<any, any, any, any>> = QuerySubState<D> & {
|
||||
/**
|
||||
* Where `data` tries to hold data as much as possible, also re-using
|
||||
* data from the last arguments passed into the hook, this property
|
||||
* will always contain the received data from the query, for the current query arguments.
|
||||
*/
|
||||
currentData?: ResultTypeFrom<D>;
|
||||
/**
|
||||
* Query has not started yet.
|
||||
*/
|
||||
isUninitialized: false;
|
||||
/**
|
||||
* Query is currently loading for the first time. No data yet.
|
||||
*/
|
||||
isLoading: false;
|
||||
/**
|
||||
* Query is currently fetching, but might have data from an earlier request.
|
||||
*/
|
||||
isFetching: false;
|
||||
/**
|
||||
* Query has data from a successful load.
|
||||
*/
|
||||
isSuccess: false;
|
||||
/**
|
||||
* Query is currently in "error" state.
|
||||
*/
|
||||
isError: false;
|
||||
};
|
||||
type UseQueryStateUninitialized<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<Extract<UseQueryStateBaseResult<D>, {
|
||||
status: QueryStatus.uninitialized;
|
||||
}>, {
|
||||
isUninitialized: true;
|
||||
}>;
|
||||
type UseQueryStateLoading<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isLoading: true;
|
||||
isFetching: boolean;
|
||||
data: undefined;
|
||||
}>;
|
||||
type UseQueryStateSuccessFetching<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isSuccess: true;
|
||||
isFetching: true;
|
||||
error: undefined;
|
||||
} & {
|
||||
data: ResultTypeFrom<D>;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'fulfilledTimeStamp'>>>;
|
||||
type UseQueryStateSuccessNotFetching<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isSuccess: true;
|
||||
isFetching: false;
|
||||
error: undefined;
|
||||
} & {
|
||||
data: ResultTypeFrom<D>;
|
||||
currentData: ResultTypeFrom<D>;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'fulfilledTimeStamp'>>>;
|
||||
type UseQueryStateError<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isError: true;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'error'>>>;
|
||||
type UseQueryStateDefaultResult<D extends QueryDefinition<any, any, any, any>> = TSHelpersId<UseQueryStateUninitialized<D> | UseQueryStateLoading<D> | UseQueryStateSuccessFetching<D> | UseQueryStateSuccessNotFetching<D> | UseQueryStateError<D>> & {
|
||||
/**
|
||||
* @deprecated Included for completeness, but discouraged.
|
||||
* Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
|
||||
* and `isUninitialized` flags instead
|
||||
*/
|
||||
status: QueryStatus;
|
||||
};
|
||||
type LazyInfiniteQueryTrigger<D extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers a lazy query.
|
||||
*
|
||||
* By default, this will start a new request even if there is already a value in the cache.
|
||||
* If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
|
||||
*
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await getUserById(1).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>, direction: InfiniteQueryDirection): InfiniteQueryActionCreatorResult<D>;
|
||||
};
|
||||
type TypedLazyInfiniteQueryTrigger<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = LazyInfiniteQueryTrigger<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type UseInfiniteQuerySubscriptionOptions<D extends InfiniteQueryDefinition<any, any, any, any, any>> = SubscriptionOptions & {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When `skip` is true (or `skipToken` is passed in as `arg`):
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* Defaults to `false`. This setting allows you to control whether if a cached result is already available, RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
|
||||
* - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
|
||||
* - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
|
||||
* - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
|
||||
*
|
||||
* If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
|
||||
*/
|
||||
refetchOnMountOrArgChange?: boolean | number;
|
||||
initialPageParam?: PageParamFrom<D>;
|
||||
/**
|
||||
* Defaults to `true`. When this is `true` and an infinite query endpoint is refetched
|
||||
* (due to tag invalidation, polling, arg change configuration, or manual refetching),
|
||||
* RTK Query will try to sequentially refetch all pages currently in the cache.
|
||||
* When `false` only the first page will be refetched.
|
||||
*
|
||||
* This option applies to all automatic refetches for this subscription (polling, tag invalidation, etc.).
|
||||
* It can be overridden on a per-call basis using the `refetch()` method.
|
||||
*/
|
||||
refetchCachedPages?: boolean;
|
||||
};
|
||||
type TypedUseInfiniteQuerySubscription<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuerySubscription<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type UseInfiniteQuerySubscriptionResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
refetch: (options?: Pick<UseInfiniteQuerySubscriptionOptions<D>, 'refetchCachedPages'>) => InfiniteQueryActionCreatorResult<D>;
|
||||
trigger: LazyInfiniteQueryTrigger<D>;
|
||||
fetchNextPage: () => InfiniteQueryActionCreatorResult<D>;
|
||||
fetchPreviousPage: () => InfiniteQueryActionCreatorResult<D>;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuerySubscription` hook in userland code.
|
||||
*/
|
||||
type TypedUseInfiniteQuerySubscriptionResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuerySubscriptionResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type InfiniteQueryStateSelector<R extends Record<string, any>, D extends InfiniteQueryDefinition<any, any, any, any, any>> = (state: UseInfiniteQueryStateDefaultResult<D>) => R;
|
||||
type TypedInfiniteQueryStateSelector<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = InfiniteQueryStateSelector<SelectedResult, InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available. Additionally, it will cache multiple "pages" worth of responses within a single cache entry, and allows fetching more pages forwards and backwards from the current cached pages.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already, and the hook will return the data for that query arg once it's available.
|
||||
*
|
||||
* The `data` field will be a `{pages: Data[], pageParams: PageParam[]}` structure containing all fetched page responses and the corresponding page param values for each page. You may use this to render individual pages, combine all pages into a single infinite list, or other display logic as needed.
|
||||
*
|
||||
* This hook combines the functionality of both [`useInfiniteQueryState`](#useinfinitequerystate) and [`useInfiniteQuerySubscription`](#useinfinitequerysubscription) together, and is intended to be used in the majority of situations.
|
||||
*
|
||||
* As with normal query hooks, `skipToken` is a valid argument that will skip the query from executing.
|
||||
*
|
||||
* By default, the initial request will use the `initialPageParam` value that was defined on the infinite query endpoint. If you want to start from a different value, you can pass `initialPageParam` as part of the hook options to override that initial request value.
|
||||
*
|
||||
* Use the returned `fetchNextPage` and `fetchPreviousPage` methods on the hook result object to trigger fetches forwards and backwards. These will always calculate the next or previous page param based on the current cached pages and the provided `getNext/PreviousPageParam` callbacks defined in the endpoint.
|
||||
*
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Caches multiple pages worth of responses, and provides methods to trigger more page fetches forwards and backwards
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseInfiniteQuery<D extends InfiniteQueryDefinition<any, any, any, any, any>> = <R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<D>>(arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQuerySubscriptionOptions<D> & UseInfiniteQueryStateOptions<D, R>) => UseInfiniteQueryHookResult<D, R> & Pick<UseInfiniteQuerySubscriptionResult<D>, 'fetchNextPage' | 'fetchPreviousPage'>;
|
||||
type TypedUseInfiniteQuery<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuery<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* Note that this hook does not trigger fetching new data. For that use-case, see [`useInfiniteQuery`](#useinfinitequery) or [`useInfiniteQuerySubscription`](#useinfinitequerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseInfiniteQueryState<D extends InfiniteQueryDefinition<any, any, any, any, any>> = <R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<D>>(arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQueryStateOptions<D, R>) => UseInfiniteQueryStateResult<D, R>;
|
||||
type TypedUseInfiniteQueryState<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQueryState<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, and 'subscribes' the component to the cached data. Additionally, it will cache multiple "pages" worth of responses within a single cache entry, and allows fetching more pages forwards and backwards from the current cached pages.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useInfiniteQuery`](#useinfinitequery) or [`useInfiniteQueryState`](#useinfinitequerystate).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Caches multiple pages worth of responses, and provides methods to trigger more page fetches forwards and backwards
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
*/
|
||||
type UseInfiniteQuerySubscription<D extends InfiniteQueryDefinition<any, any, any, any, any>> = (arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQuerySubscriptionOptions<D>) => UseInfiniteQuerySubscriptionResult<D>;
|
||||
type UseInfiniteQueryHookResult<D extends InfiniteQueryDefinition<any, any, any, any, any>, R = UseInfiniteQueryStateDefaultResult<D>> = UseInfiniteQueryStateResult<D, R> & Pick<UseInfiniteQuerySubscriptionResult<D>, 'refetch' | 'fetchNextPage' | 'fetchPreviousPage'>;
|
||||
type TypedUseInfiniteQueryHookResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryHookResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, R>;
|
||||
type UseInfiniteQueryStateOptions<D extends InfiniteQueryDefinition<any, any, any, any, any>, R extends Record<string, any>> = {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When skip is true:
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after skipping the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* `selectFromResult` allows you to get a specific segment from a query result in a performant manner.
|
||||
* When using this feature, the component will not rerender unless the underlying data of the selected item has changed.
|
||||
* If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection.
|
||||
* Note that this should always return an object (not a primitive), as RTKQ adds fields to the return value.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using selectFromResult to extract a single result"
|
||||
* function PostsList() {
|
||||
* const { data: posts } = api.useGetPostsQuery();
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {posts?.data?.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* // Will select the post with the given id, and will only rerender if the given posts data changes
|
||||
* const { post } = api.useGetPostsQuery(undefined, {
|
||||
* selectFromResult: ({ data }) => ({ post: data?.find((post) => post.id === id) }),
|
||||
* });
|
||||
*
|
||||
* return <li>{post?.name}</li>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
selectFromResult?: InfiniteQueryStateSelector<R, D>;
|
||||
};
|
||||
type TypedUseInfiniteQueryStateOptions<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryStateOptions<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, SelectedResult>;
|
||||
type UseInfiniteQueryStateResult<D extends InfiniteQueryDefinition<any, any, any, any, any>, R = UseInfiniteQueryStateDefaultResult<D>> = TSHelpersNoInfer<R>;
|
||||
type TypedUseInfiniteQueryStateResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, R = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryStateResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, R>;
|
||||
type UseInfiniteQueryStateBaseResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = InfiniteQuerySubState<D> & {
|
||||
/**
|
||||
* Where `data` tries to hold data as much as possible, also re-using
|
||||
* data from the last arguments passed into the hook, this property
|
||||
* will always contain the received data from the query, for the current query arguments.
|
||||
*/
|
||||
currentData?: InfiniteData<ResultTypeFrom<D>, PageParamFrom<D>>;
|
||||
/**
|
||||
* Query has not started yet.
|
||||
*/
|
||||
isUninitialized: false;
|
||||
/**
|
||||
* Query is currently loading for the first time. No data yet.
|
||||
*/
|
||||
isLoading: false;
|
||||
/**
|
||||
* Query is currently fetching, but might have data from an earlier request.
|
||||
*/
|
||||
isFetching: false;
|
||||
/**
|
||||
* Query has data from a successful load.
|
||||
*/
|
||||
isSuccess: false;
|
||||
/**
|
||||
* Query is currently in "error" state.
|
||||
*/
|
||||
isError: false;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
isFetchingPreviousPage: boolean;
|
||||
};
|
||||
type UseInfiniteQueryStateDefaultResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = TSHelpersId<TSHelpersOverride<Extract<UseInfiniteQueryStateBaseResult<D>, {
|
||||
status: QueryStatus.uninitialized;
|
||||
}>, {
|
||||
isUninitialized: true;
|
||||
}> | TSHelpersOverride<UseInfiniteQueryStateBaseResult<D>, {
|
||||
isLoading: true;
|
||||
isFetching: boolean;
|
||||
data: undefined;
|
||||
} | ({
|
||||
isSuccess: true;
|
||||
isFetching: true;
|
||||
error: undefined;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'data' | 'fulfilledTimeStamp'>>) | ({
|
||||
isSuccess: true;
|
||||
isFetching: false;
|
||||
error: undefined;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'data' | 'fulfilledTimeStamp' | 'currentData'>>) | ({
|
||||
isError: true;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'error'>>)>> & {
|
||||
/**
|
||||
* @deprecated Included for completeness, but discouraged.
|
||||
* Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
|
||||
* and `isUninitialized` flags instead
|
||||
*/
|
||||
status: QueryStatus;
|
||||
};
|
||||
type MutationStateSelector<R extends Record<string, any>, D extends MutationDefinition<any, any, any, any>> = (state: MutationResultSelectorResult<D>) => R;
|
||||
type UseMutationStateOptions<D extends MutationDefinition<any, any, any, any>, R extends Record<string, any>> = {
|
||||
selectFromResult?: MutationStateSelector<R, D>;
|
||||
fixedCacheKey?: string;
|
||||
};
|
||||
type UseMutationStateResult<D extends MutationDefinition<any, any, any, any>, R> = TSHelpersNoInfer<R> & {
|
||||
originalArgs?: QueryArgFrom<D>;
|
||||
/**
|
||||
* Resets the hook state to its initial `uninitialized` state.
|
||||
* This will also remove the last result from the cache.
|
||||
*/
|
||||
reset: () => void;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useMutation` hook in userland code.
|
||||
*/
|
||||
type TypedUseMutationResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = MutationResultSelectorResult<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseMutationStateResult<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>, R>;
|
||||
/**
|
||||
* A React hook that lets you trigger an update request for a given endpoint, and subscribes the component to read the request status from the Redux store. The component will re-render as the loading status changes.
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to alter data on the server or possibly invalidate the cache
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseMutation<D extends MutationDefinition<any, any, any, any>> = <R extends Record<string, any> = MutationResultSelectorResult<D>>(options?: UseMutationStateOptions<D, R>) => readonly [MutationTrigger<D>, UseMutationStateResult<D, R>];
|
||||
type TypedUseMutation<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseMutation<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type MutationTrigger<D extends MutationDefinition<any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers the mutation and returns a Promise.
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>): MutationActionCreatorResult<D>;
|
||||
};
|
||||
type TypedMutationTrigger<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = MutationTrigger<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
|
||||
type QueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.query;
|
||||
} ? `use${Capitalize<K & string>}Query` : never]: UseQuery<Extract<Definitions[K], QueryDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type LazyQueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.query;
|
||||
} ? `useLazy${Capitalize<K & string>}Query` : never]: UseLazyQuery<Extract<Definitions[K], QueryDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type InfiniteQueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.infinitequery;
|
||||
} ? `use${Capitalize<K & string>}InfiniteQuery` : never]: UseInfiniteQuery<Extract<Definitions[K], InfiniteQueryDefinition<any, any, any, any, any>>>;
|
||||
};
|
||||
type MutationHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.mutation;
|
||||
} ? `use${Capitalize<K & string>}Mutation` : never]: UseMutation<Extract<Definitions[K], MutationDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type HooksWithUniqueNames<Definitions extends EndpointDefinitions> = QueryHookNames<Definitions> & LazyQueryHookNames<Definitions> & InfiniteQueryHookNames<Definitions> & MutationHookNames<Definitions>;
|
||||
|
||||
export declare const reactHooksModuleName: unique symbol;
|
||||
type ReactHooksModule = typeof reactHooksModuleName;
|
||||
declare module '@reduxjs/toolkit/query' {
|
||||
interface ApiModules<BaseQuery extends BaseQueryFn, Definitions extends EndpointDefinitions, ReducerPath extends string, TagTypes extends string> {
|
||||
[reactHooksModuleName]: {
|
||||
/**
|
||||
* Endpoints based on the input endpoints provided to `createApi`, containing `select`, `hooks` and `action matchers`.
|
||||
*/
|
||||
endpoints: {
|
||||
[K in keyof Definitions]: Definitions[K] extends QueryDefinition<any, any, any, any, any> ? QueryHooks<Definitions[K]> : Definitions[K] extends MutationDefinition<any, any, any, any, any> ? MutationHooks<Definitions[K]> : Definitions[K] extends InfiniteQueryDefinition<any, any, any, any, any> ? InfiniteQueryHooks<Definitions[K]> : never;
|
||||
};
|
||||
/**
|
||||
* A hook that accepts a string endpoint name, and provides a callback that when called, pre-fetches the data for that endpoint.
|
||||
*/
|
||||
usePrefetch<EndpointName extends QueryKeys<Definitions>>(endpointName: EndpointName, options?: PrefetchOptions): (arg: QueryArgFrom<Definitions[EndpointName]>, options?: PrefetchOptions) => void;
|
||||
} & HooksWithUniqueNames<Definitions>;
|
||||
}
|
||||
}
|
||||
type RR = typeof react_redux;
|
||||
interface ReactHooksModuleOptions {
|
||||
/**
|
||||
* The hooks from React Redux to be used
|
||||
*/
|
||||
hooks?: {
|
||||
/**
|
||||
* The version of the `useDispatch` hook to be used
|
||||
*/
|
||||
useDispatch: RR['useDispatch'];
|
||||
/**
|
||||
* The version of the `useSelector` hook to be used
|
||||
*/
|
||||
useSelector: RR['useSelector'];
|
||||
/**
|
||||
* The version of the `useStore` hook to be used
|
||||
*/
|
||||
useStore: RR['useStore'];
|
||||
};
|
||||
/**
|
||||
* The version of the `batchedUpdates` function to be used
|
||||
*/
|
||||
batch?: RR['batch'];
|
||||
/**
|
||||
* Enables performing asynchronous tasks immediately within a render.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* buildCreateApi,
|
||||
* coreModule,
|
||||
* reactHooksModule
|
||||
* } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* const createApi = buildCreateApi(
|
||||
* coreModule(),
|
||||
* reactHooksModule({ unstable__sideEffectsInRender: true })
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
unstable__sideEffectsInRender?: boolean;
|
||||
/**
|
||||
* A selector creator (usually from `reselect`, or matching the same signature)
|
||||
*/
|
||||
createSelector?: CreateSelectorFunction<any, any, any>;
|
||||
}
|
||||
/**
|
||||
* Creates a module that generates react hooks from endpoints, for use with `buildCreateApi`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const MyContext = React.createContext<ReactReduxContextValue | null>(null);
|
||||
* const customCreateApi = buildCreateApi(
|
||||
* coreModule(),
|
||||
* reactHooksModule({
|
||||
* hooks: {
|
||||
* useDispatch: createDispatchHook(MyContext),
|
||||
* useSelector: createSelectorHook(MyContext),
|
||||
* useStore: createStoreHook(MyContext)
|
||||
* }
|
||||
* })
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @returns A module for use with `buildCreateApi`
|
||||
*/
|
||||
declare const reactHooksModule: ({ batch, hooks, createSelector, unstable__sideEffectsInRender, ...rest }?: ReactHooksModuleOptions) => Module<ReactHooksModule>;
|
||||
|
||||
/**
|
||||
* Can be used as a `Provider` if you **do not already have a Redux store**.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Basic usage - wrap your App with ApiProvider"
|
||||
* import * as React from 'react';
|
||||
* import { ApiProvider } from '@reduxjs/toolkit/query/react';
|
||||
* import { Pokemon } from './features/Pokemon';
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <ApiProvider api={api}>
|
||||
* <Pokemon />
|
||||
* </ApiProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* Using this together with an existing redux store, both will
|
||||
* conflict with each other - please use the traditional redux setup
|
||||
* in that case.
|
||||
*/
|
||||
declare function ApiProvider(props: {
|
||||
children: any;
|
||||
api: Api<any, {}, any, any>;
|
||||
setupListeners?: Parameters<typeof setupListeners>[1] | false;
|
||||
context?: Context<ReactReduxContextValue | null>;
|
||||
}): React.JSX.Element;
|
||||
|
||||
declare const createApi: _reduxjs_toolkit_query.CreateApi<typeof _reduxjs_toolkit_query.coreModuleName | typeof reactHooksModuleName>;
|
||||
|
||||
export { ApiProvider, type TypedInfiniteQueryStateSelector, type TypedLazyInfiniteQueryTrigger, type TypedLazyQueryTrigger, type TypedMutationTrigger, type TypedQueryStateSelector, type TypedUseInfiniteQuery, type TypedUseInfiniteQueryHookResult, type TypedUseInfiniteQueryState, type TypedUseInfiniteQueryStateOptions, type TypedUseInfiniteQueryStateResult, type TypedUseInfiniteQuerySubscription, type TypedUseInfiniteQuerySubscriptionResult, type TypedUseLazyQuery, type TypedUseLazyQueryStateResult, type TypedUseLazyQuerySubscription, type TypedUseMutation, type TypedUseMutationResult, type TypedUseQuery, type TypedUseQueryHookResult, type TypedUseQueryState, type TypedUseQueryStateOptions, type TypedUseQueryStateResult, type TypedUseQuerySubscription, type TypedUseQuerySubscriptionResult, createApi, reactHooksModule };
|
||||
980
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/index.d.ts
generated
vendored
Normal file
980
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,980 @@
|
||||
import * as _reduxjs_toolkit_query from '@reduxjs/toolkit/query';
|
||||
import { QueryDefinition, TSHelpersId, TSHelpersOverride, QuerySubState, ResultTypeFrom, QueryStatus, QueryArgFrom, SkipToken, SubscriptionOptions, TSHelpersNoInfer, QueryActionCreatorResult, MutationDefinition, MutationResultSelectorResult, MutationActionCreatorResult, InfiniteQueryDefinition, InfiniteQuerySubState, PageParamFrom, InfiniteQueryArgFrom, InfiniteQueryActionCreatorResult, BaseQueryFn, EndpointDefinitions, DefinitionType, QueryKeys, PrefetchOptions, Module, Api, setupListeners } from '@reduxjs/toolkit/query';
|
||||
export * from '@reduxjs/toolkit/query';
|
||||
import * as react_redux from 'react-redux';
|
||||
import { ReactReduxContextValue } from 'react-redux';
|
||||
import { CreateSelectorFunction } from 'reselect';
|
||||
import * as React from 'react';
|
||||
import { Context } from 'react';
|
||||
|
||||
type InfiniteData<DataType, PageParam> = {
|
||||
pages: Array<DataType>;
|
||||
pageParams: Array<PageParam>;
|
||||
};
|
||||
type InfiniteQueryDirection = 'forward' | 'backward';
|
||||
|
||||
export declare const UNINITIALIZED_VALUE: unique symbol;
|
||||
type UninitializedValue = typeof UNINITIALIZED_VALUE;
|
||||
|
||||
type QueryHooks<Definition extends QueryDefinition<any, any, any, any, any>> = {
|
||||
useQuery: UseQuery<Definition>;
|
||||
useLazyQuery: UseLazyQuery<Definition>;
|
||||
useQuerySubscription: UseQuerySubscription<Definition>;
|
||||
useLazyQuerySubscription: UseLazyQuerySubscription<Definition>;
|
||||
useQueryState: UseQueryState<Definition>;
|
||||
};
|
||||
type InfiniteQueryHooks<Definition extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
useInfiniteQuery: UseInfiniteQuery<Definition>;
|
||||
useInfiniteQuerySubscription: UseInfiniteQuerySubscription<Definition>;
|
||||
useInfiniteQueryState: UseInfiniteQueryState<Definition>;
|
||||
};
|
||||
type MutationHooks<Definition extends MutationDefinition<any, any, any, any, any>> = {
|
||||
useMutation: UseMutation<Definition>;
|
||||
};
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already, and the hook will return the data for that query arg once it's available.
|
||||
*
|
||||
* This hook combines the functionality of both [`useQueryState`](#usequerystate) and [`useQuerySubscription`](#usequerysubscription) together, and is intended to be used in the majority of situations.
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseQuery<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(arg: QueryArgFrom<D> | SkipToken, options?: UseQuerySubscriptionOptions & UseQueryStateOptions<D, R>) => UseQueryHookResult<D, R>;
|
||||
type TypedUseQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseQueryHookResult<D extends QueryDefinition<any, any, any, any>, R = UseQueryStateDefaultResult<D>> = UseQueryStateResult<D, R> & UseQuerySubscriptionResult<D>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuery` hook in userland code.
|
||||
*/
|
||||
type TypedUseQueryHookResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = TypedUseQueryStateResult<ResultType, QueryArg, BaseQuery, R> & TypedUseQuerySubscriptionResult<ResultType, QueryArg, BaseQuery>;
|
||||
type UseQuerySubscriptionOptions = SubscriptionOptions & {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When `skip` is true (or `skipToken` is passed in as `arg`):
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* Defaults to `false`. This setting allows you to control whether if a cached result is already available, RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
|
||||
* - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
|
||||
* - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
|
||||
* - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
|
||||
*
|
||||
* If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
|
||||
*/
|
||||
refetchOnMountOrArgChange?: boolean | number;
|
||||
};
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, and 'subscribes' the component to the cached data.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useQuery`](#usequery) or [`useQueryState`](#usequerystate).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
*/
|
||||
type UseQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (arg: QueryArgFrom<D> | SkipToken, options?: UseQuerySubscriptionOptions) => UseQuerySubscriptionResult<D>;
|
||||
type TypedUseQuerySubscription<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuerySubscription<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseQuerySubscriptionResult<D extends QueryDefinition<any, any, any, any>> = Pick<QueryActionCreatorResult<D>, 'refetch'>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuerySubscription` hook in userland code.
|
||||
*/
|
||||
type TypedUseQuerySubscriptionResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQuerySubscriptionResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseLazyQueryLastPromiseInfo<D extends QueryDefinition<any, any, any, any>> = {
|
||||
lastArg: QueryArgFrom<D>;
|
||||
};
|
||||
/**
|
||||
* A React hook similar to [`useQuery`](#usequery), but with manual control over when the data fetching occurs.
|
||||
*
|
||||
* This hook includes the functionality of [`useLazyQuerySubscription`](#uselazyquerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to retrieve data
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
|
||||
*
|
||||
* #### Note
|
||||
*
|
||||
* When the trigger function returned from a LazyQuery is called, it always initiates a new request to the server even if there is cached data. Set `preferCacheValue`(the second argument to the function) as `true` if you want it to immediately return a cached value if one exists.
|
||||
*/
|
||||
type UseLazyQuery<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(options?: SubscriptionOptions & Omit<UseQueryStateOptions<D, R>, 'skip'>) => [
|
||||
LazyQueryTrigger<D>,
|
||||
UseLazyQueryStateResult<D, R>,
|
||||
UseLazyQueryLastPromiseInfo<D>
|
||||
];
|
||||
type TypedUseLazyQuery<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuery<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type UseLazyQueryStateResult<D extends QueryDefinition<any, any, any, any>, R = UseQueryStateDefaultResult<D>> = UseQueryStateResult<D, R> & {
|
||||
/**
|
||||
* Resets the hook state to its initial `uninitialized` state.
|
||||
* This will also remove the last result from the cache.
|
||||
*/
|
||||
reset: () => void;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useLazyQuery` hook in userland code.
|
||||
*/
|
||||
type TypedUseLazyQueryStateResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseLazyQueryStateResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, R>;
|
||||
type LazyQueryTrigger<D extends QueryDefinition<any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers a lazy query.
|
||||
*
|
||||
* By default, this will start a new request even if there is already a value in the cache.
|
||||
* If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
|
||||
*
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await getUserById(1).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>, preferCacheValue?: boolean): QueryActionCreatorResult<D>;
|
||||
};
|
||||
type TypedLazyQueryTrigger<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = LazyQueryTrigger<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook similar to [`useQuerySubscription`](#usequerysubscription), but with manual control over when the data fetching occurs.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useLazyQuery`](#uselazyquery).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to retrieve data
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met and the fetch has been manually called at least once
|
||||
*/
|
||||
type UseLazyQuerySubscription<D extends QueryDefinition<any, any, any, any>> = (options?: SubscriptionOptions) => readonly [
|
||||
LazyQueryTrigger<D>,
|
||||
QueryArgFrom<D> | UninitializedValue,
|
||||
{
|
||||
reset: () => void;
|
||||
}
|
||||
];
|
||||
type TypedUseLazyQuerySubscription<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseLazyQuerySubscription<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type QueryStateSelector<R extends Record<string, any>, D extends QueryDefinition<any, any, any, any>> = (state: UseQueryStateDefaultResult<D>) => R;
|
||||
/**
|
||||
* Provides a way to define a strongly-typed version of
|
||||
* {@linkcode QueryStateSelector} for use with a specific query.
|
||||
* This is useful for scenarios where you want to create a "pre-typed"
|
||||
* {@linkcode UseQueryStateOptions.selectFromResult | selectFromResult}
|
||||
* function.
|
||||
*
|
||||
* @example
|
||||
* <caption>#### __Create a strongly-typed `selectFromResult` selector function__</caption>
|
||||
*
|
||||
* ```tsx
|
||||
* import type { TypedQueryStateSelector } from '@reduxjs/toolkit/query/react'
|
||||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* type Post = {
|
||||
* id: number
|
||||
* title: string
|
||||
* }
|
||||
*
|
||||
* type PostsApiResponse = {
|
||||
* posts: Post[]
|
||||
* total: number
|
||||
* skip: number
|
||||
* limit: number
|
||||
* }
|
||||
*
|
||||
* type QueryArgument = number | undefined
|
||||
*
|
||||
* type BaseQueryFunction = ReturnType<typeof fetchBaseQuery>
|
||||
*
|
||||
* type SelectedResult = Pick<PostsApiResponse, 'posts'>
|
||||
*
|
||||
* const postsApiSlice = createApi({
|
||||
* baseQuery: fetchBaseQuery({ baseUrl: 'https://dummyjson.com/posts' }),
|
||||
* reducerPath: 'postsApi',
|
||||
* tagTypes: ['Posts'],
|
||||
* endpoints: (build) => ({
|
||||
* getPosts: build.query<PostsApiResponse, QueryArgument>({
|
||||
* query: (limit = 5) => `?limit=${limit}&select=title`,
|
||||
* }),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* const { useGetPostsQuery } = postsApiSlice
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* const { post } = useGetPostsQuery(undefined, {
|
||||
* selectFromResult: (state) => ({
|
||||
* post: state.data?.posts.find((post) => post.id === id),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* return <li>{post?.title}</li>
|
||||
* }
|
||||
*
|
||||
* const EMPTY_ARRAY: Post[] = []
|
||||
*
|
||||
* const typedSelectFromResult: TypedQueryStateSelector<
|
||||
* PostsApiResponse,
|
||||
* QueryArgument,
|
||||
* BaseQueryFunction,
|
||||
* SelectedResult
|
||||
* > = (state) => ({ posts: state.data?.posts ?? EMPTY_ARRAY })
|
||||
*
|
||||
* function PostsList() {
|
||||
* const { posts } = useGetPostsQuery(undefined, {
|
||||
* selectFromResult: typedSelectFromResult,
|
||||
* })
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <ul>
|
||||
* {posts.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* </div>
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @template ResultType - The type of the result `data` returned by the query.
|
||||
* @template QueryArgumentType - The type of the argument passed into the query.
|
||||
* @template BaseQueryFunctionType - The type of the base query function being used.
|
||||
* @template SelectedResultType - The type of the selected result returned by the __`selectFromResult`__ function.
|
||||
*
|
||||
* @since 2.3.0
|
||||
* @public
|
||||
*/
|
||||
type TypedQueryStateSelector<ResultType, QueryArgumentType, BaseQueryFunctionType extends BaseQueryFn, SelectedResultType extends Record<string, any> = UseQueryStateDefaultResult<QueryDefinition<QueryArgumentType, BaseQueryFunctionType, string, ResultType, string>>> = QueryStateSelector<SelectedResultType, QueryDefinition<QueryArgumentType, BaseQueryFunctionType, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* Note that this hook does not trigger fetching new data. For that use-case, see [`useQuery`](#usequery) or [`useQuerySubscription`](#usequerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseQueryState<D extends QueryDefinition<any, any, any, any>> = <R extends Record<string, any> = UseQueryStateDefaultResult<D>>(arg: QueryArgFrom<D> | SkipToken, options?: UseQueryStateOptions<D, R>) => UseQueryStateResult<D, R>;
|
||||
type TypedUseQueryState<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseQueryState<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
type UseQueryStateOptions<D extends QueryDefinition<any, any, any, any>, R extends Record<string, any>> = {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When skip is true:
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after skipping the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* `selectFromResult` allows you to get a specific segment from a query result in a performant manner.
|
||||
* When using this feature, the component will not rerender unless the underlying data of the selected item has changed.
|
||||
* If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using selectFromResult to extract a single result"
|
||||
* function PostsList() {
|
||||
* const { data: posts } = api.useGetPostsQuery();
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {posts?.data?.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* // Will select the post with the given id, and will only rerender if the given posts data changes
|
||||
* const { post } = api.useGetPostsQuery(undefined, {
|
||||
* selectFromResult: ({ data }) => ({ post: data?.find((post) => post.id === id) }),
|
||||
* });
|
||||
*
|
||||
* return <li>{post?.name}</li>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
selectFromResult?: QueryStateSelector<R, D>;
|
||||
};
|
||||
/**
|
||||
* Provides a way to define a "pre-typed" version of
|
||||
* {@linkcode UseQueryStateOptions} with specific options for a given query.
|
||||
* This is particularly useful for setting default query behaviors such as
|
||||
* refetching strategies, which can be overridden as needed.
|
||||
*
|
||||
* @example
|
||||
* <caption>#### __Create a `useQuery` hook with default options__</caption>
|
||||
*
|
||||
* ```ts
|
||||
* import type {
|
||||
* SubscriptionOptions,
|
||||
* TypedUseQueryStateOptions,
|
||||
* } from '@reduxjs/toolkit/query/react'
|
||||
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* type Post = {
|
||||
* id: number
|
||||
* name: string
|
||||
* }
|
||||
*
|
||||
* const api = createApi({
|
||||
* baseQuery: fetchBaseQuery({ baseUrl: '/' }),
|
||||
* tagTypes: ['Post'],
|
||||
* endpoints: (build) => ({
|
||||
* getPosts: build.query<Post[], void>({
|
||||
* query: () => 'posts',
|
||||
* }),
|
||||
* }),
|
||||
* })
|
||||
*
|
||||
* const { useGetPostsQuery } = api
|
||||
*
|
||||
* export const useGetPostsQueryWithDefaults = <
|
||||
* SelectedResult extends Record<string, any>,
|
||||
* >(
|
||||
* overrideOptions: TypedUseQueryStateOptions<
|
||||
* Post[],
|
||||
* void,
|
||||
* ReturnType<typeof fetchBaseQuery>,
|
||||
* SelectedResult
|
||||
* > &
|
||||
* SubscriptionOptions,
|
||||
* ) =>
|
||||
* useGetPostsQuery(undefined, {
|
||||
* // Insert default options here
|
||||
*
|
||||
* refetchOnMountOrArgChange: true,
|
||||
* refetchOnFocus: true,
|
||||
* ...overrideOptions,
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @template ResultType - The type of the result `data` returned by the query.
|
||||
* @template QueryArg - The type of the argument passed into the query.
|
||||
* @template BaseQuery - The type of the base query function being used.
|
||||
* @template SelectedResult - The type of the selected result returned by the __`selectFromResult`__ function.
|
||||
*
|
||||
* @since 2.2.8
|
||||
* @public
|
||||
*/
|
||||
type TypedUseQueryStateOptions<ResultType, QueryArg, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseQueryStateOptions<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>, SelectedResult>;
|
||||
type UseQueryStateResult<_ extends QueryDefinition<any, any, any, any>, R> = TSHelpersNoInfer<R>;
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQueryState` hook in userland code.
|
||||
*/
|
||||
type TypedUseQueryStateResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = UseQueryStateDefaultResult<QueryDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = TSHelpersNoInfer<R>;
|
||||
type UseQueryStateBaseResult<D extends QueryDefinition<any, any, any, any>> = QuerySubState<D> & {
|
||||
/**
|
||||
* Where `data` tries to hold data as much as possible, also re-using
|
||||
* data from the last arguments passed into the hook, this property
|
||||
* will always contain the received data from the query, for the current query arguments.
|
||||
*/
|
||||
currentData?: ResultTypeFrom<D>;
|
||||
/**
|
||||
* Query has not started yet.
|
||||
*/
|
||||
isUninitialized: false;
|
||||
/**
|
||||
* Query is currently loading for the first time. No data yet.
|
||||
*/
|
||||
isLoading: false;
|
||||
/**
|
||||
* Query is currently fetching, but might have data from an earlier request.
|
||||
*/
|
||||
isFetching: false;
|
||||
/**
|
||||
* Query has data from a successful load.
|
||||
*/
|
||||
isSuccess: false;
|
||||
/**
|
||||
* Query is currently in "error" state.
|
||||
*/
|
||||
isError: false;
|
||||
};
|
||||
type UseQueryStateUninitialized<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<Extract<UseQueryStateBaseResult<D>, {
|
||||
status: QueryStatus.uninitialized;
|
||||
}>, {
|
||||
isUninitialized: true;
|
||||
}>;
|
||||
type UseQueryStateLoading<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isLoading: true;
|
||||
isFetching: boolean;
|
||||
data: undefined;
|
||||
}>;
|
||||
type UseQueryStateSuccessFetching<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isSuccess: true;
|
||||
isFetching: true;
|
||||
error: undefined;
|
||||
} & {
|
||||
data: ResultTypeFrom<D>;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'fulfilledTimeStamp'>>>;
|
||||
type UseQueryStateSuccessNotFetching<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isSuccess: true;
|
||||
isFetching: false;
|
||||
error: undefined;
|
||||
} & {
|
||||
data: ResultTypeFrom<D>;
|
||||
currentData: ResultTypeFrom<D>;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'fulfilledTimeStamp'>>>;
|
||||
type UseQueryStateError<D extends QueryDefinition<any, any, any, any>> = TSHelpersOverride<UseQueryStateBaseResult<D>, {
|
||||
isError: true;
|
||||
} & Required<Pick<UseQueryStateBaseResult<D>, 'error'>>>;
|
||||
type UseQueryStateDefaultResult<D extends QueryDefinition<any, any, any, any>> = TSHelpersId<UseQueryStateUninitialized<D> | UseQueryStateLoading<D> | UseQueryStateSuccessFetching<D> | UseQueryStateSuccessNotFetching<D> | UseQueryStateError<D>> & {
|
||||
/**
|
||||
* @deprecated Included for completeness, but discouraged.
|
||||
* Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
|
||||
* and `isUninitialized` flags instead
|
||||
*/
|
||||
status: QueryStatus;
|
||||
};
|
||||
type LazyInfiniteQueryTrigger<D extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers a lazy query.
|
||||
*
|
||||
* By default, this will start a new request even if there is already a value in the cache.
|
||||
* If you want to use the cache value and only start a request if there is no cache value, set the second argument to `true`.
|
||||
*
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a lazy query, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await getUserById(1).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>, direction: InfiniteQueryDirection): InfiniteQueryActionCreatorResult<D>;
|
||||
};
|
||||
type TypedLazyInfiniteQueryTrigger<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = LazyInfiniteQueryTrigger<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type UseInfiniteQuerySubscriptionOptions<D extends InfiniteQueryDefinition<any, any, any, any, any>> = SubscriptionOptions & {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When `skip` is true (or `skipToken` is passed in as `arg`):
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* Defaults to `false`. This setting allows you to control whether if a cached result is already available, RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
|
||||
* - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
|
||||
* - `true` - Will always refetch when a new subscriber to a query is added. Behaves the same as calling the `refetch` callback or passing `forceRefetch: true` in the action creator.
|
||||
* - `number` - **Value is in seconds**. If a number is provided and there is an existing query in the cache, it will compare the current time vs the last fulfilled timestamp, and only refetch if enough time has elapsed.
|
||||
*
|
||||
* If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false.
|
||||
*/
|
||||
refetchOnMountOrArgChange?: boolean | number;
|
||||
initialPageParam?: PageParamFrom<D>;
|
||||
/**
|
||||
* Defaults to `true`. When this is `true` and an infinite query endpoint is refetched
|
||||
* (due to tag invalidation, polling, arg change configuration, or manual refetching),
|
||||
* RTK Query will try to sequentially refetch all pages currently in the cache.
|
||||
* When `false` only the first page will be refetched.
|
||||
*
|
||||
* This option applies to all automatic refetches for this subscription (polling, tag invalidation, etc.).
|
||||
* It can be overridden on a per-call basis using the `refetch()` method.
|
||||
*/
|
||||
refetchCachedPages?: boolean;
|
||||
};
|
||||
type TypedUseInfiniteQuerySubscription<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuerySubscription<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type UseInfiniteQuerySubscriptionResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = {
|
||||
refetch: (options?: Pick<UseInfiniteQuerySubscriptionOptions<D>, 'refetchCachedPages'>) => InfiniteQueryActionCreatorResult<D>;
|
||||
trigger: LazyInfiniteQueryTrigger<D>;
|
||||
fetchNextPage: () => InfiniteQueryActionCreatorResult<D>;
|
||||
fetchPreviousPage: () => InfiniteQueryActionCreatorResult<D>;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useQuerySubscription` hook in userland code.
|
||||
*/
|
||||
type TypedUseInfiniteQuerySubscriptionResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuerySubscriptionResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
type InfiniteQueryStateSelector<R extends Record<string, any>, D extends InfiniteQueryDefinition<any, any, any, any, any>> = (state: UseInfiniteQueryStateDefaultResult<D>) => R;
|
||||
type TypedInfiniteQueryStateSelector<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = InfiniteQueryStateSelector<SelectedResult, InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, 'subscribes' the component to the cached data, and reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available. Additionally, it will cache multiple "pages" worth of responses within a single cache entry, and allows fetching more pages forwards and backwards from the current cached pages.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already, and the hook will return the data for that query arg once it's available.
|
||||
*
|
||||
* The `data` field will be a `{pages: Data[], pageParams: PageParam[]}` structure containing all fetched page responses and the corresponding page param values for each page. You may use this to render individual pages, combine all pages into a single infinite list, or other display logic as needed.
|
||||
*
|
||||
* This hook combines the functionality of both [`useInfiniteQueryState`](#useinfinitequerystate) and [`useInfiniteQuerySubscription`](#useinfinitequerysubscription) together, and is intended to be used in the majority of situations.
|
||||
*
|
||||
* As with normal query hooks, `skipToken` is a valid argument that will skip the query from executing.
|
||||
*
|
||||
* By default, the initial request will use the `initialPageParam` value that was defined on the infinite query endpoint. If you want to start from a different value, you can pass `initialPageParam` as part of the hook options to override that initial request value.
|
||||
*
|
||||
* Use the returned `fetchNextPage` and `fetchPreviousPage` methods on the hook result object to trigger fetches forwards and backwards. These will always calculate the next or previous page param based on the current cached pages and the provided `getNext/PreviousPageParam` callbacks defined in the endpoint.
|
||||
*
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Caches multiple pages worth of responses, and provides methods to trigger more page fetches forwards and backwards
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseInfiniteQuery<D extends InfiniteQueryDefinition<any, any, any, any, any>> = <R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<D>>(arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQuerySubscriptionOptions<D> & UseInfiniteQueryStateOptions<D, R>) => UseInfiniteQueryHookResult<D, R> & Pick<UseInfiniteQuerySubscriptionResult<D>, 'fetchNextPage' | 'fetchPreviousPage'>;
|
||||
type TypedUseInfiniteQuery<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQuery<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that reads the request status and cached data from the Redux store. The component will re-render as the loading status changes and the data becomes available.
|
||||
*
|
||||
* Note that this hook does not trigger fetching new data. For that use-case, see [`useInfiniteQuery`](#useinfinitequery) or [`useInfiniteQuerySubscription`](#useinfinitequerysubscription).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseInfiniteQueryState<D extends InfiniteQueryDefinition<any, any, any, any, any>> = <R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<D>>(arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQueryStateOptions<D, R>) => UseInfiniteQueryStateResult<D, R>;
|
||||
type TypedUseInfiniteQueryState<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn> = UseInfiniteQueryState<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>;
|
||||
/**
|
||||
* A React hook that automatically triggers fetches of data from an endpoint, and 'subscribes' the component to the cached data. Additionally, it will cache multiple "pages" worth of responses within a single cache entry, and allows fetching more pages forwards and backwards from the current cached pages.
|
||||
*
|
||||
* The query arg is used as a cache key. Changing the query arg will tell the hook to re-fetch the data if it does not exist in the cache already.
|
||||
*
|
||||
* Note that this hook does not return a request status or cached data. For that use-case, see [`useInfiniteQuery`](#useinfinitequery) or [`useInfiniteQueryState`](#useinfinitequerystate).
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Automatically triggers requests to retrieve data based on the hook argument and whether cached data exists by default
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Caches multiple pages worth of responses, and provides methods to trigger more page fetches forwards and backwards
|
||||
* - Accepts polling/re-fetching options to trigger automatic re-fetches when the corresponding criteria is met
|
||||
*/
|
||||
type UseInfiniteQuerySubscription<D extends InfiniteQueryDefinition<any, any, any, any, any>> = (arg: InfiniteQueryArgFrom<D> | SkipToken, options?: UseInfiniteQuerySubscriptionOptions<D>) => UseInfiniteQuerySubscriptionResult<D>;
|
||||
type UseInfiniteQueryHookResult<D extends InfiniteQueryDefinition<any, any, any, any, any>, R = UseInfiniteQueryStateDefaultResult<D>> = UseInfiniteQueryStateResult<D, R> & Pick<UseInfiniteQuerySubscriptionResult<D>, 'refetch' | 'fetchNextPage' | 'fetchPreviousPage'>;
|
||||
type TypedUseInfiniteQueryHookResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, R extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryHookResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, R>;
|
||||
type UseInfiniteQueryStateOptions<D extends InfiniteQueryDefinition<any, any, any, any, any>, R extends Record<string, any>> = {
|
||||
/**
|
||||
* Prevents a query from automatically running.
|
||||
*
|
||||
* @remarks
|
||||
* When skip is true:
|
||||
*
|
||||
* - **If the query has cached data:**
|
||||
* * The cached data **will not be used** on the initial load, and will ignore updates from any identical query until the `skip` condition is removed
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * If `skip: false` is set after skipping the initial load, the cached result will be used
|
||||
* - **If the query does not have cached data:**
|
||||
* * The query will have a status of `uninitialized`
|
||||
* * The query will not exist in the state when viewed with the dev tools
|
||||
* * The query will not automatically fetch on mount
|
||||
* * The query will not automatically run when additional components with the same query are added that do run
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Skip example"
|
||||
* const Pokemon = ({ name, skip }: { name: string; skip: boolean }) => {
|
||||
* const { data, error, status } = useGetPokemonByNameQuery(name, {
|
||||
* skip,
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {name} - {status}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
skip?: boolean;
|
||||
/**
|
||||
* `selectFromResult` allows you to get a specific segment from a query result in a performant manner.
|
||||
* When using this feature, the component will not rerender unless the underlying data of the selected item has changed.
|
||||
* If the selected item is one element in a larger collection, it will disregard changes to elements in the same collection.
|
||||
* Note that this should always return an object (not a primitive), as RTKQ adds fields to the return value.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using selectFromResult to extract a single result"
|
||||
* function PostsList() {
|
||||
* const { data: posts } = api.useGetPostsQuery();
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {posts?.data?.map((post) => (
|
||||
* <PostById key={post.id} id={post.id} />
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function PostById({ id }: { id: number }) {
|
||||
* // Will select the post with the given id, and will only rerender if the given posts data changes
|
||||
* const { post } = api.useGetPostsQuery(undefined, {
|
||||
* selectFromResult: ({ data }) => ({ post: data?.find((post) => post.id === id) }),
|
||||
* });
|
||||
*
|
||||
* return <li>{post?.name}</li>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
selectFromResult?: InfiniteQueryStateSelector<R, D>;
|
||||
};
|
||||
type TypedUseInfiniteQueryStateOptions<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, SelectedResult extends Record<string, any> = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryStateOptions<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, SelectedResult>;
|
||||
type UseInfiniteQueryStateResult<D extends InfiniteQueryDefinition<any, any, any, any, any>, R = UseInfiniteQueryStateDefaultResult<D>> = TSHelpersNoInfer<R>;
|
||||
type TypedUseInfiniteQueryStateResult<ResultType, QueryArg, PageParam, BaseQuery extends BaseQueryFn, R = UseInfiniteQueryStateDefaultResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>>> = UseInfiniteQueryStateResult<InfiniteQueryDefinition<QueryArg, PageParam, BaseQuery, string, ResultType, string>, R>;
|
||||
type UseInfiniteQueryStateBaseResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = InfiniteQuerySubState<D> & {
|
||||
/**
|
||||
* Where `data` tries to hold data as much as possible, also re-using
|
||||
* data from the last arguments passed into the hook, this property
|
||||
* will always contain the received data from the query, for the current query arguments.
|
||||
*/
|
||||
currentData?: InfiniteData<ResultTypeFrom<D>, PageParamFrom<D>>;
|
||||
/**
|
||||
* Query has not started yet.
|
||||
*/
|
||||
isUninitialized: false;
|
||||
/**
|
||||
* Query is currently loading for the first time. No data yet.
|
||||
*/
|
||||
isLoading: false;
|
||||
/**
|
||||
* Query is currently fetching, but might have data from an earlier request.
|
||||
*/
|
||||
isFetching: false;
|
||||
/**
|
||||
* Query has data from a successful load.
|
||||
*/
|
||||
isSuccess: false;
|
||||
/**
|
||||
* Query is currently in "error" state.
|
||||
*/
|
||||
isError: false;
|
||||
hasNextPage: boolean;
|
||||
hasPreviousPage: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
isFetchingPreviousPage: boolean;
|
||||
};
|
||||
type UseInfiniteQueryStateDefaultResult<D extends InfiniteQueryDefinition<any, any, any, any, any>> = TSHelpersId<TSHelpersOverride<Extract<UseInfiniteQueryStateBaseResult<D>, {
|
||||
status: QueryStatus.uninitialized;
|
||||
}>, {
|
||||
isUninitialized: true;
|
||||
}> | TSHelpersOverride<UseInfiniteQueryStateBaseResult<D>, {
|
||||
isLoading: true;
|
||||
isFetching: boolean;
|
||||
data: undefined;
|
||||
} | ({
|
||||
isSuccess: true;
|
||||
isFetching: true;
|
||||
error: undefined;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'data' | 'fulfilledTimeStamp'>>) | ({
|
||||
isSuccess: true;
|
||||
isFetching: false;
|
||||
error: undefined;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'data' | 'fulfilledTimeStamp' | 'currentData'>>) | ({
|
||||
isError: true;
|
||||
} & Required<Pick<UseInfiniteQueryStateBaseResult<D>, 'error'>>)>> & {
|
||||
/**
|
||||
* @deprecated Included for completeness, but discouraged.
|
||||
* Please use the `isLoading`, `isFetching`, `isSuccess`, `isError`
|
||||
* and `isUninitialized` flags instead
|
||||
*/
|
||||
status: QueryStatus;
|
||||
};
|
||||
type MutationStateSelector<R extends Record<string, any>, D extends MutationDefinition<any, any, any, any>> = (state: MutationResultSelectorResult<D>) => R;
|
||||
type UseMutationStateOptions<D extends MutationDefinition<any, any, any, any>, R extends Record<string, any>> = {
|
||||
selectFromResult?: MutationStateSelector<R, D>;
|
||||
fixedCacheKey?: string;
|
||||
};
|
||||
type UseMutationStateResult<D extends MutationDefinition<any, any, any, any>, R> = TSHelpersNoInfer<R> & {
|
||||
originalArgs?: QueryArgFrom<D>;
|
||||
/**
|
||||
* Resets the hook state to its initial `uninitialized` state.
|
||||
* This will also remove the last result from the cache.
|
||||
*/
|
||||
reset: () => void;
|
||||
};
|
||||
/**
|
||||
* Helper type to manually type the result
|
||||
* of the `useMutation` hook in userland code.
|
||||
*/
|
||||
type TypedUseMutationResult<ResultType, QueryArg, BaseQuery extends BaseQueryFn, R = MutationResultSelectorResult<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>> = UseMutationStateResult<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>, R>;
|
||||
/**
|
||||
* A React hook that lets you trigger an update request for a given endpoint, and subscribes the component to read the request status from the Redux store. The component will re-render as the loading status changes.
|
||||
*
|
||||
* #### Features
|
||||
*
|
||||
* - Manual control over firing a request to alter data on the server or possibly invalidate the cache
|
||||
* - 'Subscribes' the component to keep cached data in the store, and 'unsubscribes' when the component unmounts
|
||||
* - Returns the latest request status and cached data from the Redux store
|
||||
* - Re-renders as the request status changes and data becomes available
|
||||
*/
|
||||
type UseMutation<D extends MutationDefinition<any, any, any, any>> = <R extends Record<string, any> = MutationResultSelectorResult<D>>(options?: UseMutationStateOptions<D, R>) => readonly [MutationTrigger<D>, UseMutationStateResult<D, R>];
|
||||
type TypedUseMutation<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = UseMutation<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
type MutationTrigger<D extends MutationDefinition<any, any, any, any>> = {
|
||||
/**
|
||||
* Triggers the mutation and returns a Promise.
|
||||
* @remarks
|
||||
* If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // codeblock-meta title="Using .unwrap with async await"
|
||||
* try {
|
||||
* const payload = await addPost({ id: 1, name: 'Example' }).unwrap();
|
||||
* console.log('fulfilled', payload)
|
||||
* } catch (error) {
|
||||
* console.error('rejected', error);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
(arg: QueryArgFrom<D>): MutationActionCreatorResult<D>;
|
||||
};
|
||||
type TypedMutationTrigger<ResultType, QueryArg, BaseQuery extends BaseQueryFn> = MutationTrigger<MutationDefinition<QueryArg, BaseQuery, string, ResultType, string>>;
|
||||
|
||||
type QueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.query;
|
||||
} ? `use${Capitalize<K & string>}Query` : never]: UseQuery<Extract<Definitions[K], QueryDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type LazyQueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.query;
|
||||
} ? `useLazy${Capitalize<K & string>}Query` : never]: UseLazyQuery<Extract<Definitions[K], QueryDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type InfiniteQueryHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.infinitequery;
|
||||
} ? `use${Capitalize<K & string>}InfiniteQuery` : never]: UseInfiniteQuery<Extract<Definitions[K], InfiniteQueryDefinition<any, any, any, any, any>>>;
|
||||
};
|
||||
type MutationHookNames<Definitions extends EndpointDefinitions> = {
|
||||
[K in keyof Definitions as Definitions[K] extends {
|
||||
type: DefinitionType.mutation;
|
||||
} ? `use${Capitalize<K & string>}Mutation` : never]: UseMutation<Extract<Definitions[K], MutationDefinition<any, any, any, any>>>;
|
||||
};
|
||||
type HooksWithUniqueNames<Definitions extends EndpointDefinitions> = QueryHookNames<Definitions> & LazyQueryHookNames<Definitions> & InfiniteQueryHookNames<Definitions> & MutationHookNames<Definitions>;
|
||||
|
||||
export declare const reactHooksModuleName: unique symbol;
|
||||
type ReactHooksModule = typeof reactHooksModuleName;
|
||||
declare module '@reduxjs/toolkit/query' {
|
||||
interface ApiModules<BaseQuery extends BaseQueryFn, Definitions extends EndpointDefinitions, ReducerPath extends string, TagTypes extends string> {
|
||||
[reactHooksModuleName]: {
|
||||
/**
|
||||
* Endpoints based on the input endpoints provided to `createApi`, containing `select`, `hooks` and `action matchers`.
|
||||
*/
|
||||
endpoints: {
|
||||
[K in keyof Definitions]: Definitions[K] extends QueryDefinition<any, any, any, any, any> ? QueryHooks<Definitions[K]> : Definitions[K] extends MutationDefinition<any, any, any, any, any> ? MutationHooks<Definitions[K]> : Definitions[K] extends InfiniteQueryDefinition<any, any, any, any, any> ? InfiniteQueryHooks<Definitions[K]> : never;
|
||||
};
|
||||
/**
|
||||
* A hook that accepts a string endpoint name, and provides a callback that when called, pre-fetches the data for that endpoint.
|
||||
*/
|
||||
usePrefetch<EndpointName extends QueryKeys<Definitions>>(endpointName: EndpointName, options?: PrefetchOptions): (arg: QueryArgFrom<Definitions[EndpointName]>, options?: PrefetchOptions) => void;
|
||||
} & HooksWithUniqueNames<Definitions>;
|
||||
}
|
||||
}
|
||||
type RR = typeof react_redux;
|
||||
interface ReactHooksModuleOptions {
|
||||
/**
|
||||
* The hooks from React Redux to be used
|
||||
*/
|
||||
hooks?: {
|
||||
/**
|
||||
* The version of the `useDispatch` hook to be used
|
||||
*/
|
||||
useDispatch: RR['useDispatch'];
|
||||
/**
|
||||
* The version of the `useSelector` hook to be used
|
||||
*/
|
||||
useSelector: RR['useSelector'];
|
||||
/**
|
||||
* The version of the `useStore` hook to be used
|
||||
*/
|
||||
useStore: RR['useStore'];
|
||||
};
|
||||
/**
|
||||
* The version of the `batchedUpdates` function to be used
|
||||
*/
|
||||
batch?: RR['batch'];
|
||||
/**
|
||||
* Enables performing asynchronous tasks immediately within a render.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```ts
|
||||
* import {
|
||||
* buildCreateApi,
|
||||
* coreModule,
|
||||
* reactHooksModule
|
||||
* } from '@reduxjs/toolkit/query/react'
|
||||
*
|
||||
* const createApi = buildCreateApi(
|
||||
* coreModule(),
|
||||
* reactHooksModule({ unstable__sideEffectsInRender: true })
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
unstable__sideEffectsInRender?: boolean;
|
||||
/**
|
||||
* A selector creator (usually from `reselect`, or matching the same signature)
|
||||
*/
|
||||
createSelector?: CreateSelectorFunction<any, any, any>;
|
||||
}
|
||||
/**
|
||||
* Creates a module that generates react hooks from endpoints, for use with `buildCreateApi`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const MyContext = React.createContext<ReactReduxContextValue | null>(null);
|
||||
* const customCreateApi = buildCreateApi(
|
||||
* coreModule(),
|
||||
* reactHooksModule({
|
||||
* hooks: {
|
||||
* useDispatch: createDispatchHook(MyContext),
|
||||
* useSelector: createSelectorHook(MyContext),
|
||||
* useStore: createStoreHook(MyContext)
|
||||
* }
|
||||
* })
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @returns A module for use with `buildCreateApi`
|
||||
*/
|
||||
declare const reactHooksModule: ({ batch, hooks, createSelector, unstable__sideEffectsInRender, ...rest }?: ReactHooksModuleOptions) => Module<ReactHooksModule>;
|
||||
|
||||
/**
|
||||
* Can be used as a `Provider` if you **do not already have a Redux store**.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // codeblock-meta no-transpile title="Basic usage - wrap your App with ApiProvider"
|
||||
* import * as React from 'react';
|
||||
* import { ApiProvider } from '@reduxjs/toolkit/query/react';
|
||||
* import { Pokemon } from './features/Pokemon';
|
||||
*
|
||||
* function App() {
|
||||
* return (
|
||||
* <ApiProvider api={api}>
|
||||
* <Pokemon />
|
||||
* </ApiProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* Using this together with an existing redux store, both will
|
||||
* conflict with each other - please use the traditional redux setup
|
||||
* in that case.
|
||||
*/
|
||||
declare function ApiProvider(props: {
|
||||
children: any;
|
||||
api: Api<any, {}, any, any>;
|
||||
setupListeners?: Parameters<typeof setupListeners>[1] | false;
|
||||
context?: Context<ReactReduxContextValue | null>;
|
||||
}): React.JSX.Element;
|
||||
|
||||
declare const createApi: _reduxjs_toolkit_query.CreateApi<typeof _reduxjs_toolkit_query.coreModuleName | typeof reactHooksModuleName>;
|
||||
|
||||
export { ApiProvider, type TypedInfiniteQueryStateSelector, type TypedLazyInfiniteQueryTrigger, type TypedLazyQueryTrigger, type TypedMutationTrigger, type TypedQueryStateSelector, type TypedUseInfiniteQuery, type TypedUseInfiniteQueryHookResult, type TypedUseInfiniteQueryState, type TypedUseInfiniteQueryStateOptions, type TypedUseInfiniteQueryStateResult, type TypedUseInfiniteQuerySubscription, type TypedUseInfiniteQuerySubscriptionResult, type TypedUseLazyQuery, type TypedUseLazyQueryStateResult, type TypedUseLazyQuerySubscription, type TypedUseMutation, type TypedUseMutationResult, type TypedUseQuery, type TypedUseQueryHookResult, type TypedUseQueryState, type TypedUseQueryStateOptions, type TypedUseQueryStateResult, type TypedUseQuerySubscription, type TypedUseQuerySubscriptionResult, createApi, reactHooksModule };
|
||||
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.browser.mjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.browser.mjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.browser.mjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.browser.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
740
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.legacy-esm.js
generated
vendored
Normal file
740
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.legacy-esm.js
generated
vendored
Normal file
@@ -0,0 +1,740 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defProps = Object.defineProperties;
|
||||
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
||||
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __spreadValues = (a, b) => {
|
||||
for (var prop in b || (b = {}))
|
||||
if (__hasOwnProp.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
if (__getOwnPropSymbols)
|
||||
for (var prop of __getOwnPropSymbols(b)) {
|
||||
if (__propIsEnum.call(b, prop))
|
||||
__defNormalProp(a, prop, b[prop]);
|
||||
}
|
||||
return a;
|
||||
};
|
||||
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
||||
var __objRest = (source, exclude) => {
|
||||
var target = {};
|
||||
for (var prop in source)
|
||||
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
||||
target[prop] = source[prop];
|
||||
if (source != null && __getOwnPropSymbols)
|
||||
for (var prop of __getOwnPropSymbols(source)) {
|
||||
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
||||
target[prop] = source[prop];
|
||||
}
|
||||
return target;
|
||||
};
|
||||
|
||||
// src/query/react/rtkqImports.ts
|
||||
import { buildCreateApi, coreModule, copyWithStructuralSharing, setupListeners, QueryStatus, skipToken } from "@reduxjs/toolkit/query";
|
||||
|
||||
// src/query/react/module.ts
|
||||
import { formatProdErrorMessage as _formatProdErrorMessage4 } from "@reduxjs/toolkit";
|
||||
import { batch as rrBatch, useDispatch as rrUseDispatch, useSelector as rrUseSelector, useStore as rrUseStore } from "react-redux";
|
||||
import { createSelector as _createSelector } from "reselect";
|
||||
|
||||
// src/query/utils/capitalize.ts
|
||||
function capitalize(str) {
|
||||
return str.replace(str[0], str[0].toUpperCase());
|
||||
}
|
||||
|
||||
// src/query/utils/countObjectKeys.ts
|
||||
function countObjectKeys(obj) {
|
||||
let count = 0;
|
||||
for (const _key in obj) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// src/query/endpointDefinitions.ts
|
||||
var ENDPOINT_QUERY = "query" /* query */;
|
||||
var ENDPOINT_MUTATION = "mutation" /* mutation */;
|
||||
var ENDPOINT_INFINITEQUERY = "infinitequery" /* infinitequery */;
|
||||
function isQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_QUERY;
|
||||
}
|
||||
function isMutationDefinition(e) {
|
||||
return e.type === ENDPOINT_MUTATION;
|
||||
}
|
||||
function isInfiniteQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_INFINITEQUERY;
|
||||
}
|
||||
|
||||
// src/query/tsHelpers.ts
|
||||
function safeAssign(target, ...args) {
|
||||
return Object.assign(target, ...args);
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
import { formatProdErrorMessage as _formatProdErrorMessage, formatProdErrorMessage as _formatProdErrorMessage2, formatProdErrorMessage as _formatProdErrorMessage3 } from "@reduxjs/toolkit";
|
||||
|
||||
// src/query/react/reactImports.ts
|
||||
import { useEffect, useRef, useMemo, useContext, useCallback, useDebugValue, useLayoutEffect, useState } from "react";
|
||||
|
||||
// src/query/react/reactReduxImports.ts
|
||||
import { shallowEqual, Provider, ReactReduxContext } from "react-redux";
|
||||
|
||||
// src/query/react/constants.ts
|
||||
var UNINITIALIZED_VALUE = Symbol();
|
||||
|
||||
// src/query/react/useSerializedStableValue.ts
|
||||
function useStableQueryArgs(queryArgs) {
|
||||
const cache = useRef(queryArgs);
|
||||
const copy = useMemo(() => copyWithStructuralSharing(cache.current, queryArgs), [queryArgs]);
|
||||
useEffect(() => {
|
||||
if (cache.current !== copy) {
|
||||
cache.current = copy;
|
||||
}
|
||||
}, [copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// src/query/react/useShallowStableValue.ts
|
||||
function useShallowStableValue(value) {
|
||||
const cache = useRef(value);
|
||||
useEffect(() => {
|
||||
if (!shallowEqual(cache.current, value)) {
|
||||
cache.current = value;
|
||||
}
|
||||
}, [value]);
|
||||
return shallowEqual(cache.current, value) ? cache.current : value;
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
var canUseDOM = () => !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
|
||||
var isDOM = /* @__PURE__ */ canUseDOM();
|
||||
var isRunningInReactNative = () => typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
||||
var isReactNative = /* @__PURE__ */ isRunningInReactNative();
|
||||
var getUseIsomorphicLayoutEffect = () => isDOM || isReactNative ? useLayoutEffect : useEffect;
|
||||
var useIsomorphicLayoutEffect = /* @__PURE__ */ getUseIsomorphicLayoutEffect();
|
||||
var noPendingQueryStateSelector = (selected) => {
|
||||
if (selected.isUninitialized) {
|
||||
return __spreadProps(__spreadValues({}, selected), {
|
||||
isUninitialized: false,
|
||||
isFetching: true,
|
||||
isLoading: selected.data !== void 0 ? false : true,
|
||||
// This is the one place where we still have to use `QueryStatus` as an enum,
|
||||
// since it's the only reference in the React package and not in the core.
|
||||
status: QueryStatus.pending
|
||||
});
|
||||
}
|
||||
return selected;
|
||||
};
|
||||
function pick(obj, ...keys) {
|
||||
const ret = {};
|
||||
keys.forEach((key) => {
|
||||
ret[key] = obj[key];
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
var COMMON_HOOK_DEBUG_FIELDS = ["data", "status", "isLoading", "isSuccess", "isError", "error"];
|
||||
function buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks: {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
useStore
|
||||
},
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
}) {
|
||||
const usePossiblyImmediateEffect = unstable__sideEffectsInRender ? (cb) => cb() : useEffect;
|
||||
const unsubscribePromiseRef = (ref) => {
|
||||
var _a, _b;
|
||||
return (_b = (_a = ref.current) == null ? void 0 : _a.unsubscribe) == null ? void 0 : _b.call(_a);
|
||||
};
|
||||
const endpointDefinitions = context.endpointDefinitions;
|
||||
return {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
};
|
||||
function queryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if ((lastResult == null ? void 0 : lastResult.endpointName) && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult == null ? void 0 : lastResult.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || hasData && (isFetching && !(lastResult == null ? void 0 : lastResult.isError) || currentState.isUninitialized);
|
||||
return __spreadProps(__spreadValues({}, currentState), {
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
});
|
||||
}
|
||||
function infiniteQueryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if ((lastResult == null ? void 0 : lastResult.endpointName) && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult == null ? void 0 : lastResult.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || isFetching && hasData;
|
||||
return __spreadProps(__spreadValues({}, currentState), {
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
});
|
||||
}
|
||||
function usePrefetch(endpointName, defaultOptions) {
|
||||
const dispatch = useDispatch();
|
||||
const stableDefaultOptions = useShallowStableValue(defaultOptions);
|
||||
return useCallback((arg, options) => dispatch(api.util.prefetch(endpointName, arg, __spreadValues(__spreadValues({}, stableDefaultOptions), options))), [endpointName, dispatch, stableDefaultOptions]);
|
||||
}
|
||||
function useQuerySubscriptionCommonImpl(endpointName, arg, _a = {}) {
|
||||
var _b = _a, {
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
refetchOnMountOrArgChange,
|
||||
skip = false,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false
|
||||
} = _b, rest = __objRest(_b, [
|
||||
"refetchOnReconnect",
|
||||
"refetchOnFocus",
|
||||
"refetchOnMountOrArgChange",
|
||||
"skip",
|
||||
"pollingInterval",
|
||||
"skipPollingIfUnfocused"
|
||||
]);
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const subscriptionSelectorsRef = useRef(void 0);
|
||||
if (!subscriptionSelectorsRef.current) {
|
||||
const returnedValue = dispatch(api.internalActions.internal_getRTKQSubscriptions());
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (typeof returnedValue !== "object" || typeof (returnedValue == null ? void 0 : returnedValue.type) === "string") {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage(37) : `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
|
||||
You must add the middleware for RTK-Query to function correctly!`);
|
||||
}
|
||||
}
|
||||
subscriptionSelectorsRef.current = returnedValue;
|
||||
}
|
||||
const stableArg = useStableQueryArgs(skip ? skipToken : arg);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
const initialPageParam = rest.initialPageParam;
|
||||
const stableInitialPageParam = useShallowStableValue(initialPageParam);
|
||||
const refetchCachedPages = rest.refetchCachedPages;
|
||||
const stableRefetchCachedPages = useShallowStableValue(refetchCachedPages);
|
||||
const promiseRef = useRef(void 0);
|
||||
let {
|
||||
queryCacheKey,
|
||||
requestId
|
||||
} = promiseRef.current || {};
|
||||
let currentRenderHasSubscription = false;
|
||||
if (queryCacheKey && requestId) {
|
||||
currentRenderHasSubscription = subscriptionSelectorsRef.current.isRequestSubscribed(queryCacheKey, requestId);
|
||||
}
|
||||
const subscriptionRemoved = !currentRenderHasSubscription && promiseRef.current !== void 0;
|
||||
usePossiblyImmediateEffect(() => {
|
||||
if (subscriptionRemoved) {
|
||||
promiseRef.current = void 0;
|
||||
}
|
||||
}, [subscriptionRemoved]);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
var _a2;
|
||||
const lastPromise = promiseRef.current;
|
||||
if (typeof process !== "undefined" && process.env.NODE_ENV === "removeMeOnCompilation") {
|
||||
console.log(subscriptionRemoved);
|
||||
}
|
||||
if (stableArg === skipToken) {
|
||||
lastPromise == null ? void 0 : lastPromise.unsubscribe();
|
||||
promiseRef.current = void 0;
|
||||
return;
|
||||
}
|
||||
const lastSubscriptionOptions = (_a2 = promiseRef.current) == null ? void 0 : _a2.subscriptionOptions;
|
||||
if (!lastPromise || lastPromise.arg !== stableArg) {
|
||||
lastPromise == null ? void 0 : lastPromise.unsubscribe();
|
||||
const promise = dispatch(initiate(stableArg, __spreadValues({
|
||||
subscriptionOptions: stableSubscriptionOptions,
|
||||
forceRefetch: refetchOnMountOrArgChange
|
||||
}, isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? {
|
||||
initialPageParam: stableInitialPageParam,
|
||||
refetchCachedPages: stableRefetchCachedPages
|
||||
} : {})));
|
||||
promiseRef.current = promise;
|
||||
} else if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
lastPromise.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [dispatch, initiate, refetchOnMountOrArgChange, stableArg, stableSubscriptionOptions, subscriptionRemoved, stableInitialPageParam, stableRefetchCachedPages, endpointName]);
|
||||
return [promiseRef, dispatch, initiate, stableSubscriptionOptions];
|
||||
}
|
||||
function buildUseQueryState(endpointName, preSelector) {
|
||||
const useQueryState = (arg, {
|
||||
skip = false,
|
||||
selectFromResult
|
||||
} = {}) => {
|
||||
const {
|
||||
select
|
||||
} = api.endpoints[endpointName];
|
||||
const stableArg = useStableQueryArgs(skip ? skipToken : arg);
|
||||
const lastValue = useRef(void 0);
|
||||
const selectDefaultResult = useMemo(() => (
|
||||
// Normally ts-ignores are bad and should be avoided, but we're
|
||||
// already casting this selector to be `Selector<any>` anyway,
|
||||
// so the inconsistencies don't matter here
|
||||
// @ts-ignore
|
||||
createSelector([
|
||||
// @ts-ignore
|
||||
select(stableArg),
|
||||
(_, lastResult) => lastResult,
|
||||
(_) => stableArg
|
||||
], preSelector, {
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: shallowEqual
|
||||
}
|
||||
})
|
||||
), [select, stableArg]);
|
||||
const querySelector = useMemo(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult, {
|
||||
devModeChecks: {
|
||||
identityFunctionCheck: "never"
|
||||
}
|
||||
}) : selectDefaultResult, [selectDefaultResult, selectFromResult]);
|
||||
const currentState = useSelector((state) => querySelector(state, lastValue.current), shallowEqual);
|
||||
const store = useStore();
|
||||
const newLastValue = selectDefaultResult(store.getState(), lastValue.current);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
lastValue.current = newLastValue;
|
||||
}, [newLastValue]);
|
||||
return currentState;
|
||||
};
|
||||
return useQueryState;
|
||||
}
|
||||
function usePromiseRefUnsubscribeOnUnmount(promiseRef) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = void 0;
|
||||
};
|
||||
}, [promiseRef]);
|
||||
}
|
||||
function refetchOrErrorIfUnmounted(promiseRef) {
|
||||
if (!promiseRef.current) throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage2(38) : "Cannot refetch a query that has not been started yet.");
|
||||
return promiseRef.current.refetch();
|
||||
}
|
||||
function buildQueryHooks(endpointName) {
|
||||
const useQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
return useMemo(() => ({
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch: () => refetchOrErrorIfUnmounted(promiseRef)
|
||||
}), [promiseRef]);
|
||||
};
|
||||
const useLazyQuerySubscription = ({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false
|
||||
} = {}) => {
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const [arg, setArg] = useState(UNINITIALIZED_VALUE);
|
||||
const promiseRef = useRef(void 0);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
usePossiblyImmediateEffect(() => {
|
||||
var _a, _b;
|
||||
const lastSubscriptionOptions = (_a = promiseRef.current) == null ? void 0 : _a.subscriptionOptions;
|
||||
if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
(_b = promiseRef.current) == null ? void 0 : _b.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [stableSubscriptionOptions]);
|
||||
const subscriptionOptionsRef = useRef(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const trigger = useCallback(function(arg2, preferCacheValue = false) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
forceRefetch: !preferCacheValue
|
||||
}));
|
||||
setArg(arg2);
|
||||
});
|
||||
return promise;
|
||||
}, [dispatch, initiate]);
|
||||
const reset = useCallback(() => {
|
||||
var _a, _b;
|
||||
if ((_a = promiseRef.current) == null ? void 0 : _a.queryCacheKey) {
|
||||
dispatch(api.internalActions.removeQueryResult({
|
||||
queryCacheKey: (_b = promiseRef.current) == null ? void 0 : _b.queryCacheKey
|
||||
}));
|
||||
}
|
||||
}, [dispatch]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) {
|
||||
trigger(arg, true);
|
||||
}
|
||||
}, [arg, trigger]);
|
||||
return useMemo(() => [trigger, arg, {
|
||||
reset
|
||||
}], [trigger, arg, reset]);
|
||||
};
|
||||
const useQueryState = buildUseQueryState(endpointName, queryStatePreSelector);
|
||||
return {
|
||||
useQueryState,
|
||||
useQuerySubscription,
|
||||
useLazyQuerySubscription,
|
||||
useLazyQuery(options) {
|
||||
const [trigger, arg, {
|
||||
reset
|
||||
}] = useLazyQuerySubscription(options);
|
||||
const queryStateResults = useQueryState(arg, __spreadProps(__spreadValues({}, options), {
|
||||
skip: arg === UNINITIALIZED_VALUE
|
||||
}));
|
||||
const info = useMemo(() => ({
|
||||
lastArg: arg
|
||||
}), [arg]);
|
||||
return useMemo(() => [trigger, __spreadProps(__spreadValues({}, queryStateResults), {
|
||||
reset
|
||||
}), info], [trigger, queryStateResults, reset, info]);
|
||||
},
|
||||
useQuery(arg, options) {
|
||||
const querySubscriptionResults = useQuerySubscription(arg, options);
|
||||
const queryStateResults = useQueryState(arg, __spreadValues({
|
||||
selectFromResult: arg === skipToken || (options == null ? void 0 : options.skip) ? void 0 : noPendingQueryStateSelector
|
||||
}, options));
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS);
|
||||
useDebugValue(debugValue);
|
||||
return useMemo(() => __spreadValues(__spreadValues({}, queryStateResults), querySubscriptionResults), [queryStateResults, querySubscriptionResults]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildInfiniteQueryHooks(endpointName) {
|
||||
const useInfiniteQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef, dispatch, initiate, stableSubscriptionOptions] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
const subscriptionOptionsRef = useRef(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const hookRefetchCachedPages = options.refetchCachedPages;
|
||||
const stableHookRefetchCachedPages = useShallowStableValue(hookRefetchCachedPages);
|
||||
const trigger = useCallback(function(arg2, direction) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
direction
|
||||
}));
|
||||
});
|
||||
return promise;
|
||||
}, [promiseRef, dispatch, initiate]);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
const stableArg = useStableQueryArgs(options.skip ? skipToken : arg);
|
||||
const refetch = useCallback((options2) => {
|
||||
var _a;
|
||||
if (!promiseRef.current) throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage3(38) : "Cannot refetch a query that has not been started yet.");
|
||||
const mergedOptions = {
|
||||
refetchCachedPages: (_a = options2 == null ? void 0 : options2.refetchCachedPages) != null ? _a : stableHookRefetchCachedPages
|
||||
};
|
||||
return promiseRef.current.refetch(mergedOptions);
|
||||
}, [promiseRef, stableHookRefetchCachedPages]);
|
||||
return useMemo(() => {
|
||||
const fetchNextPage = () => {
|
||||
return trigger(stableArg, "forward");
|
||||
};
|
||||
const fetchPreviousPage = () => {
|
||||
return trigger(stableArg, "backward");
|
||||
};
|
||||
return {
|
||||
trigger,
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
};
|
||||
}, [refetch, trigger, stableArg]);
|
||||
};
|
||||
const useInfiniteQueryState = buildUseQueryState(endpointName, infiniteQueryStatePreSelector);
|
||||
return {
|
||||
useInfiniteQueryState,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQuery(arg, options) {
|
||||
const {
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
} = useInfiniteQuerySubscription(arg, options);
|
||||
const queryStateResults = useInfiniteQueryState(arg, __spreadValues({
|
||||
selectFromResult: arg === skipToken || (options == null ? void 0 : options.skip) ? void 0 : noPendingQueryStateSelector
|
||||
}, options));
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS, "hasNextPage", "hasPreviousPage");
|
||||
useDebugValue(debugValue);
|
||||
return useMemo(() => __spreadProps(__spreadValues({}, queryStateResults), {
|
||||
fetchNextPage,
|
||||
fetchPreviousPage,
|
||||
refetch
|
||||
}), [queryStateResults, fetchNextPage, fetchPreviousPage, refetch]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildMutationHook(name) {
|
||||
return ({
|
||||
selectFromResult,
|
||||
fixedCacheKey
|
||||
} = {}) => {
|
||||
const {
|
||||
select,
|
||||
initiate
|
||||
} = api.endpoints[name];
|
||||
const dispatch = useDispatch();
|
||||
const [promise, setPromise] = useState();
|
||||
useEffect(() => () => {
|
||||
if (!(promise == null ? void 0 : promise.arg.fixedCacheKey)) {
|
||||
promise == null ? void 0 : promise.reset();
|
||||
}
|
||||
}, [promise]);
|
||||
const triggerMutation = useCallback(function(arg) {
|
||||
const promise2 = dispatch(initiate(arg, {
|
||||
fixedCacheKey
|
||||
}));
|
||||
setPromise(promise2);
|
||||
return promise2;
|
||||
}, [dispatch, initiate, fixedCacheKey]);
|
||||
const {
|
||||
requestId
|
||||
} = promise || {};
|
||||
const selectDefaultResult = useMemo(() => select({
|
||||
fixedCacheKey,
|
||||
requestId: promise == null ? void 0 : promise.requestId
|
||||
}), [fixedCacheKey, promise, select]);
|
||||
const mutationSelector = useMemo(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult) : selectDefaultResult, [selectFromResult, selectDefaultResult]);
|
||||
const currentState = useSelector(mutationSelector, shallowEqual);
|
||||
const originalArgs = fixedCacheKey == null ? promise == null ? void 0 : promise.arg.originalArgs : void 0;
|
||||
const reset = useCallback(() => {
|
||||
batch(() => {
|
||||
if (promise) {
|
||||
setPromise(void 0);
|
||||
}
|
||||
if (fixedCacheKey) {
|
||||
dispatch(api.internalActions.removeMutationResult({
|
||||
requestId,
|
||||
fixedCacheKey
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [dispatch, fixedCacheKey, promise, requestId]);
|
||||
const debugValue = pick(currentState, ...COMMON_HOOK_DEBUG_FIELDS, "endpointName");
|
||||
useDebugValue(debugValue);
|
||||
const finalState = useMemo(() => __spreadProps(__spreadValues({}, currentState), {
|
||||
originalArgs,
|
||||
reset
|
||||
}), [currentState, originalArgs, reset]);
|
||||
return useMemo(() => [triggerMutation, finalState], [triggerMutation, finalState]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/query/react/module.ts
|
||||
var reactHooksModuleName = /* @__PURE__ */ Symbol();
|
||||
var reactHooksModule = (_a = {}) => {
|
||||
var _b = _a, {
|
||||
batch = rrBatch,
|
||||
hooks = {
|
||||
useDispatch: rrUseDispatch,
|
||||
useSelector: rrUseSelector,
|
||||
useStore: rrUseStore
|
||||
},
|
||||
createSelector = _createSelector,
|
||||
unstable__sideEffectsInRender = false
|
||||
} = _b, rest = __objRest(_b, [
|
||||
"batch",
|
||||
"hooks",
|
||||
"createSelector",
|
||||
"unstable__sideEffectsInRender"
|
||||
]);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const hookNames = ["useDispatch", "useSelector", "useStore"];
|
||||
let warned = false;
|
||||
for (const hookName of hookNames) {
|
||||
if (countObjectKeys(rest) > 0) {
|
||||
if (rest[hookName]) {
|
||||
if (!warned) {
|
||||
console.warn("As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`");
|
||||
warned = true;
|
||||
}
|
||||
}
|
||||
hooks[hookName] = rest[hookName];
|
||||
}
|
||||
if (typeof hooks[hookName] !== "function") {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage4(36) : `When using custom hooks for context, all ${hookNames.length} hooks need to be provided: ${hookNames.join(", ")}.
|
||||
Hook ${hookName} was either not provided or not a function.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: reactHooksModuleName,
|
||||
init(api, {
|
||||
serializeQueryArgs
|
||||
}, context) {
|
||||
const anyApi = api;
|
||||
const {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
} = buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks,
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
});
|
||||
safeAssign(anyApi, {
|
||||
usePrefetch
|
||||
});
|
||||
safeAssign(context, {
|
||||
batch
|
||||
});
|
||||
return {
|
||||
injectEndpoint(endpointName, definition) {
|
||||
if (isQueryDefinition(definition)) {
|
||||
const {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
} = buildQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Query`] = useQuery;
|
||||
api[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery;
|
||||
}
|
||||
if (isMutationDefinition(definition)) {
|
||||
const useMutation = buildMutationHook(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useMutation
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Mutation`] = useMutation;
|
||||
} else if (isInfiniteQueryDefinition(definition)) {
|
||||
const {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
} = buildInfiniteQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
});
|
||||
api[`use${capitalize(endpointName)}InfiniteQuery`] = useInfiniteQuery;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// src/query/react/index.ts
|
||||
export * from "@reduxjs/toolkit/query";
|
||||
|
||||
// src/query/react/ApiProvider.tsx
|
||||
import { configureStore, formatProdErrorMessage as _formatProdErrorMessage5 } from "@reduxjs/toolkit";
|
||||
import * as React from "react";
|
||||
function ApiProvider(props) {
|
||||
const context = props.context || ReactReduxContext;
|
||||
const existingContext = useContext(context);
|
||||
if (existingContext) {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage5(35) : "Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.");
|
||||
}
|
||||
const [store] = React.useState(() => configureStore({
|
||||
reducer: {
|
||||
[props.api.reducerPath]: props.api.reducer
|
||||
},
|
||||
middleware: (gDM) => gDM().concat(props.api.middleware)
|
||||
}));
|
||||
useEffect(() => props.setupListeners === false ? void 0 : setupListeners(store.dispatch, props.setupListeners), [props.setupListeners, store.dispatch]);
|
||||
return /* @__PURE__ */ React.createElement(Provider, { store, context }, props.children);
|
||||
}
|
||||
|
||||
// src/query/react/index.ts
|
||||
var createApi = /* @__PURE__ */ buildCreateApi(coreModule(), reactHooksModule());
|
||||
export {
|
||||
ApiProvider,
|
||||
UNINITIALIZED_VALUE,
|
||||
createApi,
|
||||
reactHooksModule,
|
||||
reactHooksModuleName
|
||||
};
|
||||
//# sourceMappingURL=rtk-query-react.legacy-esm.js.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.legacy-esm.js.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.legacy-esm.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
705
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.modern.mjs
generated
vendored
Normal file
705
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.modern.mjs
generated
vendored
Normal file
@@ -0,0 +1,705 @@
|
||||
// src/query/react/rtkqImports.ts
|
||||
import { buildCreateApi, coreModule, copyWithStructuralSharing, setupListeners, QueryStatus, skipToken } from "@reduxjs/toolkit/query";
|
||||
|
||||
// src/query/react/module.ts
|
||||
import { formatProdErrorMessage as _formatProdErrorMessage4 } from "@reduxjs/toolkit";
|
||||
import { batch as rrBatch, useDispatch as rrUseDispatch, useSelector as rrUseSelector, useStore as rrUseStore } from "react-redux";
|
||||
import { createSelector as _createSelector } from "reselect";
|
||||
|
||||
// src/query/utils/capitalize.ts
|
||||
function capitalize(str) {
|
||||
return str.replace(str[0], str[0].toUpperCase());
|
||||
}
|
||||
|
||||
// src/query/utils/countObjectKeys.ts
|
||||
function countObjectKeys(obj) {
|
||||
let count = 0;
|
||||
for (const _key in obj) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// src/query/endpointDefinitions.ts
|
||||
var ENDPOINT_QUERY = "query" /* query */;
|
||||
var ENDPOINT_MUTATION = "mutation" /* mutation */;
|
||||
var ENDPOINT_INFINITEQUERY = "infinitequery" /* infinitequery */;
|
||||
function isQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_QUERY;
|
||||
}
|
||||
function isMutationDefinition(e) {
|
||||
return e.type === ENDPOINT_MUTATION;
|
||||
}
|
||||
function isInfiniteQueryDefinition(e) {
|
||||
return e.type === ENDPOINT_INFINITEQUERY;
|
||||
}
|
||||
|
||||
// src/query/tsHelpers.ts
|
||||
function safeAssign(target, ...args) {
|
||||
return Object.assign(target, ...args);
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
import { formatProdErrorMessage as _formatProdErrorMessage, formatProdErrorMessage as _formatProdErrorMessage2, formatProdErrorMessage as _formatProdErrorMessage3 } from "@reduxjs/toolkit";
|
||||
|
||||
// src/query/react/reactImports.ts
|
||||
import { useEffect, useRef, useMemo, useContext, useCallback, useDebugValue, useLayoutEffect, useState } from "react";
|
||||
|
||||
// src/query/react/reactReduxImports.ts
|
||||
import { shallowEqual, Provider, ReactReduxContext } from "react-redux";
|
||||
|
||||
// src/query/react/constants.ts
|
||||
var UNINITIALIZED_VALUE = Symbol();
|
||||
|
||||
// src/query/react/useSerializedStableValue.ts
|
||||
function useStableQueryArgs(queryArgs) {
|
||||
const cache = useRef(queryArgs);
|
||||
const copy = useMemo(() => copyWithStructuralSharing(cache.current, queryArgs), [queryArgs]);
|
||||
useEffect(() => {
|
||||
if (cache.current !== copy) {
|
||||
cache.current = copy;
|
||||
}
|
||||
}, [copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// src/query/react/useShallowStableValue.ts
|
||||
function useShallowStableValue(value) {
|
||||
const cache = useRef(value);
|
||||
useEffect(() => {
|
||||
if (!shallowEqual(cache.current, value)) {
|
||||
cache.current = value;
|
||||
}
|
||||
}, [value]);
|
||||
return shallowEqual(cache.current, value) ? cache.current : value;
|
||||
}
|
||||
|
||||
// src/query/react/buildHooks.ts
|
||||
var canUseDOM = () => !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined");
|
||||
var isDOM = /* @__PURE__ */ canUseDOM();
|
||||
var isRunningInReactNative = () => typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
||||
var isReactNative = /* @__PURE__ */ isRunningInReactNative();
|
||||
var getUseIsomorphicLayoutEffect = () => isDOM || isReactNative ? useLayoutEffect : useEffect;
|
||||
var useIsomorphicLayoutEffect = /* @__PURE__ */ getUseIsomorphicLayoutEffect();
|
||||
var noPendingQueryStateSelector = (selected) => {
|
||||
if (selected.isUninitialized) {
|
||||
return {
|
||||
...selected,
|
||||
isUninitialized: false,
|
||||
isFetching: true,
|
||||
isLoading: selected.data !== void 0 ? false : true,
|
||||
// This is the one place where we still have to use `QueryStatus` as an enum,
|
||||
// since it's the only reference in the React package and not in the core.
|
||||
status: QueryStatus.pending
|
||||
};
|
||||
}
|
||||
return selected;
|
||||
};
|
||||
function pick(obj, ...keys) {
|
||||
const ret = {};
|
||||
keys.forEach((key) => {
|
||||
ret[key] = obj[key];
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
var COMMON_HOOK_DEBUG_FIELDS = ["data", "status", "isLoading", "isSuccess", "isError", "error"];
|
||||
function buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks: {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
useStore
|
||||
},
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
}) {
|
||||
const usePossiblyImmediateEffect = unstable__sideEffectsInRender ? (cb) => cb() : useEffect;
|
||||
const unsubscribePromiseRef = (ref) => ref.current?.unsubscribe?.();
|
||||
const endpointDefinitions = context.endpointDefinitions;
|
||||
return {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
};
|
||||
function queryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if (lastResult?.endpointName && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || hasData && (isFetching && !lastResult?.isError || currentState.isUninitialized);
|
||||
return {
|
||||
...currentState,
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
};
|
||||
}
|
||||
function infiniteQueryStatePreSelector(currentState, lastResult, queryArgs) {
|
||||
if (lastResult?.endpointName && currentState.isUninitialized) {
|
||||
const {
|
||||
endpointName
|
||||
} = lastResult;
|
||||
const endpointDefinition = endpointDefinitions[endpointName];
|
||||
if (queryArgs !== skipToken && serializeQueryArgs({
|
||||
queryArgs: lastResult.originalArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
}) === serializeQueryArgs({
|
||||
queryArgs,
|
||||
endpointDefinition,
|
||||
endpointName
|
||||
})) lastResult = void 0;
|
||||
}
|
||||
let data = currentState.isSuccess ? currentState.data : lastResult?.data;
|
||||
if (data === void 0) data = currentState.data;
|
||||
const hasData = data !== void 0;
|
||||
const isFetching = currentState.isLoading;
|
||||
const isLoading = (!lastResult || lastResult.isLoading || lastResult.isUninitialized) && !hasData && isFetching;
|
||||
const isSuccess = currentState.isSuccess || isFetching && hasData;
|
||||
return {
|
||||
...currentState,
|
||||
data,
|
||||
currentData: currentState.data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isSuccess
|
||||
};
|
||||
}
|
||||
function usePrefetch(endpointName, defaultOptions) {
|
||||
const dispatch = useDispatch();
|
||||
const stableDefaultOptions = useShallowStableValue(defaultOptions);
|
||||
return useCallback((arg, options) => dispatch(api.util.prefetch(endpointName, arg, {
|
||||
...stableDefaultOptions,
|
||||
...options
|
||||
})), [endpointName, dispatch, stableDefaultOptions]);
|
||||
}
|
||||
function useQuerySubscriptionCommonImpl(endpointName, arg, {
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
refetchOnMountOrArgChange,
|
||||
skip = false,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false,
|
||||
...rest
|
||||
} = {}) {
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const subscriptionSelectorsRef = useRef(void 0);
|
||||
if (!subscriptionSelectorsRef.current) {
|
||||
const returnedValue = dispatch(api.internalActions.internal_getRTKQSubscriptions());
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (typeof returnedValue !== "object" || typeof returnedValue?.type === "string") {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage(37) : `Warning: Middleware for RTK-Query API at reducerPath "${api.reducerPath}" has not been added to the store.
|
||||
You must add the middleware for RTK-Query to function correctly!`);
|
||||
}
|
||||
}
|
||||
subscriptionSelectorsRef.current = returnedValue;
|
||||
}
|
||||
const stableArg = useStableQueryArgs(skip ? skipToken : arg);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
const initialPageParam = rest.initialPageParam;
|
||||
const stableInitialPageParam = useShallowStableValue(initialPageParam);
|
||||
const refetchCachedPages = rest.refetchCachedPages;
|
||||
const stableRefetchCachedPages = useShallowStableValue(refetchCachedPages);
|
||||
const promiseRef = useRef(void 0);
|
||||
let {
|
||||
queryCacheKey,
|
||||
requestId
|
||||
} = promiseRef.current || {};
|
||||
let currentRenderHasSubscription = false;
|
||||
if (queryCacheKey && requestId) {
|
||||
currentRenderHasSubscription = subscriptionSelectorsRef.current.isRequestSubscribed(queryCacheKey, requestId);
|
||||
}
|
||||
const subscriptionRemoved = !currentRenderHasSubscription && promiseRef.current !== void 0;
|
||||
usePossiblyImmediateEffect(() => {
|
||||
if (subscriptionRemoved) {
|
||||
promiseRef.current = void 0;
|
||||
}
|
||||
}, [subscriptionRemoved]);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
const lastPromise = promiseRef.current;
|
||||
if (typeof process !== "undefined" && process.env.NODE_ENV === "removeMeOnCompilation") {
|
||||
console.log(subscriptionRemoved);
|
||||
}
|
||||
if (stableArg === skipToken) {
|
||||
lastPromise?.unsubscribe();
|
||||
promiseRef.current = void 0;
|
||||
return;
|
||||
}
|
||||
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
|
||||
if (!lastPromise || lastPromise.arg !== stableArg) {
|
||||
lastPromise?.unsubscribe();
|
||||
const promise = dispatch(initiate(stableArg, {
|
||||
subscriptionOptions: stableSubscriptionOptions,
|
||||
forceRefetch: refetchOnMountOrArgChange,
|
||||
...isInfiniteQueryDefinition(endpointDefinitions[endpointName]) ? {
|
||||
initialPageParam: stableInitialPageParam,
|
||||
refetchCachedPages: stableRefetchCachedPages
|
||||
} : {}
|
||||
}));
|
||||
promiseRef.current = promise;
|
||||
} else if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
lastPromise.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [dispatch, initiate, refetchOnMountOrArgChange, stableArg, stableSubscriptionOptions, subscriptionRemoved, stableInitialPageParam, stableRefetchCachedPages, endpointName]);
|
||||
return [promiseRef, dispatch, initiate, stableSubscriptionOptions];
|
||||
}
|
||||
function buildUseQueryState(endpointName, preSelector) {
|
||||
const useQueryState = (arg, {
|
||||
skip = false,
|
||||
selectFromResult
|
||||
} = {}) => {
|
||||
const {
|
||||
select
|
||||
} = api.endpoints[endpointName];
|
||||
const stableArg = useStableQueryArgs(skip ? skipToken : arg);
|
||||
const lastValue = useRef(void 0);
|
||||
const selectDefaultResult = useMemo(() => (
|
||||
// Normally ts-ignores are bad and should be avoided, but we're
|
||||
// already casting this selector to be `Selector<any>` anyway,
|
||||
// so the inconsistencies don't matter here
|
||||
// @ts-ignore
|
||||
createSelector([
|
||||
// @ts-ignore
|
||||
select(stableArg),
|
||||
(_, lastResult) => lastResult,
|
||||
(_) => stableArg
|
||||
], preSelector, {
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: shallowEqual
|
||||
}
|
||||
})
|
||||
), [select, stableArg]);
|
||||
const querySelector = useMemo(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult, {
|
||||
devModeChecks: {
|
||||
identityFunctionCheck: "never"
|
||||
}
|
||||
}) : selectDefaultResult, [selectDefaultResult, selectFromResult]);
|
||||
const currentState = useSelector((state) => querySelector(state, lastValue.current), shallowEqual);
|
||||
const store = useStore();
|
||||
const newLastValue = selectDefaultResult(store.getState(), lastValue.current);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
lastValue.current = newLastValue;
|
||||
}, [newLastValue]);
|
||||
return currentState;
|
||||
};
|
||||
return useQueryState;
|
||||
}
|
||||
function usePromiseRefUnsubscribeOnUnmount(promiseRef) {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = void 0;
|
||||
};
|
||||
}, [promiseRef]);
|
||||
}
|
||||
function refetchOrErrorIfUnmounted(promiseRef) {
|
||||
if (!promiseRef.current) throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage2(38) : "Cannot refetch a query that has not been started yet.");
|
||||
return promiseRef.current.refetch();
|
||||
}
|
||||
function buildQueryHooks(endpointName) {
|
||||
const useQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
return useMemo(() => ({
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch: () => refetchOrErrorIfUnmounted(promiseRef)
|
||||
}), [promiseRef]);
|
||||
};
|
||||
const useLazyQuerySubscription = ({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval = 0,
|
||||
skipPollingIfUnfocused = false
|
||||
} = {}) => {
|
||||
const {
|
||||
initiate
|
||||
} = api.endpoints[endpointName];
|
||||
const dispatch = useDispatch();
|
||||
const [arg, setArg] = useState(UNINITIALIZED_VALUE);
|
||||
const promiseRef = useRef(void 0);
|
||||
const stableSubscriptionOptions = useShallowStableValue({
|
||||
refetchOnReconnect,
|
||||
refetchOnFocus,
|
||||
pollingInterval,
|
||||
skipPollingIfUnfocused
|
||||
});
|
||||
usePossiblyImmediateEffect(() => {
|
||||
const lastSubscriptionOptions = promiseRef.current?.subscriptionOptions;
|
||||
if (stableSubscriptionOptions !== lastSubscriptionOptions) {
|
||||
promiseRef.current?.updateSubscriptionOptions(stableSubscriptionOptions);
|
||||
}
|
||||
}, [stableSubscriptionOptions]);
|
||||
const subscriptionOptionsRef = useRef(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const trigger = useCallback(function(arg2, preferCacheValue = false) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
forceRefetch: !preferCacheValue
|
||||
}));
|
||||
setArg(arg2);
|
||||
});
|
||||
return promise;
|
||||
}, [dispatch, initiate]);
|
||||
const reset = useCallback(() => {
|
||||
if (promiseRef.current?.queryCacheKey) {
|
||||
dispatch(api.internalActions.removeQueryResult({
|
||||
queryCacheKey: promiseRef.current?.queryCacheKey
|
||||
}));
|
||||
}
|
||||
}, [dispatch]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (arg !== UNINITIALIZED_VALUE && !promiseRef.current) {
|
||||
trigger(arg, true);
|
||||
}
|
||||
}, [arg, trigger]);
|
||||
return useMemo(() => [trigger, arg, {
|
||||
reset
|
||||
}], [trigger, arg, reset]);
|
||||
};
|
||||
const useQueryState = buildUseQueryState(endpointName, queryStatePreSelector);
|
||||
return {
|
||||
useQueryState,
|
||||
useQuerySubscription,
|
||||
useLazyQuerySubscription,
|
||||
useLazyQuery(options) {
|
||||
const [trigger, arg, {
|
||||
reset
|
||||
}] = useLazyQuerySubscription(options);
|
||||
const queryStateResults = useQueryState(arg, {
|
||||
...options,
|
||||
skip: arg === UNINITIALIZED_VALUE
|
||||
});
|
||||
const info = useMemo(() => ({
|
||||
lastArg: arg
|
||||
}), [arg]);
|
||||
return useMemo(() => [trigger, {
|
||||
...queryStateResults,
|
||||
reset
|
||||
}, info], [trigger, queryStateResults, reset, info]);
|
||||
},
|
||||
useQuery(arg, options) {
|
||||
const querySubscriptionResults = useQuerySubscription(arg, options);
|
||||
const queryStateResults = useQueryState(arg, {
|
||||
selectFromResult: arg === skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
|
||||
...options
|
||||
});
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS);
|
||||
useDebugValue(debugValue);
|
||||
return useMemo(() => ({
|
||||
...queryStateResults,
|
||||
...querySubscriptionResults
|
||||
}), [queryStateResults, querySubscriptionResults]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildInfiniteQueryHooks(endpointName) {
|
||||
const useInfiniteQuerySubscription = (arg, options = {}) => {
|
||||
const [promiseRef, dispatch, initiate, stableSubscriptionOptions] = useQuerySubscriptionCommonImpl(endpointName, arg, options);
|
||||
const subscriptionOptionsRef = useRef(stableSubscriptionOptions);
|
||||
usePossiblyImmediateEffect(() => {
|
||||
subscriptionOptionsRef.current = stableSubscriptionOptions;
|
||||
}, [stableSubscriptionOptions]);
|
||||
const hookRefetchCachedPages = options.refetchCachedPages;
|
||||
const stableHookRefetchCachedPages = useShallowStableValue(hookRefetchCachedPages);
|
||||
const trigger = useCallback(function(arg2, direction) {
|
||||
let promise;
|
||||
batch(() => {
|
||||
unsubscribePromiseRef(promiseRef);
|
||||
promiseRef.current = promise = dispatch(initiate(arg2, {
|
||||
subscriptionOptions: subscriptionOptionsRef.current,
|
||||
direction
|
||||
}));
|
||||
});
|
||||
return promise;
|
||||
}, [promiseRef, dispatch, initiate]);
|
||||
usePromiseRefUnsubscribeOnUnmount(promiseRef);
|
||||
const stableArg = useStableQueryArgs(options.skip ? skipToken : arg);
|
||||
const refetch = useCallback((options2) => {
|
||||
if (!promiseRef.current) throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage3(38) : "Cannot refetch a query that has not been started yet.");
|
||||
const mergedOptions = {
|
||||
refetchCachedPages: options2?.refetchCachedPages ?? stableHookRefetchCachedPages
|
||||
};
|
||||
return promiseRef.current.refetch(mergedOptions);
|
||||
}, [promiseRef, stableHookRefetchCachedPages]);
|
||||
return useMemo(() => {
|
||||
const fetchNextPage = () => {
|
||||
return trigger(stableArg, "forward");
|
||||
};
|
||||
const fetchPreviousPage = () => {
|
||||
return trigger(stableArg, "backward");
|
||||
};
|
||||
return {
|
||||
trigger,
|
||||
/**
|
||||
* A method to manually refetch data for the query
|
||||
*/
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
};
|
||||
}, [refetch, trigger, stableArg]);
|
||||
};
|
||||
const useInfiniteQueryState = buildUseQueryState(endpointName, infiniteQueryStatePreSelector);
|
||||
return {
|
||||
useInfiniteQueryState,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQuery(arg, options) {
|
||||
const {
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage
|
||||
} = useInfiniteQuerySubscription(arg, options);
|
||||
const queryStateResults = useInfiniteQueryState(arg, {
|
||||
selectFromResult: arg === skipToken || options?.skip ? void 0 : noPendingQueryStateSelector,
|
||||
...options
|
||||
});
|
||||
const debugValue = pick(queryStateResults, ...COMMON_HOOK_DEBUG_FIELDS, "hasNextPage", "hasPreviousPage");
|
||||
useDebugValue(debugValue);
|
||||
return useMemo(() => ({
|
||||
...queryStateResults,
|
||||
fetchNextPage,
|
||||
fetchPreviousPage,
|
||||
refetch
|
||||
}), [queryStateResults, fetchNextPage, fetchPreviousPage, refetch]);
|
||||
}
|
||||
};
|
||||
}
|
||||
function buildMutationHook(name) {
|
||||
return ({
|
||||
selectFromResult,
|
||||
fixedCacheKey
|
||||
} = {}) => {
|
||||
const {
|
||||
select,
|
||||
initiate
|
||||
} = api.endpoints[name];
|
||||
const dispatch = useDispatch();
|
||||
const [promise, setPromise] = useState();
|
||||
useEffect(() => () => {
|
||||
if (!promise?.arg.fixedCacheKey) {
|
||||
promise?.reset();
|
||||
}
|
||||
}, [promise]);
|
||||
const triggerMutation = useCallback(function(arg) {
|
||||
const promise2 = dispatch(initiate(arg, {
|
||||
fixedCacheKey
|
||||
}));
|
||||
setPromise(promise2);
|
||||
return promise2;
|
||||
}, [dispatch, initiate, fixedCacheKey]);
|
||||
const {
|
||||
requestId
|
||||
} = promise || {};
|
||||
const selectDefaultResult = useMemo(() => select({
|
||||
fixedCacheKey,
|
||||
requestId: promise?.requestId
|
||||
}), [fixedCacheKey, promise, select]);
|
||||
const mutationSelector = useMemo(() => selectFromResult ? createSelector([selectDefaultResult], selectFromResult) : selectDefaultResult, [selectFromResult, selectDefaultResult]);
|
||||
const currentState = useSelector(mutationSelector, shallowEqual);
|
||||
const originalArgs = fixedCacheKey == null ? promise?.arg.originalArgs : void 0;
|
||||
const reset = useCallback(() => {
|
||||
batch(() => {
|
||||
if (promise) {
|
||||
setPromise(void 0);
|
||||
}
|
||||
if (fixedCacheKey) {
|
||||
dispatch(api.internalActions.removeMutationResult({
|
||||
requestId,
|
||||
fixedCacheKey
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [dispatch, fixedCacheKey, promise, requestId]);
|
||||
const debugValue = pick(currentState, ...COMMON_HOOK_DEBUG_FIELDS, "endpointName");
|
||||
useDebugValue(debugValue);
|
||||
const finalState = useMemo(() => ({
|
||||
...currentState,
|
||||
originalArgs,
|
||||
reset
|
||||
}), [currentState, originalArgs, reset]);
|
||||
return useMemo(() => [triggerMutation, finalState], [triggerMutation, finalState]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// src/query/react/module.ts
|
||||
var reactHooksModuleName = /* @__PURE__ */ Symbol();
|
||||
var reactHooksModule = ({
|
||||
batch = rrBatch,
|
||||
hooks = {
|
||||
useDispatch: rrUseDispatch,
|
||||
useSelector: rrUseSelector,
|
||||
useStore: rrUseStore
|
||||
},
|
||||
createSelector = _createSelector,
|
||||
unstable__sideEffectsInRender = false,
|
||||
...rest
|
||||
} = {}) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
const hookNames = ["useDispatch", "useSelector", "useStore"];
|
||||
let warned = false;
|
||||
for (const hookName of hookNames) {
|
||||
if (countObjectKeys(rest) > 0) {
|
||||
if (rest[hookName]) {
|
||||
if (!warned) {
|
||||
console.warn("As of RTK 2.0, the hooks now need to be specified as one object, provided under a `hooks` key:\n`reactHooksModule({ hooks: { useDispatch, useSelector, useStore } })`");
|
||||
warned = true;
|
||||
}
|
||||
}
|
||||
hooks[hookName] = rest[hookName];
|
||||
}
|
||||
if (typeof hooks[hookName] !== "function") {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage4(36) : `When using custom hooks for context, all ${hookNames.length} hooks need to be provided: ${hookNames.join(", ")}.
|
||||
Hook ${hookName} was either not provided or not a function.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
name: reactHooksModuleName,
|
||||
init(api, {
|
||||
serializeQueryArgs
|
||||
}, context) {
|
||||
const anyApi = api;
|
||||
const {
|
||||
buildQueryHooks,
|
||||
buildInfiniteQueryHooks,
|
||||
buildMutationHook,
|
||||
usePrefetch
|
||||
} = buildHooks({
|
||||
api,
|
||||
moduleOptions: {
|
||||
batch,
|
||||
hooks,
|
||||
unstable__sideEffectsInRender,
|
||||
createSelector
|
||||
},
|
||||
serializeQueryArgs,
|
||||
context
|
||||
});
|
||||
safeAssign(anyApi, {
|
||||
usePrefetch
|
||||
});
|
||||
safeAssign(context, {
|
||||
batch
|
||||
});
|
||||
return {
|
||||
injectEndpoint(endpointName, definition) {
|
||||
if (isQueryDefinition(definition)) {
|
||||
const {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
} = buildQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useQuery,
|
||||
useLazyQuery,
|
||||
useLazyQuerySubscription,
|
||||
useQueryState,
|
||||
useQuerySubscription
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Query`] = useQuery;
|
||||
api[`useLazy${capitalize(endpointName)}Query`] = useLazyQuery;
|
||||
}
|
||||
if (isMutationDefinition(definition)) {
|
||||
const useMutation = buildMutationHook(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useMutation
|
||||
});
|
||||
api[`use${capitalize(endpointName)}Mutation`] = useMutation;
|
||||
} else if (isInfiniteQueryDefinition(definition)) {
|
||||
const {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
} = buildInfiniteQueryHooks(endpointName);
|
||||
safeAssign(anyApi.endpoints[endpointName], {
|
||||
useInfiniteQuery,
|
||||
useInfiniteQuerySubscription,
|
||||
useInfiniteQueryState
|
||||
});
|
||||
api[`use${capitalize(endpointName)}InfiniteQuery`] = useInfiniteQuery;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// src/query/react/index.ts
|
||||
export * from "@reduxjs/toolkit/query";
|
||||
|
||||
// src/query/react/ApiProvider.tsx
|
||||
import { configureStore, formatProdErrorMessage as _formatProdErrorMessage5 } from "@reduxjs/toolkit";
|
||||
import * as React from "react";
|
||||
function ApiProvider(props) {
|
||||
const context = props.context || ReactReduxContext;
|
||||
const existingContext = useContext(context);
|
||||
if (existingContext) {
|
||||
throw new Error(process.env.NODE_ENV === "production" ? _formatProdErrorMessage5(35) : "Existing Redux context detected. If you already have a store set up, please use the traditional Redux setup.");
|
||||
}
|
||||
const [store] = React.useState(() => configureStore({
|
||||
reducer: {
|
||||
[props.api.reducerPath]: props.api.reducer
|
||||
},
|
||||
middleware: (gDM) => gDM().concat(props.api.middleware)
|
||||
}));
|
||||
useEffect(() => props.setupListeners === false ? void 0 : setupListeners(store.dispatch, props.setupListeners), [props.setupListeners, store.dispatch]);
|
||||
return /* @__PURE__ */ React.createElement(Provider, { store, context }, props.children);
|
||||
}
|
||||
|
||||
// src/query/react/index.ts
|
||||
var createApi = /* @__PURE__ */ buildCreateApi(coreModule(), reactHooksModule());
|
||||
export {
|
||||
ApiProvider,
|
||||
UNINITIALIZED_VALUE,
|
||||
createApi,
|
||||
reactHooksModule,
|
||||
reactHooksModuleName
|
||||
};
|
||||
//# sourceMappingURL=rtk-query-react.modern.mjs.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.modern.mjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/react/rtk-query-react.modern.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.browser.mjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.browser.mjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.browser.mjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.browser.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3116
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.legacy-esm.js
generated
vendored
Normal file
3116
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.legacy-esm.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.legacy-esm.js.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.legacy-esm.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3047
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.modern.mjs
generated
vendored
Normal file
3047
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.modern.mjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.modern.mjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/query/rtk-query.modern.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/index.js
generated
vendored
Normal file
6
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/index.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./redux-toolkit-react.production.min.cjs')
|
||||
} else {
|
||||
module.exports = require('./redux-toolkit-react.development.cjs')
|
||||
}
|
||||
55
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.development.cjs
generated
vendored
Normal file
55
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.development.cjs
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/react/index.ts
|
||||
var react_exports = {};
|
||||
__export(react_exports, {
|
||||
createDynamicMiddleware: () => createDynamicMiddleware
|
||||
});
|
||||
module.exports = __toCommonJS(react_exports);
|
||||
__reExport(react_exports, require("@reduxjs/toolkit"), module.exports);
|
||||
|
||||
// src/dynamicMiddleware/react/index.ts
|
||||
var import_toolkit = require("@reduxjs/toolkit");
|
||||
var import_react_redux = require("react-redux");
|
||||
var createDynamicMiddleware = () => {
|
||||
const instance = (0, import_toolkit.createDynamicMiddleware)();
|
||||
const createDispatchWithMiddlewareHookFactory = (context = import_react_redux.ReactReduxContext) => {
|
||||
const useDispatch = context === import_react_redux.ReactReduxContext ? import_react_redux.useDispatch : (0, import_react_redux.createDispatchHook)(context);
|
||||
function createDispatchWithMiddlewareHook2(...middlewares) {
|
||||
instance.addMiddleware(...middlewares);
|
||||
return useDispatch;
|
||||
}
|
||||
createDispatchWithMiddlewareHook2.withTypes = () => createDispatchWithMiddlewareHook2;
|
||||
return createDispatchWithMiddlewareHook2;
|
||||
};
|
||||
const createDispatchWithMiddlewareHook = createDispatchWithMiddlewareHookFactory();
|
||||
return {
|
||||
...instance,
|
||||
createDispatchWithMiddlewareHookFactory,
|
||||
createDispatchWithMiddlewareHook
|
||||
};
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
createDynamicMiddleware,
|
||||
...require("@reduxjs/toolkit")
|
||||
});
|
||||
//# sourceMappingURL=redux-toolkit-react.development.cjs.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.development.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.development.cjs.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/react/index.ts","../../../src/dynamicMiddleware/react/index.ts"],"sourcesContent":["// This must remain here so that the `mangleErrors.cjs` build script\n// does not have to import this into each source file it rewrites.\nimport { formatProdErrorMessage } from '@reduxjs/toolkit';\nexport * from '@reduxjs/toolkit';\nexport { createDynamicMiddleware } from '../dynamicMiddleware/react';\nexport type { CreateDispatchWithMiddlewareHook } from '../dynamicMiddleware/react/index';","import type { DynamicMiddlewareInstance, GetDispatch, GetState, MiddlewareApiConfig, TSHelpersExtractDispatchExtensions } from '@reduxjs/toolkit';\nimport { createDynamicMiddleware as cDM } from '@reduxjs/toolkit';\nimport type { Context } from 'react';\nimport type { ReactReduxContextValue } from 'react-redux';\nimport { createDispatchHook, ReactReduxContext, useDispatch as useDefaultDispatch } from 'react-redux';\nimport type { Action, Dispatch, Middleware, UnknownAction } from 'redux';\nexport type UseDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[] = [], State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = () => TSHelpersExtractDispatchExtensions<Middlewares> & DispatchType;\nexport type CreateDispatchWithMiddlewareHook<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = {\n <Middlewares extends [Middleware<any, State, DispatchType>, ...Middleware<any, State, DispatchType>[]]>(...middlewares: Middlewares): UseDispatchWithMiddlewareHook<Middlewares, State, DispatchType>;\n withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): CreateDispatchWithMiddlewareHook<GetState<MiddlewareConfig>, GetDispatch<MiddlewareConfig>>;\n};\ntype ActionFromDispatch<DispatchType extends Dispatch<Action>> = DispatchType extends Dispatch<infer Action> ? Action : never;\ntype ReactDynamicMiddlewareInstance<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = DynamicMiddlewareInstance<State, DispatchType> & {\n createDispatchWithMiddlewareHookFactory: (context?: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null>) => CreateDispatchWithMiddlewareHook<State, DispatchType>;\n createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;\n};\nexport const createDynamicMiddleware = <State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): ReactDynamicMiddlewareInstance<State, DispatchType> => {\n const instance = cDM<State, DispatchType>();\n const createDispatchWithMiddlewareHookFactory = (\n // @ts-ignore\n context: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null> = ReactReduxContext) => {\n const useDispatch = context === ReactReduxContext ? useDefaultDispatch : createDispatchHook(context);\n function createDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[]>(...middlewares: Middlewares) {\n instance.addMiddleware(...middlewares);\n return useDispatch;\n }\n createDispatchWithMiddlewareHook.withTypes = () => createDispatchWithMiddlewareHook;\n return createDispatchWithMiddlewareHook as CreateDispatchWithMiddlewareHook<State, DispatchType>;\n };\n const createDispatchWithMiddlewareHook = createDispatchWithMiddlewareHookFactory();\n return {\n ...instance,\n createDispatchWithMiddlewareHookFactory,\n createDispatchWithMiddlewareHook\n };\n};"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,0BAAc,6BAHd;;;ACCA,qBAA+C;AAG/C,yBAAyF;AAYlF,IAAM,0BAA0B,MAAgJ;AACrL,QAAM,eAAW,eAAAA,yBAAyB;AAC1C,QAAM,0CAA0C,CAEhD,UAA2F,yCAAsB;AAC/G,UAAM,cAAc,YAAY,uCAAoB,mBAAAC,kBAAqB,uCAAmB,OAAO;AACnG,aAASC,qCAAgG,aAA0B;AACjI,eAAS,cAAc,GAAG,WAAW;AACrC,aAAO;AAAA,IACT;AACA,IAAAA,kCAAiC,YAAY,MAAMA;AACnD,WAAOA;AAAA,EACT;AACA,QAAM,mCAAmC,wCAAwC;AACjF,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,EACF;AACF;","names":["cDM","useDefaultDispatch","createDispatchWithMiddlewareHook"]}
|
||||
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.production.min.cjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.production.min.cjs
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";var s=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var x=(t,e)=>{for(var a in e)s(t,a,{get:e[a],enumerable:!0})},d=(t,e,a,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of y(e))!M.call(t,i)&&i!==a&&s(t,i,{get:()=>e[i],enumerable:!(n=w(e,i))||n.enumerable});return t},r=(t,e,a)=>(d(t,e,"default"),a&&d(a,e,"default"));var m=t=>d(s({},"__esModule",{value:!0}),t);var o={};x(o,{createDynamicMiddleware:()=>D});module.exports=m(o);r(o,require("@reduxjs/toolkit"),module.exports);var h=require("@reduxjs/toolkit"),c=require("react-redux"),D=()=>{let t=(0,h.createDynamicMiddleware)(),e=(n=c.ReactReduxContext)=>{let i=n===c.ReactReduxContext?c.useDispatch:(0,c.createDispatchHook)(n);function p(...l){return t.addMiddleware(...l),i}return p.withTypes=()=>p,p},a=e();return{...t,createDispatchWithMiddlewareHookFactory:e,createDispatchWithMiddlewareHook:a}};0&&(module.exports={createDynamicMiddleware,...require("@reduxjs/toolkit")});
|
||||
//# sourceMappingURL=redux-toolkit-react.production.min.cjs.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.production.min.cjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/cjs/redux-toolkit-react.production.min.cjs.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/react/index.ts","../../../src/dynamicMiddleware/react/index.ts"],"sourcesContent":["// This must remain here so that the `mangleErrors.cjs` build script\n// does not have to import this into each source file it rewrites.\nimport { formatProdErrorMessage } from '@reduxjs/toolkit';\nexport * from '@reduxjs/toolkit';\nexport { createDynamicMiddleware } from '../dynamicMiddleware/react';\nexport type { CreateDispatchWithMiddlewareHook } from '../dynamicMiddleware/react/index';","import type { DynamicMiddlewareInstance, GetDispatch, GetState, MiddlewareApiConfig, TSHelpersExtractDispatchExtensions } from '@reduxjs/toolkit';\nimport { createDynamicMiddleware as cDM } from '@reduxjs/toolkit';\nimport type { Context } from 'react';\nimport type { ReactReduxContextValue } from 'react-redux';\nimport { createDispatchHook, ReactReduxContext, useDispatch as useDefaultDispatch } from 'react-redux';\nimport type { Action, Dispatch, Middleware, UnknownAction } from 'redux';\nexport type UseDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[] = [], State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = () => TSHelpersExtractDispatchExtensions<Middlewares> & DispatchType;\nexport type CreateDispatchWithMiddlewareHook<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = {\n <Middlewares extends [Middleware<any, State, DispatchType>, ...Middleware<any, State, DispatchType>[]]>(...middlewares: Middlewares): UseDispatchWithMiddlewareHook<Middlewares, State, DispatchType>;\n withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): CreateDispatchWithMiddlewareHook<GetState<MiddlewareConfig>, GetDispatch<MiddlewareConfig>>;\n};\ntype ActionFromDispatch<DispatchType extends Dispatch<Action>> = DispatchType extends Dispatch<infer Action> ? Action : never;\ntype ReactDynamicMiddlewareInstance<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = DynamicMiddlewareInstance<State, DispatchType> & {\n createDispatchWithMiddlewareHookFactory: (context?: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null>) => CreateDispatchWithMiddlewareHook<State, DispatchType>;\n createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;\n};\nexport const createDynamicMiddleware = <State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): ReactDynamicMiddlewareInstance<State, DispatchType> => {\n const instance = cDM<State, DispatchType>();\n const createDispatchWithMiddlewareHookFactory = (\n // @ts-ignore\n context: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null> = ReactReduxContext) => {\n const useDispatch = context === ReactReduxContext ? useDefaultDispatch : createDispatchHook(context);\n function createDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[]>(...middlewares: Middlewares) {\n instance.addMiddleware(...middlewares);\n return useDispatch;\n }\n createDispatchWithMiddlewareHook.withTypes = () => createDispatchWithMiddlewareHook;\n return createDispatchWithMiddlewareHook as CreateDispatchWithMiddlewareHook<State, DispatchType>;\n };\n const createDispatchWithMiddlewareHook = createDispatchWithMiddlewareHookFactory();\n return {\n ...instance,\n createDispatchWithMiddlewareHookFactory,\n createDispatchWithMiddlewareHook\n };\n};"],"mappings":"2dAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,6BAAAE,IAAA,eAAAC,EAAAH,GAGAI,EAAAJ,EAAc,4BAHd,gBCCA,IAAAK,EAA+C,4BAG/CC,EAAyF,uBAY5EC,EAA0B,IAAgJ,CACrL,IAAMC,KAAW,EAAAC,yBAAyB,EACpCC,EAA0C,CAEhDC,EAA2F,sBAAsB,CAC/G,IAAMC,EAAcD,IAAY,oBAAoB,EAAAE,eAAqB,sBAAmBF,CAAO,EACnG,SAASG,KAAgGC,EAA0B,CACjI,OAAAP,EAAS,cAAc,GAAGO,CAAW,EAC9BH,CACT,CACA,OAAAE,EAAiC,UAAY,IAAMA,EAC5CA,CACT,EACMA,EAAmCJ,EAAwC,EACjF,MAAO,CACL,GAAGF,EACH,wCAAAE,EACA,iCAAAI,CACF,CACF","names":["react_exports","__export","createDynamicMiddleware","__toCommonJS","__reExport","import_toolkit","import_react_redux","createDynamicMiddleware","instance","cDM","createDispatchWithMiddlewareHookFactory","context","useDispatch","useDefaultDispatch","createDispatchWithMiddlewareHook","middlewares"]}
|
||||
22
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/index.d.mts
generated
vendored
Normal file
22
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/index.d.mts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DynamicMiddlewareInstance, TSHelpersExtractDispatchExtensions, MiddlewareApiConfig, GetState, GetDispatch } from '@reduxjs/toolkit';
|
||||
export * from '@reduxjs/toolkit';
|
||||
import { Context } from 'react';
|
||||
import { ReactReduxContextValue } from 'react-redux';
|
||||
import { Dispatch, UnknownAction, Action, Middleware } from 'redux';
|
||||
|
||||
type UseDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[] = [], State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = () => TSHelpersExtractDispatchExtensions<Middlewares> & DispatchType;
|
||||
type CreateDispatchWithMiddlewareHook<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = {
|
||||
<Middlewares extends [
|
||||
Middleware<any, State, DispatchType>,
|
||||
...Middleware<any, State, DispatchType>[]
|
||||
]>(...middlewares: Middlewares): UseDispatchWithMiddlewareHook<Middlewares, State, DispatchType>;
|
||||
withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): CreateDispatchWithMiddlewareHook<GetState<MiddlewareConfig>, GetDispatch<MiddlewareConfig>>;
|
||||
};
|
||||
type ActionFromDispatch<DispatchType extends Dispatch<Action>> = DispatchType extends Dispatch<infer Action> ? Action : never;
|
||||
type ReactDynamicMiddlewareInstance<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = DynamicMiddlewareInstance<State, DispatchType> & {
|
||||
createDispatchWithMiddlewareHookFactory: (context?: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null>) => CreateDispatchWithMiddlewareHook<State, DispatchType>;
|
||||
createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;
|
||||
};
|
||||
declare const createDynamicMiddleware: <State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>() => ReactDynamicMiddlewareInstance<State, DispatchType>;
|
||||
|
||||
export { type CreateDispatchWithMiddlewareHook, createDynamicMiddleware };
|
||||
22
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/index.d.ts
generated
vendored
Normal file
22
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DynamicMiddlewareInstance, TSHelpersExtractDispatchExtensions, MiddlewareApiConfig, GetState, GetDispatch } from '@reduxjs/toolkit';
|
||||
export * from '@reduxjs/toolkit';
|
||||
import { Context } from 'react';
|
||||
import { ReactReduxContextValue } from 'react-redux';
|
||||
import { Dispatch, UnknownAction, Action, Middleware } from 'redux';
|
||||
|
||||
type UseDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[] = [], State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = () => TSHelpersExtractDispatchExtensions<Middlewares> & DispatchType;
|
||||
type CreateDispatchWithMiddlewareHook<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = {
|
||||
<Middlewares extends [
|
||||
Middleware<any, State, DispatchType>,
|
||||
...Middleware<any, State, DispatchType>[]
|
||||
]>(...middlewares: Middlewares): UseDispatchWithMiddlewareHook<Middlewares, State, DispatchType>;
|
||||
withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): CreateDispatchWithMiddlewareHook<GetState<MiddlewareConfig>, GetDispatch<MiddlewareConfig>>;
|
||||
};
|
||||
type ActionFromDispatch<DispatchType extends Dispatch<Action>> = DispatchType extends Dispatch<infer Action> ? Action : never;
|
||||
type ReactDynamicMiddlewareInstance<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = DynamicMiddlewareInstance<State, DispatchType> & {
|
||||
createDispatchWithMiddlewareHookFactory: (context?: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null>) => CreateDispatchWithMiddlewareHook<State, DispatchType>;
|
||||
createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;
|
||||
};
|
||||
declare const createDynamicMiddleware: <State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>() => ReactDynamicMiddlewareInstance<State, DispatchType>;
|
||||
|
||||
export { type CreateDispatchWithMiddlewareHook, createDynamicMiddleware };
|
||||
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/redux-toolkit-react.browser.mjs
generated
vendored
Normal file
2
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/redux-toolkit-react.browser.mjs
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export*from"@reduxjs/toolkit";import{createDynamicMiddleware as p}from"@reduxjs/toolkit";import{createDispatchHook as d,ReactReduxContext as c,useDispatch as s}from"react-redux";var h=()=>{let t=p(),a=(i=c)=>{let o=i===c?s:d(i);function e(...r){return t.addMiddleware(...r),o}return e.withTypes=()=>e,e},n=a();return{...t,createDispatchWithMiddlewareHookFactory:a,createDispatchWithMiddlewareHook:n}};export{h as createDynamicMiddleware};
|
||||
//# sourceMappingURL=redux-toolkit-react.browser.mjs.map
|
||||
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/redux-toolkit-react.browser.mjs.map
generated
vendored
Normal file
1
front-end-agency/front-end-agency/node_modules/@reduxjs/toolkit/dist/react/redux-toolkit-react.browser.mjs.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../src/react/index.ts","../../src/dynamicMiddleware/react/index.ts"],"sourcesContent":["// This must remain here so that the `mangleErrors.cjs` build script\n// does not have to import this into each source file it rewrites.\nimport { formatProdErrorMessage } from '@reduxjs/toolkit';\nexport * from '@reduxjs/toolkit';\nexport { createDynamicMiddleware } from '../dynamicMiddleware/react';\nexport type { CreateDispatchWithMiddlewareHook } from '../dynamicMiddleware/react/index';","import type { DynamicMiddlewareInstance, GetDispatch, GetState, MiddlewareApiConfig, TSHelpersExtractDispatchExtensions } from '@reduxjs/toolkit';\nimport { createDynamicMiddleware as cDM } from '@reduxjs/toolkit';\nimport type { Context } from 'react';\nimport type { ReactReduxContextValue } from 'react-redux';\nimport { createDispatchHook, ReactReduxContext, useDispatch as useDefaultDispatch } from 'react-redux';\nimport type { Action, Dispatch, Middleware, UnknownAction } from 'redux';\nexport type UseDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[] = [], State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = () => TSHelpersExtractDispatchExtensions<Middlewares> & DispatchType;\nexport type CreateDispatchWithMiddlewareHook<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = {\n <Middlewares extends [Middleware<any, State, DispatchType>, ...Middleware<any, State, DispatchType>[]]>(...middlewares: Middlewares): UseDispatchWithMiddlewareHook<Middlewares, State, DispatchType>;\n withTypes<MiddlewareConfig extends MiddlewareApiConfig>(): CreateDispatchWithMiddlewareHook<GetState<MiddlewareConfig>, GetDispatch<MiddlewareConfig>>;\n};\ntype ActionFromDispatch<DispatchType extends Dispatch<Action>> = DispatchType extends Dispatch<infer Action> ? Action : never;\ntype ReactDynamicMiddlewareInstance<State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>> = DynamicMiddlewareInstance<State, DispatchType> & {\n createDispatchWithMiddlewareHookFactory: (context?: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null>) => CreateDispatchWithMiddlewareHook<State, DispatchType>;\n createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;\n};\nexport const createDynamicMiddleware = <State = any, DispatchType extends Dispatch<UnknownAction> = Dispatch<UnknownAction>>(): ReactDynamicMiddlewareInstance<State, DispatchType> => {\n const instance = cDM<State, DispatchType>();\n const createDispatchWithMiddlewareHookFactory = (\n // @ts-ignore\n context: Context<ReactReduxContextValue<State, ActionFromDispatch<DispatchType>> | null> = ReactReduxContext) => {\n const useDispatch = context === ReactReduxContext ? useDefaultDispatch : createDispatchHook(context);\n function createDispatchWithMiddlewareHook<Middlewares extends Middleware<any, State, DispatchType>[]>(...middlewares: Middlewares) {\n instance.addMiddleware(...middlewares);\n return useDispatch;\n }\n createDispatchWithMiddlewareHook.withTypes = () => createDispatchWithMiddlewareHook;\n return createDispatchWithMiddlewareHook as CreateDispatchWithMiddlewareHook<State, DispatchType>;\n };\n const createDispatchWithMiddlewareHook = createDispatchWithMiddlewareHookFactory();\n return {\n ...instance,\n createDispatchWithMiddlewareHookFactory,\n createDispatchWithMiddlewareHook\n };\n};"],"mappings":"AAGA,WAAc,mBCFd,OAAS,2BAA2BA,MAAW,mBAG/C,OAAS,sBAAAC,EAAoB,qBAAAC,EAAmB,eAAeC,MAA0B,cAYlF,IAAMC,EAA0B,IAAgJ,CACrL,IAAMC,EAAWL,EAAyB,EACpCM,EAA0C,CAEhDC,EAA2FL,IAAsB,CAC/G,IAAMM,EAAcD,IAAYL,EAAoBC,EAAqBF,EAAmBM,CAAO,EACnG,SAASE,KAAgGC,EAA0B,CACjI,OAAAL,EAAS,cAAc,GAAGK,CAAW,EAC9BF,CACT,CACA,OAAAC,EAAiC,UAAY,IAAMA,EAC5CA,CACT,EACMA,EAAmCH,EAAwC,EACjF,MAAO,CACL,GAAGD,EACH,wCAAAC,EACA,iCAAAG,CACF,CACF","names":["cDM","createDispatchHook","ReactReduxContext","useDefaultDispatch","createDynamicMiddleware","instance","createDispatchWithMiddlewareHookFactory","context","useDispatch","createDispatchWithMiddlewareHook","middlewares"]}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user