Compare commits

...
28 Commits
Author SHA1 Message Date
rmcampos d3e6bb71bf docs: update changelog with docker images 2026-07-27 19:40:31 -03:00
rmcampos a819151772 fix: toast notification now can be clicked
Frontend CD / build (push) Successful in 3m52s
2026-07-27 19:15:04 -03:00
rmcampos a71c6fdb70 docs: update changelog with latest docker images 2026-07-27 15:18:50 -03:00
rmcampos 87d9cb31d1 feat: add category to pos by industry (#17)
Backend CD / build (push) Successful in 4m27s
Frontend CD / build (push) Successful in 3m29s
# What

- Add category to POS by Industry.

# Why

- Needed.Reviewed-on: #17
2026-07-27 18:00:05 +00:00
rmcampos 35816f0a8a hotfix: missing package
Frontend CD / build (push) Successful in 6m44s
2026-07-27 13:05:11 -03:00
rmcampos 3673f20079 hotfix: customer filter not actually working
Frontend CD / build (push) Failing after 1m19s
2026-07-27 12:58:18 -03:00
rmcampos 0dfaf65b1e docs: update changelog with docker images 2026-07-27 12:53:02 -03:00
rmcampos 2f4ea534e4 Fix: Dashboard POS by industry query and Customer filters (#16)
Backend CD / build (push) Successful in 4m20s
Frontend CD / build (push) Successful in 3m32s
# What

- Fixes the dashboard query for POS by Industry bringing only active customers.
- Fixes the Customer page filter not working properly.

# Why

- The app needs to be reliable and consistent.Reviewed-on: #16
2026-07-27 15:42:31 +00:00
rmcampos 1a5f00fd47 ci: fix wrong identation in backend-cd yml file
Backend CD / build (push) Canceled after 21s
2026-07-24 17:23:03 +02:00
rmcampos 951518b30f docs: update changelog and job summary
Frontend CD / build (push) Canceled after 1m2s
2026-07-24 17:19:53 +02:00
rmcampos c04a3c6252 feat: allow to expand pos per industry
Backend CD / build (push) Successful in 9m54s
Frontend CD / build (push) Successful in 6m46s
2026-07-24 16:56:18 +02:00
renovate-botandrmcampos d07780ec7c chore(deps): update node.js to v24 (#14)
Backend CD / build (push) Successful in 4m29s
Frontend CD / build (push) Successful in 2m28s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://github.com/nodejs/node) | stage | major | `20-alpine` → `24-alpine` |
| [node](https://github.com/nodejs/node) | final | major | `22-bookworm-slim` → `24-bookworm-slim` |
| [node](https://github.com/nodejs/node) | stage | major | `22-bookworm-slim` → `24-bookworm-slim` |

>  **Important**
>
> Release Notes retrieval for this PR were skipped because no github.com credentials were available.
> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNTIuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI1Mi4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
Reviewed-on: #14
Reviewed-by: Ricardo Campos <ricardompcampos@gmail.com>
Co-authored-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
Co-committed-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
2026-07-10 22:03:17 +00:00
rmcampos 9a8bde285d ci: add simple ci file 2026-07-10 23:52:54 +02:00
renovate-botandrmcampos 30d9962e4b chore: Configure Renovate (#12)
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

📚 See our [Reading List](https://docs.renovatebot.com/reading-list/) for relevant documentation you may be interested in reading.

🔡 Do you want to change how Renovate upgrades your dependencies? Add your custom config to `renovate.json` in this branch. Renovate will update the Pull Request description the next time it runs.

---
### Detected Package Files

 * `backend/Dockerfile` (dockerfile)
 * `frontend/Dockerfile` (dockerfile)
 * `backend/package.json` (npm)
 * `frontend/package.json` (npm)

### Configuration Summary

Based on the default config's presets, Renovate will:

  - Start dependency updates only once this onboarding PR is merged
  - Enable Renovate Dependency Dashboard creation.
  - Use semantic commit type `fix` for dependencies and `chore` for all others if semantic commits are in use.
  - Ignore `node_modules`, `bower_components`, `vendor` and various test/tests (except for nuget) directories.
  - Group known monorepo packages together.
  - Use curated list of recommended non-monorepo package groupings.
  - Show only the Age and Confidence Merge Confidence badges for pull requests.
  - Apply crowd-sourced package replacement rules.
  - Apply crowd-sourced workarounds for known problems with packages.
  - Ensure that every dependency pinned by digest and sourced from Forgejo contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from Gitea contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from GitHub.com and Github enterprise contains a link to the commit-to-commit diff
  - Ensure that every dependency pinned by digest and sourced from GitLab.com contains a link to the commit-to-commit diff
  - Correctly link to the source code for golang.org/x packages
  - Link to pkg.go.dev/... for golang.org/x packages' title
  - Provide a link to octochangelog's improved breakdown for Renovate's changelogs

---

### What to Expect

With your current configuration, Renovate will create 2 Pull Requests:

<details>
<summary>chore(deps): update dependency fastify to v5.10.0</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/fastify-5.x-lockfile`
  - Merge into: `main`
  - Upgrade [fastify](https://github.com/fastify/fastify) to `5.10.0`

</details>

<details>
<summary>chore(deps): update node.js to v24</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/node-24.x`
  - Merge into: `main`
  - Upgrade [node](https://github.com/nodejs/node) to `24-alpine`
  - Upgrade [node](https://github.com/nodejs/node) to `24-bookworm-slim`

</details>

---

 Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).

<!--renovate-config-hash:e80b4e42a3043bc12fa0640db4bac392d2bf770acf841360d7c8ceeeac2ec1a9-->

Reviewed-on: #12
Co-authored-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
Co-committed-by: Renovate Bot <renovate-bot@lightroasted.vps-kinghost.net>
2026-07-05 21:12:10 +00:00
rmcampos 8fa22fca01 docs: update changelog 2026-07-03 19:30:18 +02:00
rmcampos f44b476c2e fix: mobile ui issues (#11)
Backend CD / build (push) Successful in 7m52s
Frontend CD / build (push) Successful in 5m38s
## What

- Fixed mobile UI issues, viewport, components going offscreen.
- Bumped frontend and backend deps.

## Why

- Making the app function well, properly, and have the latest updates.

## Mood
<img width="200" src="https://media0.giphy.com/media/U6GCKHGsRHiPqP6Cn0/200w.webp?cid=36b14facakatgxdbq8gbj7evktuomyhslgjf3fvlhksaf2ga&ep=v1_gifs_search&rid=200w.webp&ct=g"/>

Reviewed-on: #11
2026-07-03 17:13:34 +00:00
rmcampos 0046dbf1a3 Update .github/workflows/deploy.yml 2026-07-03 15:37:48 +00:00
rmcampos 7d23e5f3c4 Update CHANGELOG.md 2026-07-03 15:35:49 +00:00
rmcampos cef58d9974 Merge branch 'feat/minor-improvements'
Frontend CD / build (push) Successful in 5m42s
2026-07-03 17:27:52 +02:00
rmcampos da0099b697 hotfix: silence lint issue for now. an issue will address the fix properly 2026-07-03 17:24:09 +02:00
rmcampos 9258576635 feat: minor improvements (#9)
Frontend CD / build (push) Failing after 57s
## What

- Added button to start a sale from the router stop list
- Replaced the dropdown input in the Manage POS modal in the Routes page by an input text search.

## Why

- Improvements to make the user's life easier.

## Mood
<img width="200" src="https://media2.giphy.com/media/ui1hpJSyBDWlG/giphy.gif?cid=36b14facxaosmt8cmpptl1gkbb6j2zzupsb9r2icd7va79ml&ep=v1_gifs_search&rid=giphy.gif&ct=g" />

Reviewed-on: #9
2026-07-03 15:05:19 +00:00
rmcampos 173efc2b73 feat: rename from polpa gestao to polpa go 2026-07-03 17:04:08 +02:00
rmcampos 22b2cfbe8e feat: added button to start a sale from the router stop list 2026-07-03 16:55:51 +02:00
rmcampos be20a84e49 feat: replace dropdown by input search in router manage pos modal 2026-07-03 16:36:55 +02:00
rmcampos 0271f2e156 chore: bump backend dependencies to latest version
Backend CD / build (push) Successful in 7m50s
2026-06-24 12:45:36 -03:00
rmcampos 228650e936 fix: wrong build number variable name
Frontend CD / build (push) Successful in 2m36s
2026-06-24 12:00:48 -03:00
rmcampos 96408487bc Update CHANGELOG.md 2026-06-24 14:31:08 +00:00
rmcampos aae2848012 fix: build number issue (#8)
Backend CD / build (push) Successful in 8m34s
Frontend CD / build (push) Successful in 1m34s
## What

- Fix the build number not showing up in prod correctly.
- In prod I see "dev" instead of the actual number;

## Why

- The build number helps to track the deployed version.

## Mood

<img width="200" src="https://media4.giphy.com/media/K0KUml2ebG1vYpx11l/200w.webp?cid=36b14facis4p2almweeljo1kzjohgid2jvv66z9xtxgcwvrc&ep=v1_gifs_search&rid=200w.webp&ct=g"/>

Reviewed-on: #8
2026-06-24 14:18:11 +00:00
32 changed files with 1330 additions and 441 deletions
+34 -1
View File
@@ -85,10 +85,43 @@ jobs:
docker.io/rmcampos/polpa-gestao-api:${{ steps.version.outputs.tag }}-prisma
docker.io/rmcampos/polpa-gestao-api:latest-prisma
- name: Output Docker image URLs
run: |
echo ""
echo "=========================================="
echo "🚀 Docker Images Published Successfully!"
echo "=========================================="
echo ""
echo "API Image:"
echo " rmcampos/polpa-gestao-api:${{ steps.version.outputs.tag }}"
echo " rmcampos/polpa-gestao-api:latest"
echo ""
echo "Prisma Image:"
echo " rmcampos/polpa-gestao-api:${{ steps.version.outputs.tag }}-prisma"
echo " rmcampos/polpa-gestao-api:latest-prisma"
echo ""
echo "=========================================="
{
echo "## 🚀 Docker Images Published Successfully!"
echo ""
echo "### API Image"
echo '```'
echo "rmcampos/polpa-gestao-api:${{ steps.version.outputs.tag }}"
echo "rmcampos/polpa-gestao-api:latest"
echo '```'
echo ""
echo "### Prisma Image"
echo '```'
echo "rmcampos/polpa-gestao-api:${{ steps.version.outputs.tag }}-prisma"
echo "rmcampos/polpa-gestao-api:latest-prisma"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Create and push Git tag
run: |
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
git push origin ${{ steps.version.outputs.tag }}
+59
View File
@@ -0,0 +1,59 @@
name: CI
on:
pull_request:
branches: [main]
jobs:
build-backend:
name: Build
runs-on: easynode-debian
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/backend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
working-directory: ./backend
run: npm i
- name: Build backend
working-directory: ./backend
run: npx tsc -p tsconfig.prod.json
build-frontend:
name: Build
runs-on: easynode-debian
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install dependencies
working-directory: ./frontend
run: npm i
- name: Run lint
working-directory: ./frontend
run: npm run lint
- name: Build frontend
working-directory: ./frontend
run: npm run build
+4 -50
View File
@@ -26,7 +26,7 @@ jobs:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: easynode-debian
outputs:
no_changes: ${{ steps.check-changes.outputs.no_changes }}
has_changes: ${{ steps.check-changes.outputs.has_changes }}
permissions:
contents: read
steps:
@@ -119,63 +119,17 @@ jobs:
'
terraform show -json tfplan > tfplan.json
if jq -e '.resource_changes | length == 0' tfplan.json >/dev/null; then
echo "no_changes=true" >> "$GITHUB_OUTPUT"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No changes to apply."
exit 0
else
echo "Changes detected. Proceeding with apply"
echo "no_changes=false" >> "$GITHUB_OUTPUT"
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Upload plan artifact
uses: actions/upload-artifact@v3
with:
name: tfplan
path: terraform/tfplan
terraform-apply:
runs-on: easynode-debian
needs: terraform-plan
if: >
(github.event_name == 'push' || github.event_name == 'workflow_run' || inputs.apply == 'true')
&& needs.terraform-plan.outputs.no_changes == 'false'
environment:
name: production
url: https://polpa-gestao.darkroasted.vps-kinghost.net
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Download plan artifact
uses: actions/download-artifact@v3
with:
name: tfplan
path: terraform
- name: Setup Doppler CLI
uses: dopplerhq/cli-action@v4
- name: Setup Kubeconfig
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: |
mkdir -p ~/.kube
doppler run --config prd_secrets -- bash -c 'echo "$KUBECONFIG_DATA" | base64 -d > ~/.kube/config'
chmod 600 ~/.kube/config
- name: Terraform Init
working-directory: terraform
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: doppler run --config prd_secrets -- terraform init -input=false
- name: Terraform Apply
working-directory: terraform
if: steps.check-changes.outputs.has_changes == 'true'
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
run: doppler run --config prd_secrets -- timeout 2m terraform apply tfplan
+32 -10
View File
@@ -35,12 +35,6 @@ jobs:
run: npm run lint
working-directory: ./frontend
- name: Run build
run: npm run build
working-directory: ./frontend
env:
VITE_BUILD_NUMBER: ${{ steps.version.outputs.build_number }}
- name: Generate version tag
id: version
run: |
@@ -50,8 +44,13 @@ jobs:
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "build_number=${BUILD_NUMBER}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
echo "Generated build number: ${BUILD_NUMBER}"
echo "Generated build number: ${BUILD_NUMBER}"
- name: Run build
run: npm run build
working-directory: ./frontend
env:
VITE_BUILD_NUMBER: ${{ steps.version.outputs.build_number }}
- name: Set up Docker Buildx
run: docker buildx inspect --bootstrap
@@ -82,11 +81,34 @@ jobs:
cache-to: type=registry,ref=rmcampos/polpa-gestao-app:buildcache,mode=max
build-args: |
VITE_BACKEND_SERVER=${{ vars.BACKEND_SERVER_URL }}
VITE_BUILD=${{ steps.version.outputs.tag }}
VITE_BUILD_NUMBER=${{ steps.version.outputs.build_number }}
- name: Output Docker image URLs
run: |
echo ""
echo "=========================================="
echo "🚀 Docker Images Published Successfully!"
echo "=========================================="
echo ""
echo "App Image:"
echo " rmcampos/polpa-gestao-app:${{ steps.version.outputs.tag }}"
echo " rmcampos/polpa-gestao-app:latest"
echo ""
echo "=========================================="
{
echo "## 🚀 Docker Images Published Successfully!"
echo ""
echo "### App Image"
echo '```'
echo "rmcampos/polpa-gestao-app:${{ steps.version.outputs.tag }}"
echo "rmcampos/polpa-gestao-app:latest"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Create and push Git tag
run: |
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
git push origin ${{ steps.version.outputs.tag }}
git push origin ${{ steps.version.outputs.tag }}
+2 -2
View File
@@ -1,10 +1,10 @@
# AGENTS.md
This document provides context and guidelines for AI agents interacting with the Polpa Gestão project.
This document provides context and guidelines for AI agents interacting with the Polpa Go project.
## Project Overview
Polpa Gestão is a full-stack business management web application designed for managing customers, products, sales, and delivery routes. It serves as a comprehensive platform for administrative tasks within a business.
Polpa Go is a full-stack business management web application designed for managing customers, products, sales, and delivery routes. It serves as a comprehensive platform for administrative tasks within a business.
## Architecture
+75 -5
View File
@@ -7,16 +7,86 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## api-v2026.06.16.7 && app-v2026.06.16.8 - 2026-06-16
## 27/07/2026
### Adicionado
- Nova categoria para POS by Industry: Casa de carnes
### Corrigido
- Bug de filtro na tela de Customers agora filtra corretamente.
- Na tela de dashboard na consulta POS By Industry agora considera apenas customers ativos.
- Notificaçao Toast agora pode ser clicada para fechar corretamente.
```bash
# Docker images
docker pull rmcampos/polpa-gestao-app:app-v2026.07.27.65
docker pull rmcampos/polpa-gestao-api:api-v2026.07.27.61
```
---
## 24/07/2026
### Adicionado
- Opção de clicar para ver ou expandir os clientes por indústria na tela de dashboard.
```bash
# Docker images
docker pull rmcampos/polpa-gestao-app:app-v2026.07.24.44
docker pull rmcampos/polpa-gestao-api:api-v2026.07.24.43
```
---
## 03/07/2026
### Adicionado
- Botão para iniciar uma venda a partir de um PDV na lista de rotas.
### Alterado
- Tela de gerenciar Paradas na página de Rotas, removido o dropdown e adicionado um campo de texto para pesquisa manual.
- Nome do app alterado de Polpa Gestão para Polpa Go.
- Versão das dependências no frontend e backend, atualizado para as últimas versões disponíveis.
### Corrigido
- Tamanho e viewport em dispositivos móveis empurrando components para fora da tela visíveil.
```bash
# Docker images
docker pull rmcampos/polpa-gestao-app:app-v2026.07.03.32
```
---
## 24/06/2026
### Alterado
- Dependências do projeto atualizadas para a última versão, incluindo React.
### Corrigido
- Número do build da versão não aparecendo corretamente.
```bash
# Docker images
docker pull rmcampos/polpa-gestao-api:api-v2026.06.24.22
docker pull rmcampos/polpa-gestao-app:app-v2026.06.24.23
```
---
## 2026-06-16
### Changed
- Container Registry to Docker Hub.
- Migrated from GitHub to Gitea.
### Docker images
- [rmcampos/polpa-gestao-app:app-v2026.06.16.8](https://hub.docker.com/layers/rmcampos/polpa-gestao-app/app-v2026.06.16.8/images/sha256-030671a44d04701da05abdf15dec4f1c834745a1c13bf404eca19050b92bcf3a)
- [rmcampos/polpa-gestao-api:api-v2026.06.16.7](https://hub.docker.com/layers/rmcampos/polpa-gestao-api/api-v2026.06.16.7/images/sha256-57e283fffcaac2a48d95f0ca11f73d8b1937ffc2346a9f4d93dc7b4cd7a3eaa6)
- [rmcampos/polpa-gestao-api:api-v2026.06.16.7-prisma](https://hub.docker.com/layers/rmcampos/polpa-gestao-api/api-v2026.06.16.7-prisma/images/sha256-74225e87715fea0803e78ffd1d7f1d9f2c14d058650caa5db2dd504ae7454e46)
```bash
# Docker images
docker pull rmcampos/polpa-gestao-app:app-v2026.06.16.8
docker pull rmcampos/polpa-gestao-api:api-v2026.06.16.7
```
---
## 2026-06-13 #1
+1 -1
View File
@@ -1,4 +1,4 @@
# Gemini Context: Polpa Gestão
# Gemini Context: Polpa Go
This project is a full-stack business management web application designed for managing customers, products, sales, and delivery routes.
+1 -1
View File
@@ -1,4 +1,4 @@
# Polpa Gestão
# Polpa Go
A full-stack business management web application for managing customers, points-of-sale (POS), products, sales, and delivery routes.
+3 -3
View File
@@ -1,4 +1,4 @@
FROM node:22-bookworm-slim AS deps
FROM node:24-bookworm-slim AS deps
WORKDIR /app
@@ -10,7 +10,7 @@ RUN npm ci && DATABASE_URL=postgres://dummy npx prisma generate
FROM deps AS prisma
FROM node:22-bookworm-slim AS builder
FROM node:24-bookworm-slim AS builder
WORKDIR /app
@@ -24,7 +24,7 @@ COPY src ./src
RUN npx tsc -p tsconfig.prod.json
FROM node:22-bookworm-slim AS runner
FROM node:24-bookworm-slim AS runner
WORKDIR /app
+118 -82
View File
@@ -11,23 +11,23 @@
"dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/rate-limit": "^11.0.0",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@fastify/jwt": "^10.1.0",
"@fastify/rate-limit": "^11.1.0",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcrypt": "^6.0.0",
"dotenv": "^16.4.7",
"fastify": "^5.8.5",
"fastify-plugin": "^5.1.0",
"pg": "^8.13.3"
"dotenv": "^17.4.2",
"fastify": "^5.9.0",
"fastify-plugin": "^6.0.0",
"pg": "^8.22.0"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/node": "^25.6.0",
"@types/pg": "^8.11.11",
"prisma": "^7.7.0",
"@types/node": "^26.1.0",
"@types/pg": "^8.20.0",
"prisma": "^7.8.0",
"ts-node": "^10.9.2",
"typescript": "^6.0.2"
"typescript": "^6.0.3"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -114,6 +114,22 @@
"fastify-plugin": "^5.0.0"
}
},
"node_modules/@fastify/cookie/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/cors": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz",
@@ -134,6 +150,22 @@
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/cors/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
@@ -186,9 +218,9 @@
"license": "MIT"
},
"node_modules/@fastify/jwt": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.0.0.tgz",
"integrity": "sha512-2Qka3NiyNNcsfejMUvyzot1T4UYIzzcbkFGDdVyrl344fRZ/WkD6VFXOoXhxe2Pzf3LpJNkoSxUM4Ru4DVgkYA==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-10.1.0.tgz",
"integrity": "sha512-U1y8ZbxoH1Pjon3euzPJmbCkuYBM+hrQlFWLQWvKmJGCNT6mVsAolnVJdEWfXeQOKpgmuRVCIsPll5RLZxj10A==",
"funding": [
{
"type": "github",
@@ -203,11 +235,27 @@
"dependencies": {
"@fastify/error": "^4.2.0",
"@lukeed/ms": "^2.0.2",
"fast-jwt": "^6.0.2",
"fast-jwt": "^6.2.0",
"fastify-plugin": "^5.0.1",
"steed": "^1.1.3"
}
},
"node_modules/@fastify/jwt/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/merge-json-schemas": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
@@ -248,9 +296,9 @@
}
},
"node_modules/@fastify/rate-limit": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.0.0.tgz",
"integrity": "sha512-kCs+G59SitZw9TL/ekFe+MrzXk20dEp6zPAM8WEZjFl5Ubvv5ksTbEXYr4jGlBwWAKn78q+NFsj5CN75zXLjaw==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.1.0.tgz",
"integrity": "sha512-BeJ9tizLvmTXGD7deYU5G04OtHhwk5uHxbpEPVp09gKvUBIXmau/4Bshxhu9ci54MvVWfGjCEx4RzvsTntojwA==",
"funding": [
{
"type": "github",
@@ -264,7 +312,7 @@
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^5.0.0",
"fastify-plugin": "^6.0.0",
"toad-cache": "^3.7.0"
}
},
@@ -734,12 +782,12 @@
}
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
"integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
"undici-types": "~8.3.0"
}
},
"node_modules/@types/pg": {
@@ -943,19 +991,6 @@
}
}
},
"node_modules/c12/node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
@@ -1089,9 +1124,9 @@
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -1210,15 +1245,16 @@
}
},
"node_modules/fast-jwt": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.1.0.tgz",
"integrity": "sha512-cGK/TXlud8INL49Iv7yRtZy0PHzNJId1shfqNCqdF0gOlWiy+1FPgjxX+ZHp/CYxFYDaoNnxeYEGzcXSkahUEQ==",
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-6.2.4.tgz",
"integrity": "sha512-IoQa53wI6TbARU2yelb0L44ggFQnP2qVcwswCSYHbCAWuwpr70icDb3QjG0v01I8Tt01rVGDkN/rRvpk0lKFTA==",
"license": "Apache-2.0",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"asn1.js": "^5.4.1",
"ecdsa-sig-formatter": "^1.0.11",
"mnemonist": "^0.40.0"
"mnemonist": "^0.40.0",
"safe-regex2": "^5.1.0"
},
"engines": {
"node": ">=20"
@@ -1262,9 +1298,9 @@
}
},
"node_modules/fastify": {
"version": "5.8.5",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz",
"integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==",
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.9.0.tgz",
"integrity": "sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==",
"funding": [
{
"type": "github",
@@ -1284,7 +1320,7 @@
"abstract-logging": "^2.0.1",
"avvio": "^9.0.0",
"fast-json-stringify": "^6.0.0",
"find-my-way": "^9.0.0",
"find-my-way": "^9.6.0",
"light-my-request": "^6.0.0",
"pino": "^9.14.0 || ^10.1.0",
"process-warning": "^5.0.0",
@@ -1295,9 +1331,9 @@
}
},
"node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
"integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
"funding": [
{
"type": "github",
@@ -1340,9 +1376,9 @@
}
},
"node_modules/find-my-way": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz",
"integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==",
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz",
"integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -1590,9 +1626,9 @@
"license": "ISC"
},
"node_modules/mnemonist": {
"version": "0.40.3",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz",
"integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==",
"version": "0.40.4",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.4.tgz",
"integrity": "sha512-ZAv+KNavneRVzu4tUeOgzkScI3W5BGwZ3rkxIpKtzzVgfTtWQFN1CgX0U72cyvyh3iTuHL3SiSmrQxTlryEIcw==",
"license": "MIT",
"dependencies": {
"obliterator": "^2.0.4"
@@ -1699,14 +1735,14 @@
"license": "MIT"
},
"node_modules/pg": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
"integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
"pg-protocol": "^1.13.0",
"pg-connection-string": "^2.14.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.15.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
@@ -1714,7 +1750,7 @@
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.3.0"
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
@@ -1726,16 +1762,16 @@
}
},
"node_modules/pg-cloudflare": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz",
"integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==",
"license": "MIT"
},
"node_modules/pg-int8": {
@@ -1748,18 +1784,18 @@
}
},
"node_modules/pg-pool": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz",
"integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==",
"license": "MIT"
},
"node_modules/pg-types": {
@@ -2358,9 +2394,9 @@
}
},
"node_modules/typescript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -2372,9 +2408,9 @@
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
+12 -12
View File
@@ -13,22 +13,22 @@
"dependencies": {
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0",
"@fastify/rate-limit": "^11.0.0",
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@fastify/jwt": "^10.1.0",
"@fastify/rate-limit": "^11.1.0",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcrypt": "^6.0.0",
"dotenv": "^16.4.7",
"fastify": "^5.8.5",
"fastify-plugin": "^5.1.0",
"pg": "^8.13.3"
"dotenv": "^17.4.2",
"fastify": "^5.9.0",
"fastify-plugin": "^6.0.0",
"pg": "^8.22.0"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/node": "^25.6.0",
"@types/pg": "^8.11.11",
"prisma": "^7.7.0",
"@types/node": "^26.1.0",
"@types/pg": "^8.20.0",
"prisma": "^7.8.0",
"ts-node": "^10.9.2",
"typescript": "^6.0.2"
"typescript": "^6.0.3"
}
}
+1
View File
@@ -10,6 +10,7 @@ const POS_INDUSTRIES = new Set([
'Academia',
'Arena de esporte',
'Assadão',
'Casa de carnes',
'Conveniência',
'Hamburgueria',
'Lanchonete',
+40
View File
@@ -254,6 +254,46 @@ export default async function dashboardRoutes(app: FastifyInstance) {
.sort((a, b) => b.count - a.count);
});
app.get('/pos-by-industry', async (request, reply) => {
const { industry } = request.query as { industry?: string };
if (!industry || industry.trim() === '') {
return reply.status(400).send({ error: 'Industry query parameter is required' });
}
const fallbackIndustry = 'Não Informado';
const normalizedIndustry = industry.trim();
const isFallback = normalizedIndustry === fallbackIndustry;
const poses = await prisma.customerPos.findMany({
where: {
disabledAt: null,
customer: { disabledAt: null },
...(isFallback
? { OR: [{ industry: { equals: null } }, { industry: { equals: '' } }] }
: { industry: normalizedIndustry })
},
include: {
customer: {
select: { name: true }
}
},
orderBy: [
{ customer: { name: 'asc' } },
{ address: 'asc' }
]
});
return poses.map((pos) => ({
id: pos.id,
customerName: pos.customer.name,
address: pos.address,
personName: pos.personName,
phone: pos.phone,
industry: pos.industry?.trim() || fallbackIndustry
}));
});
app.get('/inactive-pos', async () => {
const now = new Date();
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
#
# dockerhub-cleanup.sh
#
# Keeps only the N most recently updated tags in a Docker Hub repository
# and deletes the rest, using the Docker Hub v2 API.
#
# Usage:
# ./dockerhub-cleanup.sh -u <dockerhub_username> -r <namespace/repo> [-k 5] [-e "latest,stable"] [--yes]
#
# Examples:
# Dry run (default, nothing is deleted):
# ./dockerhub-cleanup.sh -u johndoe -r johndoe/myapp -k 5
#
# Actually delete:
# ./dockerhub-cleanup.sh -u johndoe -r johndoe/myapp -k 5 --yes
#
# Keep 5 newest, never delete "latest" or "stable" even if old:
# ./dockerhub-cleanup.sh -u johndoe -r johndoe/myapp -k 5 -e "latest,stable" --yes
#
# You will be prompted for your Docker Hub password (or a Personal Access
# Token, recommended) — it is never passed as a CLI argument or logged.
set -euo pipefail
KEEP=5
EXCLUDE=""
EXECUTE=false
USERNAME=""
REPO=""
usage() {
echo "Usage: $0 -u <username> -r <namespace/repo> [-k keep_count] [-e tag1,tag2] [--yes]"
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
-u) USERNAME="$2"; shift 2 ;;
-r) REPO="$2"; shift 2 ;;
-k) KEEP="$2"; shift 2 ;;
-e) EXCLUDE="$2"; shift 2 ;;
--yes) EXECUTE=true; shift ;;
-h|--help) usage ;;
*) echo "Unknown argument: $1"; usage ;;
esac
done
[[ -z "$USERNAME" || -z "$REPO" ]] && usage
if ! command -v jq >/dev/null 2>&1; then
echo "This script requires 'jq'. Install it first (e.g. apt install jq / brew install jq)." >&2
exit 1
fi
read -rsp "Docker Hub password or access token for $USERNAME: " PASSWORD
echo
echo "Authenticating..."
TOKEN=$(curl -s -H "Content-Type: application/json" \
-X POST -d "{\"username\": \"$USERNAME\", \"password\": \"$PASSWORD\"}" \
https://hub.docker.com/v2/users/login/ | jq -r .token)
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
echo "Authentication failed. Check your username/password (or use a Personal Access Token)." >&2
exit 1
fi
echo "Fetching tags for $REPO..."
ALL_TAGS_JSON="[]"
URL="https://hub.docker.com/v2/repositories/${REPO}/tags/?page_size=100&ordering=last_updated"
while [[ -n "$URL" && "$URL" != "null" ]]; do
PAGE=$(curl -s -H "Authorization: JWT ${TOKEN}" "$URL")
ALL_TAGS_JSON=$(jq -s '.[0] + .[1].results' <(echo "$ALL_TAGS_JSON") <(echo "$PAGE"))
URL=$(echo "$PAGE" | jq -r '.next')
done
# ordering=last_updated from the API is ascending oldest->newest in some
# accounts, so re-sort locally to be safe: newest first.
SORTED=$(echo "$ALL_TAGS_JSON" | jq 'sort_by(.last_updated) | reverse')
TOTAL=$(echo "$SORTED" | jq 'length')
echo "Found $TOTAL tags. Keeping the $KEEP most recent."
[[ -n "$EXCLUDE" ]] && echo "Always keeping (excluded from deletion): $EXCLUDE"
IFS=',' read -ra EXCLUDE_ARR <<< "$EXCLUDE"
KEPT=0
DELETED=0
echo "$SORTED" | jq -c '.[]' | while read -r ENTRY; do
TAG=$(echo "$ENTRY" | jq -r '.name')
UPDATED=$(echo "$ENTRY" | jq -r '.last_updated')
IS_EXCLUDED=false
for EX in "${EXCLUDE_ARR[@]:-}"; do
[[ -n "$EX" && "$TAG" == "$EX" ]] && IS_EXCLUDED=true
done
if $IS_EXCLUDED; then
echo "KEEP (excluded) $TAG (last updated: $UPDATED)"
continue
fi
# index within the (non-excluded) loop isn't tracked across subshell
# iterations reliably in a pipe, so we recompute rank using a counter file.
COUNTER_FILE="/tmp/.dockerhub_cleanup_counter"
if [[ ! -f "$COUNTER_FILE" ]]; then echo 0 > "$COUNTER_FILE"; fi
RANK=$(cat "$COUNTER_FILE")
if [[ "$RANK" -lt "$KEEP" ]]; then
echo "KEEP $TAG (last updated: $UPDATED)"
echo $((RANK + 1)) > "$COUNTER_FILE"
else
if $EXECUTE; then
echo "DELETE $TAG (last updated: $UPDATED)"
curl -s -o /dev/null -w " -> HTTP %{http_code}\n" \
-X DELETE -H "Authorization: JWT ${TOKEN}" \
"https://hub.docker.com/v2/repositories/${REPO}/tags/${TAG}/"
else
echo "WOULD DELETE (dry run) $TAG (last updated: $UPDATED)"
fi
echo $((RANK + 1)) > "$COUNTER_FILE"
fi
done
rm -f /tmp/.dockerhub_cleanup_counter
if ! $EXECUTE; then
echo
echo "Dry run complete. No tags were deleted. Re-run with --yes to actually delete."
fi
+1 -1
View File
@@ -1,5 +1,5 @@
# Build stage
FROM node:20-alpine AS build
FROM node:24-alpine AS build
WORKDIR /app
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Polpa Gestão - Gestão de Polpas</title>
<title>Polpa Go - Gestão de Polpas</title>
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
@@ -21,11 +21,11 @@
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
<meta name="description" content="Polpa Gestão - Gestão de Polpas" />
<meta name="description" content="Polpa Go - Gestão de Polpas" />
<!-- PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Polpa Gestão">
<meta name="apple-mobile-web-app-title" content="Polpa Go">
<meta name="mobile-web-app-capable" content="yes">
</head>
<body>
+522 -229
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -15,23 +15,24 @@
"bootstrap-icons": "^1.13.1",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0"
"react-router-dom": "^7.18.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/bootstrap": "^5.2.11",
"@types/node": "^26.0.0",
"@types/node": "^26.1.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"eslint": "^10.5.0",
"eslint": "^10.6.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.7.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.62.0",
"vite": "^8.1.0",
"vite-plugin-pwa": "^1.3.0"
"typescript-eslint": "^8.62.1",
"vite": "^8.1.3",
"vite-plugin-pwa": "^1.3.0",
"vitest": "^4.1.10"
},
"overrides": {
"vite-plugin-pwa": {
+1 -1
View File
@@ -65,7 +65,7 @@ export default function App() {
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<p className="m-0 text-white-50">Loading Polpa Gestão...</p>
<p className="m-0 text-white-50">Loading Polpa Go...</p>
</div>
</div>
);
+2 -2
View File
@@ -19,7 +19,7 @@ export default function Sidebar({ onLogout }: SidebarProps) {
<>
{/* Mobile top bar */}
<div className="mobile-topbar d-flex d-md-none">
<h3 className="fw-bold m-0" style={{ color: 'var(--primary-color)' }}>Polpa Gestão</h3>
<h3 className="fw-bold m-0" style={{ color: 'var(--primary-color)' }}>Polpa Go</h3>
<button
className="btn btn-link p-0"
style={{ color: 'var(--text-primary)', fontSize: '1.5rem', lineHeight: 1 }}
@@ -36,7 +36,7 @@ export default function Sidebar({ onLogout }: SidebarProps) {
<div className={`sidebar d-flex flex-column${isOpen ? ' open' : ''}`}>
<div className="mb-3 px-2 d-flex align-items-center justify-content-between">
<div>
<h3 className="fw-bold m-0" style={{ color: 'var(--primary-color)' }}>Polpa Gestão</h3>
<h3 className="fw-bold m-0" style={{ color: 'var(--primary-color)' }}>Polpa Go</h3>
<small className="text-secondary">Welcome, {userName}</small>
</div>
<button className="btn btn-link p-0 d-md-none" style={{ color: 'var(--text-secondary)', fontSize: '1.25rem' }} onClick={closeMenu} aria-label="Close menu">
+2
View File
@@ -7,6 +7,7 @@
flex-direction: column;
gap: 12px;
max-width: 400px;
pointer-events: none;
}
.toast-item {
@@ -20,6 +21,7 @@
animation: slideInRight 0.3s ease-out;
border: 1px solid;
min-width: 320px;
pointer-events: auto;
}
@keyframes slideInRight {
+6 -1
View File
@@ -14,6 +14,10 @@
--warning-color: #f59e0b;
}
html, body {
overflow-x: hidden;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
@@ -76,6 +80,7 @@ body {
.main-content {
flex-grow: 1;
min-width: 0;
padding: 2rem;
margin-left: 250px; /* Sidebar width */
}
@@ -158,7 +163,7 @@ body {
top: 0;
border-right: 1px solid var(--glass-border);
border-bottom: none;
padding: 1.5rem 1rem;
padding: calc(60px + 1.5rem) 1rem 1.5rem;
}
.sidebar.open {
transform: translateX(0);
+1 -1
View File
@@ -128,7 +128,7 @@ export default function ClosestPos() {
<div className="text-secondary mt-4">No points of sale with coordinates were found.</div>
)}
<footer className="text-center text-secondary small mt-auto pt-4">© 2026 Polpa Gestão</footer>
<footer className="text-center text-secondary small mt-auto pt-4">© 2026 Polpa Go</footer>
</div>
);
}
+81
View File
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import type { Customer, CustomerPOS } from '../types';
function filterCustomers(customers: Customer[], filterText: string): Customer[] {
const normalizedFilter = filterText.trim().toLowerCase();
if (!normalizedFilter) return customers;
const digits = normalizedFilter.replace(/\D/g, '');
const isPhoneSearch = /^\d+$/.test(normalizedFilter) && digits.length >= 3;
const addressFilterMatches = (pos: CustomerPOS[] | undefined, text: string): boolean => {
if (pos && Array.isArray(pos)) {
const normalizedText = text.toLowerCase();
for (let i = 0; i < pos.length; i++) {
const normalizedAddress = pos[i].address
.replace(/á/g, 'a')
.replace(/ã/g, 'a')
.replace(/â/g, 'a')
.replace(/ê/g, 'e')
.replace(/é/g, 'e')
.replace(/ó/g, 'o')
.replace(/ú/g, 'u')
.replace(/ç/g, 'c')
.toLowerCase();
if (normalizedAddress.includes(normalizedText)) {
return true;
}
}
}
return false;
};
return customers.filter((c: Customer) =>
c.name.toLowerCase().includes(normalizedFilter) ||
(c.personName && c.personName.toLowerCase().includes(normalizedFilter)) ||
(isPhoneSearch && c.phone && c.phone.replace(/\D/g, '').includes(digits)) ||
addressFilterMatches(c.pos, normalizedFilter)
);
}
describe('Customer name filter', () => {
const customers: Customer[] = [
{
name: 'Bakery 3 st',
phone: '1234567890',
pos: [],
},
{
name: 'Bakery 4 st',
phone: '9876543210',
pos: [],
},
{
name: 'Another Customer',
phone: '1112223333',
pos: [],
},
];
it('finds "Bakery 3 st" when typing a single word', () => {
expect(filterCustomers(customers, 'Bakery')).toHaveLength(2);
});
it('finds "Bakery 3 st" when typing a space followed by a number', () => {
expect(filterCustomers(customers, ' 3')).toHaveLength(1);
expect(filterCustomers(customers, ' 3')[0].name).toBe('Bakery 3 st');
});
it('finds "Bakery 3 st" when typing the full name with spaces', () => {
expect(filterCustomers(customers, 'Bakery 3 st')).toHaveLength(1);
expect(filterCustomers(customers, 'Bakery 3 st')[0].name).toBe('Bakery 3 st');
});
it('finds "Bakery 3 st" when typing trailing spaces', () => {
expect(filterCustomers(customers, ' Bakery 3 st ')).toHaveLength(1);
expect(filterCustomers(customers, ' Bakery 3 st ')[0].name).toBe('Bakery 3 st');
});
it('returns all customers when filter is only spaces', () => {
expect(filterCustomers(customers, ' ')).toHaveLength(3);
});
});
+13 -8
View File
@@ -8,6 +8,7 @@ const POS_INDUSTRY_OPTIONS = [
'Academia',
'Arena de esporte',
'Assadão',
'Casa de carnes',
'Conveniência',
'Hamburgueria',
'Lanchonete',
@@ -68,6 +69,7 @@ export default function Customers() {
const addressFilterMatches = (pos: CustomerPOS[] | undefined, text: string): boolean => {
if (pos && Array.isArray(pos)) {
const normalizedText = text.toLowerCase();
for (let i = 0; i < pos.length; i++) {
const normalizedAddress = pos[i].address
.replace(/á/g, 'a')
@@ -79,21 +81,24 @@ export default function Customers() {
.replace(/ú/g, 'u')
.replace(/ç/g, 'c')
.toLowerCase();
return normalizedAddress.includes(text.toLowerCase());
if (normalizedAddress.includes(normalizedText)) {
return true;
}
}
}
return false;
};
const filteredCustomers = useMemo(() => {
if (!filterText.trim()) return customers;
const lower = filterText.toLowerCase();
const digits = filterText.replace(/\D/g, '');
const normalizedFilter = filterText.trim().toLowerCase();
if (!normalizedFilter) return customers;
const digits = normalizedFilter.replace(/\D/g, '');
const isPhoneSearch = /^\d+$/.test(normalizedFilter) && digits.length >= 3;
return customers.filter((c: Customer) =>
c.name.toLowerCase().includes(lower) ||
(c.personName && c.personName.toLowerCase().includes(lower)) ||
(digits && c.phone && c.phone.replace(/\D/g, '').includes(digits)) ||
addressFilterMatches(c.pos, filterText)
c.name.toLowerCase().includes(normalizedFilter) ||
(c.personName && c.personName.toLowerCase().includes(normalizedFilter)) ||
(isPhoneSearch && c.phone && c.phone.replace(/\D/g, '').includes(digits)) ||
addressFilterMatches(c.pos, normalizedFilter)
);
}, [customers, filterText]);
+102 -5
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import axios from 'axios';
import type { FridgePosSummary, InactivePosSummary, IndustriesSummary, RegionsSummary, SalesByCustomer, SalesByProduct, SalesSummary } from '../types';
import type { FridgePosSummary, IndustryPosDetail, InactivePosSummary, IndustriesSummary, RegionsSummary, SalesByCustomer, SalesByProduct, SalesSummary } from '../types';
export default function Dashboard() {
const [range, setRange] = useState('last-30-days');
@@ -13,6 +13,10 @@ export default function Dashboard() {
const [fridgePoses, setFridgePoses] = useState<FridgePosSummary[]>([]);
const [showFridgesModal, setShowFridgesModal] = useState(false);
const [loadingFridgePoses, setLoadingFridgePoses] = useState(false);
const [showIndustryModal, setShowIndustryModal] = useState(false);
const [selectedIndustry, setSelectedIndustry] = useState<string | null>(null);
const [industryPoses, setIndustryPoses] = useState<IndustryPosDetail[]>([]);
const [loadingIndustryPoses, setLoadingIndustryPoses] = useState(false);
const [inactivePoses, setInactivePoses] = useState<InactivePosSummary[]>([]);
const apiBase = import.meta.env.VITE_BACKEND_SERVER || '/api';
@@ -77,12 +81,35 @@ export default function Dashboard() {
}
};
const openIndustryModal = async (industry: string) => {
setSelectedIndustry(industry);
setShowIndustryModal(true);
setLoadingIndustryPoses(true);
try {
const response = await axios.get(`${apiBase}/api/dashboard/pos-by-industry`, {
params: { industry }
});
setIndustryPoses(response.data);
} catch (err) {
console.error('Failed to load industry POS details', err);
setIndustryPoses([]);
} finally {
setLoadingIndustryPoses(false);
}
};
const closeIndustryModal = () => {
setShowIndustryModal(false);
setSelectedIndustry(null);
setIndustryPoses([]);
};
return (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-4">
<h2 className="m-0 fw-bold">Dashboard</h2>
<div style={{ width: '200px' }}>
<select
<div style={{ width: '100%', maxWidth: '200px' }}>
<select
className="form-select bg-dark text-white border-secondary"
aria-label="Date range"
value={range}
@@ -200,7 +227,20 @@ export default function Dashboard() {
) : (
<ul className="list-group list-group-flush" style={{ background: 'transparent' }}>
{industriesSummary.map((item) => (
<li key={item.industry} className="list-group-item d-flex justify-content-between align-items-center text-white px-0" style={{ background: 'transparent', borderBottomColor: 'var(--glass-border)' }}>
<li
key={item.industry}
className="list-group-item d-flex justify-content-between align-items-center text-white px-0"
style={{ background: 'transparent', borderBottomColor: 'var(--glass-border)', cursor: 'pointer' }}
role="button"
tabIndex={0}
onClick={() => openIndustryModal(item.industry)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openIndustryModal(item.industry);
}
}}
>
<span>{item.industry}</span>
<span className="badge bg-secondary rounded-pill">{item.count}</span>
</li>
@@ -363,6 +403,63 @@ export default function Dashboard() {
</>,
document.body
)}
{showIndustryModal && selectedIndustry && createPortal(
<>
<div className="modal-backdrop fade show" style={{ zIndex: 1040 }}></div>
<div
className="modal fade show d-block"
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-labelledby="industry-modal-title"
style={{ zIndex: 1050 }}
>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content glass-card">
<div className="modal-header border-bottom-0" style={{ borderColor: 'var(--glass-border)' }}>
<h5 id="industry-modal-title" className="modal-title text-white">{selectedIndustry}</h5>
<button type="button" className="btn-close btn-close-white" onClick={closeIndustryModal}></button>
</div>
<div className="modal-body">
{loadingIndustryPoses ? (
<p className="text-secondary mb-0">Loading...</p>
) : industryPoses.length === 0 ? (
<p className="text-secondary mb-0">No POS found for this industry.</p>
) : (
<div className="table-responsive">
<table className="table table-dark table-hover align-middle mb-0">
<thead>
<tr>
<th>Customer</th>
<th>Address</th>
<th>Contact Person</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
{industryPoses.map((pos) => (
<tr key={pos.id}>
<td>{pos.customerName}</td>
<td>{pos.address}</td>
<td>{pos.personName || '—'}</td>
<td>{pos.phone || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div className="modal-footer border-top-0" style={{ borderColor: 'var(--glass-border)' }}>
<button type="button" className="btn btn-outline-light" onClick={closeIndustryModal}>Close</button>
</div>
</div>
</div>
</div>
</>,
document.body
)}
</div>
);
}
+1 -1
View File
@@ -46,7 +46,7 @@ export default function Login({ setToken }: LoginProps) {
<div className="d-flex align-items-center justify-content-center" style={{ minHeight: '100vh', width: '100vw', padding: '1rem' }}>
<div className="glass-card p-5 animate-fade-in" style={{ width: '100%', maxWidth: '450px' }}>
<div className="text-center mb-4">
<h2 className="fw-bold" style={{ color: 'var(--primary-color)' }}>Polpa Gestão</h2>
<h2 className="fw-bold" style={{ color: 'var(--primary-color)' }}>Polpa Go</h2>
<p className="text-secondary">Sign in to your account</p>
</div>
+24 -14
View File
@@ -1,8 +1,10 @@
import { useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import { useToast } from '../context/toast';
import type { Customer, CustomerPOS, Route } from '../types';
import { CustomerPosCombobox } from '../components/CustomerPosCombobox';
type RouteStopApiItem = {
customerPosId?: string;
@@ -49,12 +51,14 @@ const normalizeRoute = (route: RouteApiResponse | Route): Route => {
export default function RoutesPage() {
const toast = useToast();
const navigate = useNavigate();
const [routesData, setRoutesData] = useState<Route[]>([]);
const [customers, setCustomers] = useState<Customer[]>([]);
const [showModal, setShowModal] = useState(false);
const [showStopsModal, setShowStopsModal] = useState(false);
const [selectedRoute, setSelectedRoute] = useState<Route | null>(null);
const [selectedPosIdToAdd, setSelectedPosIdToAdd] = useState('');
const [stopFilterText, setStopFilterText] = useState('');
const [newRoute, setNewRoute] = useState<Route>({ name: '', completed: false, dayOfWeek: 0, customerPos: [] });
const [editingRoute, setEditingRoute] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -153,6 +157,7 @@ export default function RoutesPage() {
const openStopsModal = (route: Route) => {
setSelectedRoute(normalizeRoute(route));
setSelectedPosIdToAdd('');
setStopFilterText('');
fetchCustomers();
setShowStopsModal(true);
};
@@ -175,6 +180,7 @@ export default function RoutesPage() {
await axios.put(`${apiBase}/api/routes/${selectedRoute.id}`, { customerPosIds: newPosIds });
setSelectedPosIdToAdd('');
setStopFilterText('');
fetchRoutes();
const res = await axios.get<RouteApiResponse>(`${apiBase}/api/routes/${selectedRoute.id}`);
setSelectedRoute(normalizeRoute(res.data));
@@ -290,6 +296,9 @@ export default function RoutesPage() {
<a href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(cp.address)}`} target="_blank" rel="noopener noreferrer" className="text-secondary small">View on Google Maps</a>
)}
</div>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-outline-success" onClick={() => navigate(`/sales?posId=${cp.id}`)}>New Sale</button>
</div>
</li>
))}
{(!route.customerPos || route.customerPos.length === 0) && (
@@ -396,20 +405,21 @@ export default function RoutesPage() {
<form onSubmit={handleAddStop}>
<div className="row g-3">
<div className="col-md-9">
<select className="form-select form-select-sm" value={selectedPosIdToAdd} onChange={e => setSelectedPosIdToAdd(e.target.value)} required>
<option value="" disabled>Select a Customer POS...</option>
{customers.map((c: Customer) => (
c.pos && c.pos.length > 0 && (
<optgroup key={c.id} label={c.name}>
{c.pos
.filter((p: CustomerPOS) => Boolean(p.id))
.map((p: CustomerPOS) => (
<option key={p.id} value={p.id}>{p.address} {p.phone ? `(${p.phone})` : ''}</option>
))}
</optgroup>
)
))}
</select>
<CustomerPosCombobox
customers={customers}
selectedPosId={selectedPosIdToAdd}
filterText={stopFilterText}
onFilterTextChange={(value) => {
setStopFilterText(value);
if (selectedPosIdToAdd) {
setSelectedPosIdToAdd('');
}
}}
onSelectPos={(posId, displayText) => {
setSelectedPosIdToAdd(posId);
setStopFilterText(displayText);
}}
/>
</div>
<div className="col-md-3">
<button type="submit" className="btn btn-sm btn-success w-100" disabled={loading || !selectedPosIdToAdd}>{loading ? '...' : 'Add Stop'}</button>
+31
View File
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { createPortal } from 'react-dom';
import axios from 'axios';
import { useToast } from '../context/toast';
@@ -73,6 +74,7 @@ const buildSaleCopyText = (sale: Sale) => {
export default function Sales() {
const toast = useToast();
const [searchParams] = useSearchParams();
const [sales, setSales] = useState<Sale[]>([]);
const [customers, setCustomers] = useState<Customer[]>([]);
const [products, setProducts] = useState<Product[]>([]);
@@ -128,6 +130,35 @@ export default function Sales() {
};
}, [fetchData]);
useEffect(function posIdFromUrlEffect() {
const posId = searchParams.get('posId');
if (!posId || customers.length === 0) return;
const customerWithPos = customers.find((c) =>
c.pos?.some((p) => p.id === posId)
);
if (!customerWithPos) return;
const pos = customerWithPos.pos?.find((p) => p.id === posId);
if (!pos) return;
// Syncing local modal state from a URL param (posId) is a legitimate use of an
// effect here; there's no way to compute this during render since it depends on
// async-loaded `customers`. See tech-debt issue for a proper render-time rewrite.
// eslint-disable-next-line react-hooks/set-state-in-effect
setEditingMode(false);
setEditingSaleId(null);
setCustomerFilter(formatCustomerPosDisplay(customerWithPos.name, pos.address));
setCustomerPosId(posId);
setPaymentMethod('Cash');
setPaymentDueDate('');
setPaymentDate('');
setComments('');
setNextVisitDate('');
setCart([]);
setShowModal(true);
}, [customers, searchParams]);
const filteredSales = useMemo(() => {
if (!filterText.trim()) return sales;
const lower = filterText.toLowerCase();
+9
View File
@@ -26,6 +26,15 @@ export type IndustriesSummary = {
count: number;
}
export type IndustryPosDetail = {
id: string;
customerName: string;
address: string;
personName: string | null;
phone: string;
industry: string;
}
export type RegionsSummary = {
region: string;
count: number;
+2 -2
View File
@@ -10,8 +10,8 @@ export default defineConfig({
registerType: 'autoUpdate',
includeAssets: ['assets/*.png', 'assets/*.ico', 'assets/*.xml'],
manifest: {
name: 'Polpa Gestão',
short_name: 'Polpa Gestão',
name: 'Polpa Go',
short_name: 'Polpa Go',
description: 'Manage point of sales, products, routes and sales.',
theme_color: '#0f172a',
background_color: '#0f172a',
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}