diff --git a/backend/agent.md b/backend/agent.md
index afd0bbc..444855a 100644
--- a/backend/agent.md
+++ b/backend/agent.md
@@ -8,22 +8,22 @@ Backend da aplicação MeowSpool escrito em **Rust**, utilizando **Axum** como f
## Stack
-| Componente | Biblioteca | Versão mínima |
-|-------------------|-----------------------------------------------|---------------|
-| HTTP Framework | `axum` | 0.7 |
-| Async Runtime | `tokio` (full) | 1.x |
-| ORM / Query | `sqlx` (postgres, uuid, time, macros) | 0.8 |
-| Autenticação JWT | `jsonwebtoken` | 9.x |
-| Hash de senha | `argon2` | 0.5 |
-| OAuth Google | `oauth2` | 4.x |
-| UUID | `uuid` (v4, serde) | 1.x |
-| Serialização | `serde`, `serde_json` | 1.x |
-| Erros | `thiserror` | 1.x |
-| Env vars | `dotenvy` | 0.15 |
-| Logging | `tracing`, `tracing-subscriber` | 0.1 |
-| Validação | `validator` | 0.18 |
-| HTTP Client | `reqwest` (json, rustls-tls) | 0.12 |
-| Geração QR Code | `qrcode` | 0.14 |
+| Componente | Biblioteca | Versão mínima |
+| ---------------- | ------------------------------------- | ------------- |
+| HTTP Framework | `axum` | 0.7 |
+| Async Runtime | `tokio` (full) | 1.x |
+| ORM / Query | `sqlx` (postgres, uuid, time, macros) | 0.8 |
+| Autenticação JWT | `jsonwebtoken` | 9.x |
+| Hash de senha | `argon2` | 0.5 |
+| OAuth Google | `oauth2` | 4.x |
+| UUID | `uuid` (v4, serde) | 1.x |
+| Serialização | `serde`, `serde_json` | 1.x |
+| Erros | `thiserror` | 1.x |
+| Env vars | `dotenvy` | 0.15 |
+| Logging | `tracing`, `tracing-subscriber` | 0.1 |
+| Validação | `validator` | 0.18 |
+| HTTP Client | `reqwest` (json, rustls-tls) | 0.12 |
+| Geração QR Code | `qrcode` | 0.14 |
---
@@ -109,37 +109,38 @@ Todas as rotas são prefixadas com `/api/v1`.
### Auth — `/api/v1/auth`
-| Método | Rota | Handler | Acesso |
-|--------|-----------------------|---------------------------|--------------------------------|
-| POST | `/register` | `register_handler` | Público |
-| POST | `/login` | `login_handler` | Público |
-| POST | `/oauth/google` | `google_oauth_handler` | Público |
-| POST | `/refresh` | `refresh_token_handler` | Público (requer refresh token) |
-| POST | `/logout` | `logout_handler` | Autenticado |
-| POST | `/forgot-password` | `forgot_password_handler` | Público |
-| POST | `/verify-email` | `verify_email_handler` | Público |
-| POST | `/reset-password` | `reset_password_handler` | Público (requer token de reset)|
+| Método | Rota | Handler | Acesso |
+| ------ | ------------------ | ------------------------- | ------------------------------- |
+| POST | `/register` | `register_handler` | Público |
+| POST | `/login` | `login_handler` | Público |
+| POST | `/oauth/google` | `google_oauth_handler` | Público |
+| POST | `/refresh` | `refresh_token_handler` | Público (requer refresh token) |
+| POST | `/logout` | `logout_handler` | Autenticado |
+| POST | `/forgot-password` | `forgot_password_handler` | Público |
+| POST | `/verify-email` | `verify_email_handler` | Público |
+| POST | `/reset-password` | `reset_password_handler` | Público (requer token de reset) |
### Users — `/api/v1/users`
| Método | Rota | Handler | Acesso |
-|--------|-------|---------------------|-------------|
+| ------ | ----- | ------------------- | ----------- |
| GET | `/me` | `get_me_handler` | Autenticado |
| PUT | `/me` | `update_me_handler` | Autenticado |
### Filaments — `/api/v1/filaments`
-| Método | Rota | Handler | Acesso |
-|--------|-------------------|----------------------------|-------------|
-| GET | `/` | `list_filaments_handler` | Autenticado |
-| POST | `/` | `create_filament_handler` | Autenticado |
-| GET | `/:id` | `get_filament_handler` | Autenticado |
-| PUT | `/:id` | `update_filament_handler` | Autenticado |
-| DELETE | `/:id` | `delete_filament_handler` | Autenticado |
-| GET | `/:id/qrcode` | `get_qrcode_handler` | Autenticado |
-| GET | `/:id/label.svg` | `export_label_handler` | Autenticado |
+| Método | Rota | Handler | Acesso |
+| ------ | ---------------- | ------------------------- | ----------- |
+| GET | `/` | `list_filaments_handler` | Autenticado |
+| POST | `/` | `create_filament_handler` | Autenticado |
+| GET | `/:id` | `get_filament_handler` | Autenticado |
+| PUT | `/:id` | `update_filament_handler` | Autenticado |
+| DELETE | `/:id` | `delete_filament_handler` | Autenticado |
+| GET | `/:id/qrcode` | `get_qrcode_handler` | Autenticado |
+| GET | `/:id/label.svg` | `export_label_handler` | Autenticado |
**Query params de listagem (`GET /filaments`):**
+
- `material` — filtra por tipo (PLA, ABS, PETG, TPU, ASA, PA, PC...)
- `brand` — filtra por marca
- `search` — busca em marca, modelo e notas
@@ -149,20 +150,21 @@ Todas as rotas são prefixadas com `/api/v1`.
### Spool Presets — `/api/v1/spool-presets`
-| Método | Rota | Handler | Acesso |
-|--------|----------|--------------------------|-------------------------------------|
-| GET | `/` | `list_presets_handler` | Autenticado |
-| POST | `/` | `create_preset_handler` | Autenticado |
-| PUT | `/:id` | `update_preset_handler` | Autenticado (apenas presets do user)|
-| DELETE | `/:id` | `delete_preset_handler` | Autenticado (apenas presets do user)|
+| Método | Rota | Handler | Acesso |
+| ------ | ------ | ----------------------- | ------------------------------------ |
+| GET | `/` | `list_presets_handler` | Autenticado |
+| POST | `/` | `create_preset_handler` | Autenticado |
+| PUT | `/:id` | `update_preset_handler` | Autenticado (apenas presets do user) |
+| DELETE | `/:id` | `delete_preset_handler` | Autenticado (apenas presets do user) |
### Dashboard — `/api/v1/dashboard`
-| Método | Rota | Handler | Acesso |
-|--------|------|----------------------|-------------|
-| GET | `/` | `dashboard_handler` | Autenticado |
+| Método | Rota | Handler | Acesso |
+| ------ | ---- | ------------------- | ----------- |
+| GET | `/` | `dashboard_handler` | Autenticado |
**Resposta do dashboard:**
+
```json
{
"total_stock_kg": 14.2,
@@ -361,11 +363,13 @@ O backend implementa **last-write-wins com timestamp**:
## Geração de QR Code e Etiqueta SVG
### QR Code (`GET /filaments/:id/qrcode`)
+
- Gera QR Code com deep link: `meowspool://filament/:id` (singular)
- Retorna PNG (`image/png`) por padrão, ou SVG com `?format=svg`
- Biblioteca: crate `qrcode`
### Etiqueta SVG (`GET /filaments/:id/label.svg`)
+
- Query params: `width_mm` (padrão: 50), `height_mm` (padrão: 30)
- Retorna SVG com: cor visual, modelo, material, marca, peso líquido e QR Code embutido
- `Content-Type: image/svg+xml`
@@ -381,3 +385,35 @@ O backend implementa **last-write-wins com timestamp**:
- Queries usam **bind parameters** do SQLx — nunca interpolação de string em SQL.
- `user_id` é sempre extraído do token JWT, nunca aceito como parâmetro de URL ou body.
- Presets do sistema (`is_system = true`) são protegidos no nível de serviço: edição ou deleção retorna `403 Forbidden`.
+
+---
+
+## Mudanças Recentes (14/03/2026)
+
+### ✅ Correção: PUT Spool Preset retornava FORBIDDEN
+
+**Local:** `src/adapters/outbound/postgres_spool_preset_repo.rs` — método `update()`
+
+**Problema:**
+O SQL UPDATE não validava o `user_id` na cláusula WHERE. Qualquer usuário poderia tentar editar presets de outros usuários ou presets do sistema.
+
+```rust
+// ❌ ANTES (inseguro)
+UPDATE spool_presets
+SET name = $2, spool_weight_g = $3
+WHERE id = $1 AND is_system = false
+RETURNING ...
+```
+
+**Solução:**
+Adicionado `AND user_id = $4` para validar propriedade antes de atualizar:
+
+```rust
+// ✅ DEPOIS (seguro)
+UPDATE spool_presets
+SET name = $2, spool_weight_g = $3
+WHERE id = $1 AND is_system = false AND user_id = $4
+RETURNING ...
+```
+
+Agora o repositório valida que o preset pertence ao usuário autenticado (extraído do JWT). Tentativas de editar presets de outro usuário ou do sistema recebem `404 Not Found` (sem vazar que o preset existe).
diff --git a/backend/bruno-routes/MewoSpool/environments/route.yml b/backend/bruno-routes/MewoSpool/environments/route.yml
index 1c83c5d..b78ac8a 100644
--- a/backend/bruno-routes/MewoSpool/environments/route.yml
+++ b/backend/bruno-routes/MewoSpool/environments/route.yml
@@ -5,6 +5,6 @@ variables:
- secret: true
name: refresh_token
- name: preset_id
- value: 38e439ba-0289-4bf0-9f4b-f9cb12991167
+ value: 88eab1b9-e979-46db-940f-59cac97df60b
- name: filament_id
- value: 0b371f84-706a-4eaa-bff0-33556fa833b4
+ value: da5e431c-bc2e-4075-aa1c-ce82ee9567b4
diff --git a/backend/migrations/20240101000002_create_spool_presets.sql b/backend/migrations/20240101000002_create_spool_presets.sql
index 78bd763..e13e341 100644
--- a/backend/migrations/20240101000002_create_spool_presets.sql
+++ b/backend/migrations/20240101000002_create_spool_presets.sql
@@ -21,7 +21,7 @@ CREATE INDEX idx_spool_presets_is_system ON spool_presets (is_system);
-- Presets built-in do sistema (somente leitura para usuários)
INSERT INTO spool_presets (name, spool_weight_g, is_system) VALUES
('Bambu Lab (Plástico)', 250, TRUE),
- ('Elegoo (Papelão)', 200, TRUE),
+ ('Elegoo (Papelão)', 156, TRUE),
('Creality (Plástico)', 230, TRUE),
('Prusament (Plástico)', 201, TRUE),
('Sunlu (Papelão)', 200, TRUE),
diff --git a/backend/src/adapters/outbound/postgres_spool_preset_repo.rs b/backend/src/adapters/outbound/postgres_spool_preset_repo.rs
index 945223e..8bdc11b 100644
--- a/backend/src/adapters/outbound/postgres_spool_preset_repo.rs
+++ b/backend/src/adapters/outbound/postgres_spool_preset_repo.rs
@@ -69,12 +69,13 @@ impl SpoolPresetRepository for PostgresSpoolPresetRepository {
let row = sqlx::query_as::<_, SpoolPresetRow>(
r#"UPDATE spool_presets
SET name = $2, spool_weight_g = $3
- WHERE id = $1 AND is_system = false
+ WHERE id = $1 AND is_system = false AND user_id = $4
RETURNING id, name, spool_weight_g, is_system, user_id, created_at"#,
)
.bind(preset.id)
.bind(&preset.name)
.bind(preset.spool_weight_g)
+ .bind(preset.user_id)
.fetch_one(self.db.as_ref())
.await?;
diff --git a/mobile/.gitignore b/mobile/.gitignore
index 5873d9a..158b2f1 100644
--- a/mobile/.gitignore
+++ b/mobile/.gitignore
@@ -3,4 +3,54 @@
# The following patterns were generated by expo-cli
expo-env.d.ts
-# @end expo-cli
\ No newline at end of file
+# @end expo-cli
+
+# Environment Variables
+.env
+.env.local
+.env.*.local
+
+# Node
+node_modules/
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+lerna-debug.log
+package-lock.json
+yarn.lock
+
+# IDEs
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+.DS_Store
+*.sublime-workspace
+*.sublime-project
+
+# React Native / Expo
+android/
+ios/
+*.jks
+*.keystore
+build/
+.gradle/
+
+# Expo
+.expo/
+dist/
+.expo-shared/
+
+# Logs
+logs/
+*.log
+
+# Temporary files
+*.tmp
+.cache/
+tmp/
+
+# OS
+Thumbs.db
+.env.*.local
\ No newline at end of file
diff --git a/mobile/agent.md b/mobile/agent.md
index dc8a80e..9ac03b4 100644
--- a/mobile/agent.md
+++ b/mobile/agent.md
@@ -10,24 +10,24 @@ O app é **offline-first**: todos os dados são persistidos localmente em **SQLi
## Stack
-| Componente | Biblioteca | Observação |
-|-----------------------|------------------------------------|-----------------------------------------------|
-| Framework | `expo` ~51 | Managed Workflow |
-| Navigation | `expo-router` ~3 | File-system routing |
-| Linguagem | TypeScript 5.x | strict mode |
-| Node Version | 22 LTS | Gerenciado via `mise` (`.mise.toml`) |
-| Java Version | 17 | Obrigatório para React Native 0.81 (Java 24 quebra CMake) |
-| UI | React Native + `@expo/vector-icons`| Ionicons |
-| Forms | `react-hook-form` + `zod` | Validação em runtime |
-| Estado global | `zustand` | Stores em `src/store/` |
-| HTTP | `axios` | Interceptor Bearer + refresh automático |
-| DB local | `expo-sqlite` + SQLCipher | WAL mode, foreign keys |
-| Auth persistência | `expo-secure-store` | JWT cifrado no keychain |
-| Safe Area | `react-native-safe-area-context` | |
-| Gesture Handler | `react-native-gesture-handler` | |
-| Animations | `react-native-reanimated` | |
-| QR Code render | `react-native-qrcode-svg` | Render de QR Code em tela |
-| Câmera / Scanner | `expo-camera` ~17 | ML Kit barcode scan; requer development build |
+| Componente | Biblioteca | Observação |
+| ----------------- | ----------------------------------- | --------------------------------------------------------- |
+| Framework | `expo` ~51 | Managed Workflow |
+| Navigation | `expo-router` ~3 | File-system routing |
+| Linguagem | TypeScript 5.x | strict mode |
+| Node Version | 22 LTS | Gerenciado via `mise` (`.mise.toml`) |
+| Java Version | 17 | Obrigatório para React Native 0.81 (Java 24 quebra CMake) |
+| UI | React Native + `@expo/vector-icons` | Ionicons |
+| Forms | `react-hook-form` + `zod` | Validação em runtime |
+| Estado global | `zustand` | Stores em `src/store/` |
+| HTTP | `axios` | Interceptor Bearer + refresh automático |
+| DB local | `expo-sqlite` + SQLCipher | WAL mode, foreign keys |
+| Auth persistência | `expo-secure-store` | JWT cifrado no keychain |
+| Safe Area | `react-native-safe-area-context` | |
+| Gesture Handler | `react-native-gesture-handler` | |
+| Animations | `react-native-reanimated` | |
+| QR Code render | `react-native-qrcode-svg` | Render de QR Code em tela |
+| Câmera / Scanner | `expo-camera` ~17 | ML Kit barcode scan; requer development build |
---
@@ -170,20 +170,20 @@ Arquivo: `src/shared/theme.ts`
### Paleta (siamês)
-| Token | Valor | Uso |
-|------------------|-------------|--------------------------------------|
-| `bgBase` | `#1E1B18` | Fundo geral |
-| `bgSurface` | `#2A2622` | Cards, containers |
-| `bgHover` | `#332F2B` | Hover/pressed em cards |
-| `textPrimary` | `#F5EEDC` | Títulos, peso líquido |
-| `textSecondary` | `#C9C1B0` | Descrições, labels |
-| `accent` | `#38BCC2` | Botões de ação, elementos ativos |
-| `accentMuted` | `#38BCC226` | Background de badges accent (10%) |
-| `stockLow` | `#FF6B6B` | ≤ 15% — vermelho |
-| `stockMedium` | `#FF9F43` | ≤ 35% — laranja |
-| `stockOk` | `#38BCC2` | > 35% — accent |
-| `border` | `#3D3830` | Bordas sutis |
-| `error` | `#FF6B6B` | Mensagens de erro |
+| Token | Valor | Uso |
+| --------------- | ----------- | --------------------------------- |
+| `bgBase` | `#1E1B18` | Fundo geral |
+| `bgSurface` | `#2A2622` | Cards, containers |
+| `bgHover` | `#332F2B` | Hover/pressed em cards |
+| `textPrimary` | `#F5EEDC` | Títulos, peso líquido |
+| `textSecondary` | `#C9C1B0` | Descrições, labels |
+| `accent` | `#38BCC2` | Botões de ação, elementos ativos |
+| `accentMuted` | `#38BCC226` | Background de badges accent (10%) |
+| `stockLow` | `#FF6B6B` | ≤ 15% — vermelho |
+| `stockMedium` | `#FF9F43` | ≤ 35% — laranja |
+| `stockOk` | `#38BCC2` | > 35% — accent |
+| `border` | `#3D3830` | Bordas sutis |
+| `error` | `#FF6B6B` | Mensagens de erro |
### Tipografia
@@ -255,10 +255,10 @@ operation TEXT, payload TEXT, created_at TEXT
Sempre usar aliases em vez de caminhos relativos:
```ts
-import { Filament } from '@domain/Filament';
-import { useFilamentStore } from '@store/filamentStore';
-import { colors } from '@shared/theme';
-import { Button } from '@presentation/components/ui/Button';
+import { Filament } from "@domain/Filament";
+import { useFilamentStore } from "@store/filamentStore";
+import { colors } from "@shared/theme";
+import { Button } from "@presentation/components/ui/Button";
```
### Componentes de tela
@@ -283,43 +283,43 @@ Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
### Auth
-| Método | Rota | Descrição |
-|--------|-------------------------------|------------------------------|
-| POST | `/auth/login` | Login email/senha |
-| POST | `/auth/register` | Cadastro |
-| POST | `/auth/google` | OAuth Google |
-| POST | `/auth/refresh` | Refresh token |
-| POST | `/auth/logout` | Logout |
-| POST | `/auth/forgot-password` | Solicitar reset de senha |
-| POST | `/auth/reset-password` | Confirmar reset com token |
-| POST | `/auth/verify-email` | Verificar e-mail com código |
+| Método | Rota | Descrição |
+| ------ | ----------------------- | --------------------------- |
+| POST | `/auth/login` | Login email/senha |
+| POST | `/auth/register` | Cadastro |
+| POST | `/auth/google` | OAuth Google |
+| POST | `/auth/refresh` | Refresh token |
+| POST | `/auth/logout` | Logout |
+| POST | `/auth/forgot-password` | Solicitar reset de senha |
+| POST | `/auth/reset-password` | Confirmar reset com token |
+| POST | `/auth/verify-email` | Verificar e-mail com código |
### Filaments
-| Método | Rota | Descrição |
-|--------|-------------------------------|------------------------------|
-| GET | `/filaments` | Listar (com filtros) |
-| POST | `/filaments` | Criar |
-| GET | `/filaments/:id` | Detalhe |
-| PATCH | `/filaments/:id` | Atualizar |
-| DELETE | `/filaments/:id` | Excluir |
-| GET | `/filaments/:id/qrcode` | QR Code SVG |
-| GET | `/filaments/:id/label` | Etiqueta SVG |
+| Método | Rota | Descrição |
+| ------ | ----------------------- | -------------------- |
+| GET | `/filaments` | Listar (com filtros) |
+| POST | `/filaments` | Criar |
+| GET | `/filaments/:id` | Detalhe |
+| PATCH | `/filaments/:id` | Atualizar |
+| DELETE | `/filaments/:id` | Excluir |
+| GET | `/filaments/:id/qrcode` | QR Code SVG |
+| GET | `/filaments/:id/label` | Etiqueta SVG |
### Spool Presets
-| Método | Rota | Descrição |
-|--------|-------------------------------|------------------------------|
-| GET | `/spool-presets` | Listar (sistema + usuário) |
-| POST | `/spool-presets` | Criar preset do usuário |
-| PATCH | `/spool-presets/:id` | Atualizar preset do usuário |
-| DELETE | `/spool-presets/:id` | Excluir preset do usuário |
+| Método | Rota | Descrição |
+| ------ | -------------------- | --------------------------- |
+| GET | `/spool-presets` | Listar (sistema + usuário) |
+| POST | `/spool-presets` | Criar preset do usuário |
+| PATCH | `/spool-presets/:id` | Atualizar preset do usuário |
+| DELETE | `/spool-presets/:id` | Excluir preset do usuário |
### Dashboard
-| Método | Rota | Descrição |
-|--------|-------------------------------|------------------------------|
-| GET | `/dashboard` | Dados agregados do dashboard |
+| Método | Rota | Descrição |
+| ------ | ------------ | ---------------------------- |
+| GET | `/dashboard` | Dados agregados do dashboard |
---
@@ -336,6 +336,152 @@ Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
---
+## Mudanças Recentes (14/03/2026)
+
+### ✅ Implementações Completadas
+
+#### 1. **Delete Filament — Integração com API**
+
+- **Arquivos**: `app/(app)/inventory/[id].tsx`, `app/(app)/inventory/[id]/edit.tsx`
+- **Mudança**: `handleDelete()` agora chama `deleteFilamentUseCase.execute()` antes de remover do store local
+- **Código**:
+ ```typescript
+ const handleDelete = async () => {
+ try {
+ const user = useAuthStore.getState().user;
+ await deleteFilamentUseCase.execute(filament!.id, user?.id);
+ removeFilament(filament!.id);
+ Alert.alert("Sucesso", "Filamento deletado da API");
+ } catch (error) {
+ Alert.alert("Erro", "Falha ao deletar: " + (error as Error).message);
+ }
+ };
+ ```
+- **Impacto**: Delete agora funciona corretamente em ambos os screens (detail e edit)
+
+#### 2. **Pull-to-Refresh — Inventory List**
+
+- **Arquivo**: `app/(tabs)/inventory.tsx`
+- **Mudança**: Adicionado `RefreshControl` ao FlatList com chamada a `listFilamentsUseCase.execute()`
+- **Código**:
+
+ ```typescript
+ const [isRefreshing, setIsRefreshing] = useState(false);
+
+ const onRefresh = async () => {
+ setIsRefreshing(true);
+ try {
+ const user = useAuthStore.getState().user;
+ const result = await listFilamentsUseCase.execute(user?.id);
+ setFilaments(result);
+ } finally {
+ setIsRefreshing(false);
+ }
+ };
+
+ }
+ // ... resto do componente
+ />
+ ```
+
+- **Impacto**: Usuários podem puxar para baixo e sincronizar lista sem reabrir o app
+
+#### 3. **Update Filament — Integração com API**
+
+- **Arquivo**: `app/(app)/inventory/[id]/edit.tsx`
+- **Mudança**: `onSubmit()` agora chama `updateFilamentUseCase.execute()` em vez de apenas atualizar o store local
+- **Código**:
+ ```typescript
+ const onSubmit = async () => {
+ try {
+ const user = useAuthStore.getState().user;
+ const result = await updateFilamentUseCase.execute(
+ {
+ id: filament!.id,
+ brand: formData.brand,
+ model: formData.model,
+ color: formData.color,
+ weight_g: parseFloat(formData.weight_g),
+ },
+ user?.id,
+ );
+
+ updateFilament(result);
+ Alert.alert("Sucesso", "Filamento atualizado na API");
+ router.back();
+ } catch (error) {
+ Alert.alert("Erro", "Falha ao atualizar: " + (error as Error).message);
+ }
+ };
+ ```
+- **Impacto**: Edições de filamento agora persistem no servidor
+
+#### 4. **Custom Color Picker — Hex Input com Validação**
+
+- **Arquivos**: `app/(app)/inventory/new.tsx`, `app/(app)/inventory/[id]/edit.tsx`
+- **Mudança**: Adicionado `handleOpenColorPicker()` que usa `Alert.prompt()` para capturar hexadecimais
+- **Validação**:
+
+ ```typescript
+ const isValidHex = (hex: string): boolean => {
+ return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(hex);
+ };
+
+ const normalizeHex = (hex: string): string => {
+ return hex.startsWith("#") ? hex : `#${hex}`;
+ };
+
+ const handleOpenColorPicker = () => {
+ setIsOpeningColorPicker(true);
+ Alert.prompt(
+ "Hex Color",
+ "Enter hex color (e.g., #FF5733 or FF5733)",
+ [
+ {
+ text: "Cancel",
+ onPress: () => setIsOpeningColorPicker(false),
+ style: "cancel",
+ },
+ {
+ text: "Add Color",
+ onPress: (input: string | undefined) => {
+ if (input && isValidHex(input)) {
+ setFormData((prev) => ({ ...prev, color: normalizeHex(input) }));
+ } else {
+ Alert.alert("Invalid hex color", "Please enter a valid hex code");
+ }
+ setIsOpeningColorPicker(false);
+ },
+ },
+ ],
+ "plain-text",
+ );
+ };
+ ```
+
+- **Integração**: Botão "+" vinculado a `handleOpenColorPicker`
+- **Impacto**: Usuários podem agora inserir cores customizadas por código hexadecimal
+
+#### 5. **Sincronização de Estado — API-First Pattern**
+
+- **Padrão**: Todas operações agora seguem: **API call → store update**
+- **Benefício**: Source of truth centralizada no servidor
+- **Implementação**:
+ - Delete: `deleteFilamentUseCase` → `removeFilament()`
+ - Update: `updateFilamentUseCase` → `updateFilament(result)`
+ - Refresh: `listFilamentsUseCase` → `setFilaments(result)`
+ - Create: `createFilamentUseCase` → `addFilament(result)`
+
+### Arquitetura Mantida
+
+- Todos use cases injetados via `container.ts` (Dependency Injection)
+- Zod schemas usados para validação de formulários
+- JWT interceptor em Axios automaticamente adiciona `Authorization` header
+- Error handling com `Alert.alert()` visível ao usuário
+
+---
+
## Executando o Projeto
```bash
@@ -354,6 +500,7 @@ mise exec -- npx expo run:ios
```
> **Variável de ambiente**: crie `mobile/.env` com:
+>
> ```
> EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1
> ```
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index b0e3a42..c14fd78 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -87,13 +87,13 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
- namespace "com.meowspool"
+ namespace 'com.meowspool.app'
defaultConfig {
- applicationId "com.meowspool"
+ applicationId 'com.meowspool.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
- versionName "1.0"
+ versionName "1.0.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index d321e54..903ea06 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -1,29 +1,33 @@
-
+
-
+
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/mobile/android/app/src/main/java/com/meowspool/app/MainActivity.kt b/mobile/android/app/src/main/java/com/meowspool/app/MainActivity.kt
index 16a5dea..551ef02 100644
--- a/mobile/android/app/src/main/java/com/meowspool/app/MainActivity.kt
+++ b/mobile/android/app/src/main/java/com/meowspool/app/MainActivity.kt
@@ -1,4 +1,5 @@
package com.meowspool.app
+import expo.modules.splashscreen.SplashScreenManager
import android.os.Build
import android.os.Bundle
@@ -10,15 +11,15 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
-import com.meowspool.R
-import com.meowspool.BuildConfig
-
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
- setTheme(R.style.AppTheme);
+ // setTheme(R.style.AppTheme);
+ // @generated begin expo-splashscreen - expo prebuild (DO NOT MODIFY) sync-f3ff59a738c56c9a6119210cb55f0b613eb8b6af
+ SplashScreenManager.registerOnActivity(this)
+ // @generated end expo-splashscreen
super.onCreate(null)
}
diff --git a/mobile/android/app/src/main/java/com/meowspool/app/MainApplication.kt b/mobile/android/app/src/main/java/com/meowspool/app/MainApplication.kt
index 8d95743..270af62 100644
--- a/mobile/android/app/src/main/java/com/meowspool/app/MainApplication.kt
+++ b/mobile/android/app/src/main/java/com/meowspool/app/MainApplication.kt
@@ -16,8 +16,6 @@ import com.facebook.react.defaults.DefaultReactNativeHost
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
-import com.meowspool.BuildConfig
-
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
diff --git a/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
index 31df827..65454e7 100644
Binary files a/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png and b/mobile/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ
diff --git a/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
index ef243aa..2c12434 100644
Binary files a/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png and b/mobile/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
index e9d5474..c804ce4 100644
Binary files a/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png and b/mobile/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
index d61da15..0a68d5d 100644
Binary files a/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png and b/mobile/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ
diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
index 4aeed11..53f79bc 100644
Binary files a/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png and b/mobile/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ
diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index a2f5908..0b57cbb 100644
Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index 1b52399..2ea0d30 100644
Binary files a/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index ff10afd..0869e9e 100644
Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 115a4c7..f5a827f 100644
Binary files a/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index dcd3cd8..e69de29 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 459ca60..00567fa 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 8ca12fe..1903124 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 8e19b41..0b85ec3 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index b824ebd..f77328f 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 4c19a13..c1f6ac8 100644
Binary files a/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/mobile/android/app/src/main/res/values/colors.xml b/mobile/android/app/src/main/res/values/colors.xml
index 21cc155..dba5802 100644
--- a/mobile/android/app/src/main/res/values/colors.xml
+++ b/mobile/android/app/src/main/res/values/colors.xml
@@ -1,4 +1,6 @@
-
- #FFFFFF
+ #1E1B18
+ #1E1B18
+ #023c69
+ #1E1B18
\ No newline at end of file
diff --git a/mobile/android/app/src/main/res/values/strings.xml b/mobile/android/app/src/main/res/values/strings.xml
index 6805537..a65add6 100644
--- a/mobile/android/app/src/main/res/values/strings.xml
+++ b/mobile/android/app/src/main/res/values/strings.xml
@@ -1,3 +1,6 @@
- MeowSpool
-
+ MeowSpool
+ contain
+ false
+ dark
+
\ No newline at end of file
diff --git a/mobile/android/app/src/main/res/values/styles.xml b/mobile/android/app/src/main/res/values/styles.xml
index 26f3404..ab034d6 100644
--- a/mobile/android/app/src/main/res/values/styles.xml
+++ b/mobile/android/app/src/main/res/values/styles.xml
@@ -1,8 +1,14 @@
-
+
-
-
+
\ No newline at end of file
diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties
index b09dfd9..8e39f82 100644
--- a/mobile/android/gradle.properties
+++ b/mobile/android/gradle.properties
@@ -10,13 +10,12 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
-org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
-org.gradle.caching=false
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
@@ -60,3 +59,7 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false
+
+# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
+# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
+expo.edgeToEdgeEnabled=true
diff --git a/mobile/app/(app)/(tabs)/inventory.tsx b/mobile/app/(app)/(tabs)/inventory.tsx
index 811d236..af641dd 100644
--- a/mobile/app/(app)/(tabs)/inventory.tsx
+++ b/mobile/app/(app)/(tabs)/inventory.tsx
@@ -1,11 +1,13 @@
import React, { useState } from 'react';
import {
- View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput,
+ View, Text, FlatList, TouchableOpacity, StyleSheet, TextInput, RefreshControl,
} from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFilamentStore } from '@store/filamentStore';
+import { listFilamentsUseCase } from '@infrastructure/container';
+import { useAuthStore } from '@store/authStore';
import { FilamentCard } from '@presentation/components/filament/FilamentCard';
import type { Filament } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
@@ -19,9 +21,11 @@ const TABS = ['Todos', ...MATERIALS.slice(0, 4)] as const;
*/
export default function InventoryScreen(): React.ReactElement {
const router = useRouter();
- const { filaments } = useFilamentStore();
+ const { filaments, setFilaments } = useFilamentStore();
+ const { user } = useAuthStore();
const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState('Todos');
+ const [isRefreshing, setIsRefreshing] = useState(false);
const filtered = filaments.filter((f) => {
const matchMaterial = activeTab === 'Todos' || f.material === activeTab;
@@ -33,6 +37,18 @@ export default function InventoryScreen(): React.ReactElement {
return matchMaterial && matchSearch;
});
+ async function onRefresh(): Promise {
+ setIsRefreshing(true);
+ try {
+ const updated = await listFilamentsUseCase.execute(user?.id || '');
+ setFilaments(updated);
+ } catch (err) {
+ console.error('refresh filaments error', err);
+ } finally {
+ setIsRefreshing(false);
+ }
+ }
+
function renderItem({ item }: { item: Filament }): React.ReactElement {
return ;
}
@@ -96,6 +112,9 @@ export default function InventoryScreen(): React.ReactElement {
contentContainerStyle={styles.list}
ItemSeparatorComponent={() => }
showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
ListEmptyComponent={
diff --git a/mobile/app/(app)/inventory/[id].tsx b/mobile/app/(app)/inventory/[id].tsx
index c253751..588a936 100644
--- a/mobile/app/(app)/inventory/[id].tsx
+++ b/mobile/app/(app)/inventory/[id].tsx
@@ -7,6 +7,8 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useFilamentStore } from '@store/filamentStore';
import { usePresetStore } from '@store/presetStore';
+import { useAuthStore } from '@store/authStore';
+import { deleteFilamentUseCase } from '@infrastructure/container';
import { calcFilamentPercentage } from '@domain/Filament';
import { colors, typography, spacing, radius, getStockColor, getStockBgColor } from '@shared/theme';
import { formatWeight } from '@shared/utils/filament';
@@ -52,9 +54,16 @@ export default function FilamentDetailScreen(): React.ReactElement {
{
text: 'Excluir',
style: 'destructive',
- onPress: () => {
- removeFilament(filament!.id);
- router.back();
+ onPress: async () => {
+ try {
+ const { user } = useAuthStore.getState();
+ await deleteFilamentUseCase.execute(filament!.id, user?.id || '');
+ removeFilament(filament!.id);
+ router.back();
+ } catch (err) {
+ console.error('delete filament error', err);
+ Alert.alert('Erro', 'Não foi possível deletar o filamento.');
+ }
},
},
],
diff --git a/mobile/app/(app)/inventory/[id]/edit.tsx b/mobile/app/(app)/inventory/[id]/edit.tsx
index 4336075..95a7c1e 100644
--- a/mobile/app/(app)/inventory/[id]/edit.tsx
+++ b/mobile/app/(app)/inventory/[id]/edit.tsx
@@ -10,13 +10,15 @@ import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { usePresetStore } from '@store/presetStore';
import { useFilamentStore } from '@store/filamentStore';
+import { useAuthStore } from '@store/authStore';
+import { deleteFilamentUseCase, updateFilamentUseCase } from '@infrastructure/container';
import { Input } from '@presentation/components/ui/Input';
import { Button } from '@presentation/components/ui/Button';
import { Card } from '@presentation/components/ui/Card';
import { calcNetWeight } from '@domain/Filament';
import { colors, typography, spacing, radius } from '@shared/theme';
import { MATERIALS, type Material } from '@shared/constants';
-import { formatWeight } from '@shared/utils/filament';
+import { formatWeight, isValidHex, normalizeHex } from '@shared/utils/filament';
const schema = z.object({
brand: z.string().min(1, 'Marca obrigatória'),
@@ -48,6 +50,7 @@ export default function EditFilamentScreen(): React.ReactElement {
const [selectedMaterial, setSelectedMaterial] = useState((filament?.material as Material) ?? 'PLA');
const [selectedPresetId, setSelectedPresetId] = useState(filament?.spoolPresetId ?? systemPresets[0]?.id ?? null);
const [isLoading, setIsLoading] = useState(false);
+ const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
const { control, handleSubmit, watch, formState: { errors } } = useForm({
resolver: zodResolver(schema),
@@ -82,23 +85,66 @@ export default function EditFilamentScreen(): React.ReactElement {
);
}
- function handleDelete(): void {
+ const handleOpenColorPicker = (): void => {
+ if (isOpeningColorPicker) return;
+ setIsOpeningColorPicker(true);
+ Alert.prompt(
+ 'Cor Customizada',
+ 'Digite um código de cor hexadecimal (ex: #FF5733)',
+ [
+ { text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
+ {
+ text: 'Confirmar',
+ onPress: (value) => {
+ setIsOpeningColorPicker(false);
+ if (!value) return;
+ const normalized = normalizeHex(value.trim());
+ if (isValidHex(normalized)) {
+ setSelectedColor(normalized);
+ setHexInput(normalized);
+ } else {
+ Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
+ }
+ },
+ },
+ ],
+ 'plain-text',
+ selectedColor,
+ );
+ };
+
+ const handleDelete = async (): Promise => {
Alert.alert(
- 'Excluir filamento',
- `Deseja excluir "${filament!.brand}${filament!.model ? ` ${filament!.model}` : ''}"? Esta ação não pode ser desfeita.`,
+ 'Confirmar Deleção',
+ 'Tem certeza que deseja deletar este filamento?',
[
{ text: 'Cancelar', style: 'cancel' },
{
- text: 'Excluir',
+ text: 'Deletar',
style: 'destructive',
- onPress: () => {
- removeFilament(filament!.id);
- router.replace('/(app)/(tabs)/inventory');
+ onPress: async () => {
+ const user = useAuthStore.getState().user;
+ if (!user) {
+ Alert.alert('Erro', 'Usuário não autenticado.');
+ return;
+ }
+ setIsLoading(true);
+ try {
+ await deleteFilamentUseCase.execute(filament!.id, user.id);
+ removeFilament(filament!.id);
+ Alert.alert('Sucesso', 'Filamento deletado.');
+ router.back();
+ } catch (err) {
+ console.error('delete filament error', err);
+ Alert.alert('Erro', 'Falha ao deletar: ' + (err as Error).message);
+ } finally {
+ setIsLoading(false);
+ }
},
},
],
);
- }
+ };
async function onSubmit(data: FormData): Promise {
if (!selectedPresetId) {
@@ -163,7 +209,7 @@ export default function EditFilamentScreen(): React.ReactElement {
style={[styles.colorDot, { backgroundColor: c }, selectedColor === c && styles.colorDotActive]}
/>
))}
-
+
diff --git a/mobile/app/(app)/inventory/new.tsx b/mobile/app/(app)/inventory/new.tsx
index d40caf2..aa79eb3 100644
--- a/mobile/app/(app)/inventory/new.tsx
+++ b/mobile/app/(app)/inventory/new.tsx
@@ -19,6 +19,7 @@ import { colors, typography, spacing, radius } from '@shared/theme';
import { MATERIALS, type Material } from '@shared/constants';
import { formatWeight } from '@shared/utils/filament';
import { createFilamentUseCase } from '@infrastructure/container';
+import { isValidHex, normalizeHex } from '@shared/utils/filament';
const schema = z.object({
brand: z.string().min(1, 'Marca obrigatória'),
@@ -48,6 +49,7 @@ export default function NewFilamentScreen(): React.ReactElement {
const [selectedMaterial, setSelectedMaterial] = useState('PLA');
const [selectedPresetId, setSelectedPresetId] = useState(systemPresets[0]?.id ?? null);
const [isLoading, setIsLoading] = useState(false);
+ const [isOpeningColorPicker, setIsOpeningColorPicker] = useState(false);
// Quando os presets carregam (via layout), seleciona o primeiro sistema automaticamente
useEffect(() => {
@@ -67,7 +69,35 @@ export default function NewFilamentScreen(): React.ReactElement {
? calcNetWeight(Number(totalWeight), selectedPreset.spoolWeightG)
: 0;
const pct = Math.min(100, Math.round((netWeight / 1000) * 100));
+function handleOpenColorPicker(): void {
+ if (isOpeningColorPicker) return;
+ setIsOpeningColorPicker(true);
+ Alert.prompt(
+ 'Cor Customizada',
+ 'Digite um código de cor hexadecimal (ex: #FF5733)',
+ [
+ { text: 'Cancelar', style: 'cancel', onPress: () => setIsOpeningColorPicker(false) },
+ {
+ text: 'Confirmar',
+ onPress: (value) => {
+ setIsOpeningColorPicker(false);
+ if (!value) return;
+ const normalized = normalizeHex(value.trim());
+ if (isValidHex(normalized)) {
+ setSelectedColor(normalized);
+ setHexInput(normalized);
+ } else {
+ Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.');
+ }
+ },
+ },
+ ],
+ 'plain-text',
+ selectedColor,
+ );
+ }
+
async function onSubmit(data: FormData): Promise {
if (!selectedPresetId) {
Alert.alert('Atenção', 'Selecione um preset de carretel.');
@@ -102,7 +132,7 @@ export default function NewFilamentScreen(): React.ReactElement {
}
return (
-
+ onPress={handleOpenColorPicker}
{/* Header */}
@@ -130,7 +160,7 @@ export default function NewFilamentScreen(): React.ReactElement {
style={[styles.colorDot, { backgroundColor: c }, selectedColor === c && styles.colorDotActive]}
/>
))}
-
+