v1
This commit is contained in:
parent
55d3afa636
commit
850d636d8a
6
App.tsx
Normal file
6
App.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import AppNavigator from './src/navigation/AppNavigator';
|
||||
|
||||
export default function App() {
|
||||
return <AppNavigator />;
|
||||
}
|
||||
49
README-mobile.md
Normal file
49
README-mobile.md
Normal file
@ -0,0 +1,49 @@
|
||||
# ManutApp — scaffold Expo/React Native
|
||||
|
||||
Estrutura `src/` criada conforme solicitado, com telas `.tsx` e chamadas fake em `src/api/fakeApi.ts`.
|
||||
|
||||
## Login fake
|
||||
|
||||
- `tecnico` / `123456`
|
||||
- `admin` / `123456`
|
||||
|
||||
## Árvore principal
|
||||
|
||||
```text
|
||||
src
|
||||
├── api/fakeApi.ts
|
||||
├── assets
|
||||
│ ├── README.md
|
||||
│ └── styles/theme.ts
|
||||
├── components/ui.tsx
|
||||
├── navigation
|
||||
│ ├── AppNavigator.tsx
|
||||
│ └── routes.ts
|
||||
└── pages
|
||||
├── login/index.tsx
|
||||
├── home/index.tsx
|
||||
├── ordem-de-serviço/index.tsx
|
||||
├── ordem-de-serviço/nova-ordem/index.tsx
|
||||
├── lojas/index.tsx
|
||||
├── lojas/nova-loja/index.tsx
|
||||
├── peças/index.tsx
|
||||
├── peças/nova-peça/index.tsx
|
||||
├── fornos/index.tsx
|
||||
├── fornos/novo-forno/index.tsx
|
||||
├── peças-forno/index.tsx
|
||||
├── peças-forno/nova-peça/index.tsx
|
||||
├── manutençao/index.tsx
|
||||
└── manutençao/nova-manutenções/index.tsx
|
||||
```
|
||||
|
||||
## Dados e regras mockados
|
||||
|
||||
- Loja: `id`, `descricao`, `endereco`, `nomeContato`, `numeroContato`.
|
||||
- Peça: `id`, `descricao`, `localizacao`, `referencia`.
|
||||
- Localizações: Câmara de Cocção (`CC`), Painel de controle (`PCU`), Painel de Comandos (`PCE`), Gerador de Vapor (`GV`), Estrutura externa (`EE`).
|
||||
- Referência da peça: `ref + "00" + id`, exemplo `CC001`.
|
||||
- Forno: `id`, `lojaId`, `descricao`, `modelo`, `numeroSerie`.
|
||||
- Peças por forno: associação muitos-para-muitos com `fornoId` + `pecaIds`.
|
||||
- Manutenção: escolhe loja, forno e uma ou várias peças utilizadas.
|
||||
|
||||
Para conectar ao backend real depois, substitua as funções de `fakeApi.ts` por `fetch`/Axios mantendo as mesmas assinaturas.
|
||||
132
api/mock.php
Normal file
132
api/mock.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
function respond(array $payload, int $status = 200): void {
|
||||
http_response_code($status);
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
function ensure_schema(): void {
|
||||
db()->exec("CREATE TABLE IF NOT EXISTS maintenance_records (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
store_id VARCHAR(32) NOT NULL,
|
||||
store_name VARCHAR(160) NOT NULL,
|
||||
oven_id VARCHAR(32) NOT NULL,
|
||||
oven_name VARCHAR(160) NOT NULL,
|
||||
technician VARCHAR(120) NOT NULL,
|
||||
service_type VARCHAR(60) NOT NULL,
|
||||
parts_json JSON NULL,
|
||||
notes TEXT NULL,
|
||||
status VARCHAR(40) NOT NULL DEFAULT 'Concluída',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
}
|
||||
|
||||
function seed_data(): array {
|
||||
return [
|
||||
'stores' => [
|
||||
['id' => 'lj-001', 'name' => 'Loja Centro', 'city' => 'São Paulo'],
|
||||
['id' => 'lj-002', 'name' => 'Loja Norte', 'city' => 'Guarulhos'],
|
||||
['id' => 'lj-003', 'name' => 'Loja Sul', 'city' => 'Santo André'],
|
||||
],
|
||||
'ovens' => [
|
||||
['id' => 'fn-101', 'store_id' => 'lj-001', 'name' => 'Forno Turbo 01', 'model' => 'FT-900'],
|
||||
['id' => 'fn-102', 'store_id' => 'lj-001', 'name' => 'Forno Lastro 02', 'model' => 'FL-450'],
|
||||
['id' => 'fn-201', 'store_id' => 'lj-002', 'name' => 'Forno Turbo 03', 'model' => 'FT-900'],
|
||||
['id' => 'fn-301', 'store_id' => 'lj-003', 'name' => 'Forno Compacto 01', 'model' => 'FC-220'],
|
||||
],
|
||||
'parts' => [
|
||||
['id' => 'pc-01', 'name' => 'Resistência', 'sku' => 'RES-220'],
|
||||
['id' => 'pc-02', 'name' => 'Termostato', 'sku' => 'TER-110'],
|
||||
['id' => 'pc-03', 'name' => 'Sensor de temperatura', 'sku' => 'SEN-300'],
|
||||
['id' => 'pc-04', 'name' => 'Correia do motor', 'sku' => 'COR-040'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function maintenance_rows(): array {
|
||||
$stmt = db()->query('SELECT * FROM maintenance_records ORDER BY created_at DESC, id DESC LIMIT 50');
|
||||
$rows = $stmt->fetchAll();
|
||||
foreach ($rows as &$row) {
|
||||
$row['parts'] = $row['parts_json'] ? json_decode((string)$row['parts_json'], true) : [];
|
||||
unset($row['parts_json']);
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
try {
|
||||
ensure_schema();
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$action = $_GET['action'] ?? 'bootstrap';
|
||||
$seed = seed_data();
|
||||
|
||||
if ($method === 'GET' && $action === 'bootstrap') {
|
||||
respond($seed + ['maintenance' => maintenance_rows()]);
|
||||
}
|
||||
|
||||
if ($method === 'GET' && $action === 'maintenance') {
|
||||
respond(['maintenance' => maintenance_rows()]);
|
||||
}
|
||||
|
||||
if ($method === 'POST' && $action === 'maintenance') {
|
||||
$input = json_decode(file_get_contents('php://input') ?: '{}', true);
|
||||
if (!is_array($input)) {
|
||||
respond(['success' => false, 'error' => 'Payload inválido.'], 400);
|
||||
}
|
||||
|
||||
$storeId = trim((string)($input['store_id'] ?? ''));
|
||||
$ovenId = trim((string)($input['oven_id'] ?? ''));
|
||||
$technician = trim((string)($input['technician'] ?? ''));
|
||||
$serviceType = trim((string)($input['service_type'] ?? ''));
|
||||
$notes = trim((string)($input['notes'] ?? ''));
|
||||
$parts = is_array($input['parts'] ?? null) ? array_values($input['parts']) : [];
|
||||
|
||||
$store = null;
|
||||
foreach ($seed['stores'] as $candidate) {
|
||||
if ($candidate['id'] === $storeId) { $store = $candidate; break; }
|
||||
}
|
||||
$oven = null;
|
||||
foreach ($seed['ovens'] as $candidate) {
|
||||
if ($candidate['id'] === $ovenId && $candidate['store_id'] === $storeId) { $oven = $candidate; break; }
|
||||
}
|
||||
|
||||
if (!$store || !$oven || $technician === '' || $serviceType === '' || count($parts) < 1) {
|
||||
respond(['success' => false, 'error' => 'Preencha loja, forno, técnico, tipo e pelo menos uma peça.'], 422);
|
||||
}
|
||||
|
||||
$allowedParts = array_column($seed['parts'], null, 'id');
|
||||
$cleanParts = [];
|
||||
foreach ($parts as $partId) {
|
||||
$partId = (string)$partId;
|
||||
if (isset($allowedParts[$partId])) {
|
||||
$cleanParts[] = $allowedParts[$partId];
|
||||
}
|
||||
}
|
||||
if (!$cleanParts) {
|
||||
respond(['success' => false, 'error' => 'Selecione uma peça válida.'], 422);
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('INSERT INTO maintenance_records (store_id, store_name, oven_id, oven_name, technician, service_type, parts_json, notes, status) VALUES (:store_id, :store_name, :oven_id, :oven_name, :technician, :service_type, :parts_json, :notes, :status)');
|
||||
$stmt->execute([
|
||||
':store_id' => $store['id'],
|
||||
':store_name' => $store['name'],
|
||||
':oven_id' => $oven['id'],
|
||||
':oven_name' => $oven['name'],
|
||||
':technician' => substr($technician, 0, 120),
|
||||
':service_type' => substr($serviceType, 0, 60),
|
||||
':parts_json' => json_encode($cleanParts, JSON_UNESCAPED_UNICODE),
|
||||
':notes' => substr($notes, 0, 500),
|
||||
':status' => 'Concluída',
|
||||
]);
|
||||
|
||||
respond(['success' => true, 'id' => (int)db()->lastInsertId(), 'maintenance' => maintenance_rows()], 201);
|
||||
}
|
||||
|
||||
respond(['success' => false, 'error' => 'Rota não encontrada.'], 404);
|
||||
} catch (Throwable $e) {
|
||||
error_log('mock api error: ' . $e->getMessage());
|
||||
respond(['success' => false, 'error' => 'Erro interno ao processar a API mock.'], 500);
|
||||
}
|
||||
@ -1,403 +1,221 @@
|
||||
:root {
|
||||
--app-bg: #f8fafc;
|
||||
--app-surface: #ffffff;
|
||||
--app-text: #111827;
|
||||
--app-muted: #6b7280;
|
||||
--app-border: #e5e7eb;
|
||||
--app-soft: #f3f4f6;
|
||||
--app-accent: #2563eb;
|
||||
--app-radius-sm: 6px;
|
||||
--app-radius-md: 10px;
|
||||
--app-radius-lg: 14px;
|
||||
--app-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--app-bg);
|
||||
color: var(--app-text);
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -48px;
|
||||
z-index: 1000;
|
||||
background: var(--app-text);
|
||||
color: #fff;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--app-radius-sm);
|
||||
}
|
||||
.skip-link:focus { top: 12px; }
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
.phone-frame {
|
||||
width: min(100%, 430px);
|
||||
min-height: min(860px, calc(100vh - 40px));
|
||||
background: var(--app-surface);
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 22px;
|
||||
box-shadow: var(--app-shadow);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
.app-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 18px 18px 14px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.eyebrow, .label {
|
||||
margin: 0 0 4px;
|
||||
color: var(--app-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
h1, h2, p { margin-top: 0; }
|
||||
h1 { margin-bottom: 0; font-size: 20px; font-weight: 700; letter-spacing: -0.02em; }
|
||||
h2 { font-size: 24px; line-height: 1.15; font-weight: 700; letter-spacing: -0.04em; }
|
||||
|
||||
.screen {
|
||||
display: none;
|
||||
flex: 1;
|
||||
padding: 18px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.screen.active { display: block; }
|
||||
|
||||
.hero-card, .status-card, .detail-card, .empty-state, .list-item, .module-card, .parts-box {
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-radius-lg);
|
||||
background: var(--app-surface);
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
.hero-card {
|
||||
padding: 22px;
|
||||
margin-bottom: 18px;
|
||||
background: #fcfcfd;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
.muted, .empty-state p, small { color: var(--app-muted); }
|
||||
.stack { display: grid; gap: 14px; }
|
||||
|
||||
.form-control, .form-select {
|
||||
border-color: var(--app-border);
|
||||
border-radius: var(--app-radius-md);
|
||||
min-height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form-control:focus, .form-select:focus, .btn:focus-visible, .module-card:focus-visible, .list-item:focus-visible {
|
||||
border-color: var(--app-accent);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, .14);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
.btn { border-radius: var(--app-radius-md); font-weight: 650; min-height: 40px; }
|
||||
.btn-dark { background: var(--app-text); border-color: var(--app-text); }
|
||||
|
||||
.status-card {
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
background: #fcfcfd;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.module-card {
|
||||
min-height: 112px;
|
||||
padding: 14px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color .16s ease, transform .16s ease, background .16s ease;
|
||||
}
|
||||
.module-card:hover { border-color: #cbd5e1; transform: translateY(-1px); }
|
||||
.module-card.featured { grid-column: span 2; background: #f9fafb; }
|
||||
.module-card span { display: block; color: var(--app-accent); font-size: 18px; margin-bottom: 12px; }
|
||||
.module-card strong, .module-card small { display: block; }
|
||||
|
||||
.screen-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
padding: 0.85rem 1.1rem;
|
||||
border-radius: 16px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
.list-stack { display: grid; gap: 10px; }
|
||||
.list-item {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-item:hover { background: #f9fafb; }
|
||||
.list-item-row { display: flex; justify-content: space-between; gap: 10px; align-items: start; }
|
||||
|
||||
.empty-state { padding: 18px; text-align: center; background: #fcfcfd; }
|
||||
.empty-state p { margin: 8px 0 14px; }
|
||||
|
||||
.parts-box { padding: 14px; }
|
||||
.parts-box legend { float: none; width: auto; margin-bottom: 10px; font-size: 13px; font-weight: 700; }
|
||||
.parts-list { display: grid; gap: 8px; }
|
||||
.part-choice {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
.part-choice:hover { background: #f9fafb; }
|
||||
.part-choice input { width: 16px; height: 16px; }
|
||||
|
||||
.detail-card { padding: 18px; }
|
||||
.detail-grid { display: grid; gap: 12px; margin-top: 14px; }
|
||||
.detail-row { padding-bottom: 10px; border-bottom: 1px solid var(--app-border); }
|
||||
.detail-row:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
.detail-row span { display: block; color: var(--app-muted); font-size: 12px; margin-bottom: 2px; }
|
||||
|
||||
.toast-region {
|
||||
position: absolute;
|
||||
width: min(390px, calc(100% - 48px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 78px;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
.app-toast {
|
||||
border: 1px solid var(--app-border);
|
||||
background: var(--app-text);
|
||||
color: #fff;
|
||||
border-radius: var(--app-radius-md);
|
||||
padding: 10px 12px;
|
||||
box-shadow: var(--app-shadow);
|
||||
font-size: 13px;
|
||||
}
|
||||
.app-toast.error { background: #991b1b; }
|
||||
|
||||
.app-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px 18px;
|
||||
border-top: 1px solid var(--app-border);
|
||||
color: var(--app-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
@media (max-width: 460px) {
|
||||
.app-shell { padding: 0; }
|
||||
.phone-frame { min-height: 100vh; border: 0; border-radius: 0; }
|
||||
}
|
||||
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-input-area form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
background: rgba(238, 119, 82, 0.4);
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
background: rgba(35, 166, 213, 0.4);
|
||||
animation-delay: -7s;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
background: rgba(231, 60, 126, 0.3);
|
||||
animation-delay: -14s;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
.header-link {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-link:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Admin Styles */
|
||||
.admin-container {
|
||||
max-width: 900px;
|
||||
margin: 3rem auto;
|
||||
padding: 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.admin-container h1 {
|
||||
margin-top: 0;
|
||||
color: #212529;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.admin-card {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 2.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.admin-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #212529;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #0088cc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.webhook-url {
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.history-table-container {
|
||||
overflow-x: auto;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-table-time {
|
||||
width: 15%;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.history-table-user {
|
||||
width: 35%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.history-table-ai {
|
||||
width: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
@ -1,39 +1,202 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
(() => {
|
||||
const app = document.querySelector('[data-api-base]');
|
||||
const apiBase = app.dataset.apiBase;
|
||||
const state = {
|
||||
user: null,
|
||||
stores: [],
|
||||
ovens: [],
|
||||
parts: [],
|
||||
maintenance: [],
|
||||
};
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const $$ = (selector) => Array.from(document.querySelectorAll(selector));
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
function toast(message, type = 'success') {
|
||||
const region = $('#toastRegion');
|
||||
const el = document.createElement('div');
|
||||
el.className = `app-toast ${type === 'error' ? 'error' : ''}`;
|
||||
el.textContent = message;
|
||||
region.appendChild(el);
|
||||
setTimeout(() => el.remove(), 3200);
|
||||
}
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
function setScreen(screenId) {
|
||||
$$('.screen').forEach((screen) => screen.classList.toggle('active', screen.id === screenId));
|
||||
const active = document.getElementById(screenId);
|
||||
$('#screenTitle').textContent = active?.dataset.title || 'ManutApp';
|
||||
$('#logoutBtn').classList.toggle('d-none', screenId === 'loginScreen');
|
||||
if (screenId === 'maintenanceScreen') renderMaintenance();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
async function api(action, options = {}) {
|
||||
const response = await fetch(`${apiBase}?action=${encodeURIComponent(action)}`, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
...options,
|
||||
});
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok || payload.success === false) throw new Error(payload.error || 'Falha na API mock.');
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
const payload = await api('bootstrap');
|
||||
state.stores = payload.stores || [];
|
||||
state.ovens = payload.ovens || [];
|
||||
state.parts = payload.parts || [];
|
||||
state.maintenance = payload.maintenance || [];
|
||||
renderSeedLists();
|
||||
renderFormOptions();
|
||||
renderMaintenance();
|
||||
} catch (error) {
|
||||
toast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeedLists() {
|
||||
$('#storesList').innerHTML = state.stores.map((store) => `
|
||||
<article class="list-item" tabindex="0"><div class="list-item-row"><strong>${escapeHtml(store.name)}</strong><span class="badge text-bg-light">${escapeHtml(store.id)}</span></div><small>${escapeHtml(store.city)}</small></article>
|
||||
`).join('');
|
||||
|
||||
$('#partsList').innerHTML = state.parts.map((part) => `
|
||||
<article class="list-item" tabindex="0"><div class="list-item-row"><strong>${escapeHtml(part.name)}</strong><span class="badge text-bg-light">${escapeHtml(part.sku)}</span></div><small>Peça mock para seleção em manutenção</small></article>
|
||||
`).join('');
|
||||
|
||||
$('#ovensList').innerHTML = state.ovens.map((oven) => {
|
||||
const store = state.stores.find((item) => item.id === oven.store_id);
|
||||
return `<article class="list-item" tabindex="0"><div class="list-item-row"><strong>${escapeHtml(oven.name)}</strong><span class="badge text-bg-light">${escapeHtml(oven.model)}</span></div><small>${escapeHtml(store?.name || 'Loja')}</small></article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderFormOptions() {
|
||||
$('#storeSelect').innerHTML = '<option value="">Selecione uma loja</option>' + state.stores.map((store) => `<option value="${escapeAttr(store.id)}">${escapeHtml(store.name)}</option>`).join('');
|
||||
renderOvenOptions();
|
||||
$('#partsChoices').innerHTML = state.parts.map((part) => `
|
||||
<label class="part-choice">
|
||||
<input type="checkbox" value="${escapeAttr(part.id)}">
|
||||
<span><strong>${escapeHtml(part.name)}</strong><br><small>${escapeHtml(part.sku)}</small></span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderOvenOptions() {
|
||||
const storeId = $('#storeSelect').value;
|
||||
const ovens = state.ovens.filter((oven) => oven.store_id === storeId);
|
||||
$('#ovenSelect').innerHTML = '<option value="">Selecione um forno</option>' + ovens.map((oven) => `<option value="${escapeAttr(oven.id)}">${escapeHtml(oven.name)} · ${escapeHtml(oven.model)}</option>`).join('');
|
||||
}
|
||||
|
||||
function renderMaintenance() {
|
||||
$('#maintenanceCount').textContent = state.maintenance.length === 1 ? '1 manutenção' : `${state.maintenance.length} manutenções`;
|
||||
$('#maintenanceEmpty').classList.toggle('d-none', state.maintenance.length > 0);
|
||||
$('#maintenanceList').innerHTML = state.maintenance.map((item) => `
|
||||
<button class="list-item" type="button" data-detail-id="${escapeAttr(String(item.id))}">
|
||||
<div class="list-item-row">
|
||||
<strong>#${escapeHtml(String(item.id))} · ${escapeHtml(item.store_name)}</strong>
|
||||
<span class="badge text-bg-light">${escapeHtml(item.status || 'Concluída')}</span>
|
||||
</div>
|
||||
<small>${escapeHtml(item.oven_name)} · ${escapeHtml(item.service_type)} · ${formatDate(item.created_at)}</small>
|
||||
</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderDetail(id) {
|
||||
const item = state.maintenance.find((entry) => String(entry.id) === String(id));
|
||||
if (!item) {
|
||||
toast('Registro não encontrado.', 'error');
|
||||
return;
|
||||
}
|
||||
const parts = Array.isArray(item.parts) ? item.parts.map((part) => part.name).join(', ') : 'Não informado';
|
||||
$('#detailCard').innerHTML = `
|
||||
<p class="label">Registro #${escapeHtml(String(item.id))}</p>
|
||||
<h2>${escapeHtml(item.store_name)}</h2>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-row"><span>Forno</span><strong>${escapeHtml(item.oven_name)}</strong></div>
|
||||
<div class="detail-row"><span>Técnico</span><strong>${escapeHtml(item.technician)}</strong></div>
|
||||
<div class="detail-row"><span>Tipo</span><strong>${escapeHtml(item.service_type)}</strong></div>
|
||||
<div class="detail-row"><span>Peças</span><strong>${escapeHtml(parts)}</strong></div>
|
||||
<div class="detail-row"><span>Observações</span><p class="mb-0">${escapeHtml(item.notes || 'Sem observações.')}</p></div>
|
||||
<div class="detail-row"><span>Criado em</span><strong>${formatDate(item.created_at)}</strong></div>
|
||||
</div>
|
||||
`;
|
||||
setScreen('detailScreen');
|
||||
}
|
||||
|
||||
function resetMaintenanceForm() {
|
||||
$('#maintenanceForm').reset();
|
||||
renderOvenOptions();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char]));
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/`/g, '`');
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return 'agora';
|
||||
const normalized = String(value).replace(' ', 'T');
|
||||
const date = new Date(normalized);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
||||
}
|
||||
|
||||
$('#loginForm').addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const username = $('#username').value.trim();
|
||||
const password = $('#password').value.trim();
|
||||
if (!username || !password) {
|
||||
toast('Informe usuário e senha para continuar.', 'error');
|
||||
return;
|
||||
}
|
||||
state.user = username;
|
||||
$('#welcomeUser').textContent = `Olá, ${username}`;
|
||||
toast('Login fake realizado com sucesso.');
|
||||
setScreen('homeScreen');
|
||||
});
|
||||
|
||||
$('#logoutBtn').addEventListener('click', () => {
|
||||
state.user = null;
|
||||
$('#loginForm').reset();
|
||||
setScreen('loginScreen');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const nav = event.target.closest('[data-target]');
|
||||
if (nav) setScreen(nav.dataset.target);
|
||||
const detail = event.target.closest('[data-detail-id]');
|
||||
if (detail) renderDetail(detail.dataset.detailId);
|
||||
});
|
||||
|
||||
$('#storeSelect').addEventListener('change', renderOvenOptions);
|
||||
|
||||
$('#maintenanceForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const selectedParts = $$('#partsChoices input:checked').map((input) => input.value);
|
||||
const payload = {
|
||||
store_id: $('#storeSelect').value,
|
||||
oven_id: $('#ovenSelect').value,
|
||||
technician: $('#technicianInput').value.trim(),
|
||||
service_type: $('#serviceTypeSelect').value,
|
||||
parts: selectedParts,
|
||||
notes: $('#notesInput').value.trim(),
|
||||
};
|
||||
if (!payload.store_id || !payload.oven_id || !payload.technician || selectedParts.length === 0) {
|
||||
toast('Preencha loja, forno, técnico e pelo menos uma peça.', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await api('maintenance', { method: 'POST', body: JSON.stringify(payload) });
|
||||
state.maintenance = result.maintenance || [];
|
||||
resetMaintenanceForm();
|
||||
toast('Manutenção salva e listada com sucesso.');
|
||||
setScreen('maintenanceScreen');
|
||||
} catch (error) {
|
||||
toast(error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
bootstrap();
|
||||
})();
|
||||
|
||||
282
index.php
282
index.php
@ -1,150 +1,166 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'ManutApp';
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Protótipo mobile para gestão de lojas, fornos, peças, ordens de serviço e manutenções.';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($projectName) ?> — App de Manutenção</title>
|
||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<meta property="og:title" content="<?= htmlspecialchars($projectName) ?> — App de Manutenção">
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<meta property="twitter:title" content="<?= htmlspecialchars($projectName) ?> — App de Manutenção">
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=2026070102">
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
<a class="skip-link" href="#app-main">Pular para o conteúdo</a>
|
||||
<main id="app-main" class="app-shell" data-api-base="api/mock.php">
|
||||
<section class="phone-frame" aria-label="Protótipo do aplicativo mobile">
|
||||
<header class="app-topbar">
|
||||
<div>
|
||||
<span class="eyebrow">MVP Mobile</span>
|
||||
<h1 id="screenTitle">ManutApp</h1>
|
||||
</div>
|
||||
<button id="logoutBtn" class="btn btn-sm btn-outline-dark d-none" type="button">Sair</button>
|
||||
</header>
|
||||
|
||||
<div id="toastRegion" class="toast-region" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<section id="loginScreen" class="screen active" data-title="Login">
|
||||
<div class="hero-card">
|
||||
<p class="label">Equipe técnica</p>
|
||||
<h2>Entre para gerenciar manutenções.</h2>
|
||||
<p class="muted">Use qualquer usuário e senha para testar o fluxo com chamadas fake de API.</p>
|
||||
</div>
|
||||
<form id="loginForm" class="stack" novalidate>
|
||||
<div>
|
||||
<label class="form-label" for="username">Usuário</label>
|
||||
<input class="form-control" id="username" name="username" autocomplete="username" placeholder="ex: tecnico01" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="password">Senha</label>
|
||||
<input class="form-control" id="password" name="password" type="password" autocomplete="current-password" placeholder="••••••" required>
|
||||
</div>
|
||||
<button class="btn btn-dark w-100" type="submit">Entrar</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="homeScreen" class="screen" data-title="Home">
|
||||
<div class="status-card">
|
||||
<div>
|
||||
<p class="label">Operação</p>
|
||||
<strong id="welcomeUser">Bem-vindo</strong>
|
||||
</div>
|
||||
<span class="badge text-bg-light" id="maintenanceCount">0 manutenções</span>
|
||||
</div>
|
||||
<div class="module-grid" aria-label="Módulos do aplicativo">
|
||||
<button class="module-card" data-target="storesScreen" type="button"><span>▦</span><strong>Lojas</strong><small>Lista mock</small></button>
|
||||
<button class="module-card" data-target="ordersScreen" type="button"><span>□</span><strong>OS</strong><small>Ordens</small></button>
|
||||
<button class="module-card" data-target="partsScreen" type="button"><span>◌</span><strong>Peças</strong><small>Estoque</small></button>
|
||||
<button class="module-card" data-target="ovensScreen" type="button"><span>▤</span><strong>Fornos</strong><small>Por loja</small></button>
|
||||
<button class="module-card featured" data-target="maintenanceScreen" type="button"><span>✓</span><strong>Manutenção</strong><small>Criar e listar</small></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="maintenanceScreen" class="screen" data-title="Manutenções">
|
||||
<div class="screen-actions">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-target="homeScreen" type="button">Voltar</button>
|
||||
<button class="btn btn-sm btn-dark" data-target="newMaintenanceScreen" type="button">Nova manutenção</button>
|
||||
</div>
|
||||
<div id="maintenanceList" class="list-stack"></div>
|
||||
<div id="maintenanceEmpty" class="empty-state d-none">
|
||||
<strong>Nenhuma manutenção registrada.</strong>
|
||||
<p>Crie a primeira manutenção escolhendo loja, forno e peças utilizadas.</p>
|
||||
<button class="btn btn-dark btn-sm" data-target="newMaintenanceScreen" type="button">Criar agora</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="newMaintenanceScreen" class="screen" data-title="Nova manutenção">
|
||||
<div class="screen-actions">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-target="maintenanceScreen" type="button">Voltar</button>
|
||||
</div>
|
||||
<form id="maintenanceForm" class="stack" novalidate>
|
||||
<div>
|
||||
<label class="form-label" for="storeSelect">Loja</label>
|
||||
<select class="form-select" id="storeSelect" required></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="ovenSelect">Forno</label>
|
||||
<select class="form-select" id="ovenSelect" required></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="technicianInput">Técnico responsável</label>
|
||||
<input class="form-control" id="technicianInput" placeholder="Nome do técnico" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="serviceTypeSelect">Tipo de serviço</label>
|
||||
<select class="form-select" id="serviceTypeSelect" required>
|
||||
<option value="Preventiva">Preventiva</option>
|
||||
<option value="Corretiva">Corretiva</option>
|
||||
<option value="Inspeção">Inspeção</option>
|
||||
</select>
|
||||
</div>
|
||||
<fieldset class="parts-box">
|
||||
<legend>Peças utilizadas</legend>
|
||||
<div id="partsChoices" class="parts-list"></div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<label class="form-label" for="notesInput">Observações</label>
|
||||
<textarea class="form-control" id="notesInput" rows="3" maxlength="500" placeholder="Resumo do serviço executado"></textarea>
|
||||
</div>
|
||||
<button class="btn btn-dark w-100" type="submit">Salvar manutenção</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="detailScreen" class="screen" data-title="Detalhe da manutenção">
|
||||
<div class="screen-actions">
|
||||
<button class="btn btn-sm btn-outline-secondary" data-target="maintenanceScreen" type="button">Voltar</button>
|
||||
</div>
|
||||
<article id="detailCard" class="detail-card"></article>
|
||||
</section>
|
||||
|
||||
<section id="storesScreen" class="screen" data-title="Lojas">
|
||||
<div class="screen-actions"><button class="btn btn-sm btn-outline-secondary" data-target="homeScreen" type="button">Voltar</button><button class="btn btn-sm btn-light" disabled>Nova loja em breve</button></div>
|
||||
<div id="storesList" class="list-stack"></div>
|
||||
</section>
|
||||
|
||||
<section id="partsScreen" class="screen" data-title="Peças">
|
||||
<div class="screen-actions"><button class="btn btn-sm btn-outline-secondary" data-target="homeScreen" type="button">Voltar</button><button class="btn btn-sm btn-light" disabled>Nova peça em breve</button></div>
|
||||
<div id="partsList" class="list-stack"></div>
|
||||
</section>
|
||||
|
||||
<section id="ovensScreen" class="screen" data-title="Fornos">
|
||||
<div class="screen-actions"><button class="btn btn-sm btn-outline-secondary" data-target="homeScreen" type="button">Voltar</button><button class="btn btn-sm btn-light" disabled>Novo forno em breve</button></div>
|
||||
<div id="ovensList" class="list-stack"></div>
|
||||
</section>
|
||||
|
||||
<section id="ordersScreen" class="screen" data-title="Ordens de serviço">
|
||||
<div class="screen-actions"><button class="btn btn-sm btn-outline-secondary" data-target="homeScreen" type="button">Voltar</button><button class="btn btn-sm btn-light" disabled>Nova OS em breve</button></div>
|
||||
<div class="empty-state"><strong>OS mockadas</strong><p>O próximo passo é conectar ordens de serviço ao fluxo de manutenção.</p></div>
|
||||
</section>
|
||||
|
||||
<footer class="app-footer">
|
||||
<span>PHP <?= htmlspecialchars(PHP_VERSION) ?></span>
|
||||
<span>UTC <?= htmlspecialchars($now) ?></span>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
<script src="assets/js/main.js?v=2026070102" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
48
src/api/fakeApi.ts
Normal file
48
src/api/fakeApi.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { AuthUser, Maintenance, Oven, OvenPartLink, Part, PartLocation, ServiceOrder, Store } from '../types';
|
||||
|
||||
export const PART_LOCATIONS: PartLocation[] = [
|
||||
{ descricao: 'Câmara de Cocção', ref: 'CC' },
|
||||
{ descricao: 'Painel de controle', ref: 'PCU' },
|
||||
{ descricao: 'Painel de Comandos', ref: 'PCE' },
|
||||
{ descricao: 'Gerador de Vapor', ref: 'GV' },
|
||||
{ descricao: 'Estrutura externa', ref: 'EE' },
|
||||
];
|
||||
|
||||
type Bootstrap = { stores: Store[]; ovens: Oven[]; parts: Part[]; serviceOrders: ServiceOrder[]; ovenPartLinks: OvenPartLink[]; maintenances: Maintenance[] };
|
||||
const users: Array<AuthUser & { senha: string }> = [
|
||||
{ id: 1, nome: 'Técnico Demo', usuario: 'tecnico', senha: '123456', perfil: 'tecnico' },
|
||||
{ id: 2, nome: 'Administrador', usuario: 'admin', senha: '123456', perfil: 'admin' },
|
||||
];
|
||||
let stores: Store[] = [
|
||||
{ id: 1, descricao: 'Loja Centro', endereco: 'Av. Central, 100', nomeContato: 'Marina Alves', numeroContato: '(11) 90000-1000' },
|
||||
{ id: 2, descricao: 'Loja Norte', endereco: 'Rua Norte, 245', nomeContato: 'Paulo Lima', numeroContato: '(11) 90000-2000' },
|
||||
];
|
||||
const ref = (loc: PartLocation, id: number) => `${loc.ref}00${id}`;
|
||||
const part = (id: number, descricao: string, localizacao: PartLocation): Part => ({ id, descricao, localizacao, referencia: ref(localizacao, id) });
|
||||
let parts: Part[] = [part(1, 'Resistência superior', PART_LOCATIONS[0]), part(2, 'Controlador eletrônico', PART_LOCATIONS[1]), part(3, 'Válvula de vapor', PART_LOCATIONS[3])];
|
||||
let ovens: Oven[] = [
|
||||
{ id: 1, lojaId: 1, descricao: 'Forno Turbo 01', modelo: 'FT-900', numeroSerie: 'FT900-001' },
|
||||
{ id: 2, lojaId: 1, descricao: 'Forno Lastro 02', modelo: 'FL-450', numeroSerie: 'FL450-002' },
|
||||
{ id: 3, lojaId: 2, descricao: 'Forno Compacto 01', modelo: 'FC-220', numeroSerie: 'FC220-003' },
|
||||
];
|
||||
let serviceOrders: ServiceOrder[] = [{ id: 1, lojaId: 1, fornoId: 1, titulo: 'Revisão preventiva', descricao: 'Checar aquecimento e vedação.', status: 'Aberta', criadoEm: new Date().toISOString() }];
|
||||
let ovenPartLinks: OvenPartLink[] = [{ fornoId: 1, pecaIds: [1, 2] }, { fornoId: 2, pecaIds: [1] }];
|
||||
let maintenances: Maintenance[] = [{ id: 1, lojaId: 1, fornoId: 1, pecaIds: [1, 2], tecnico: 'Técnico Demo', observacao: 'Troca preventiva e teste de funcionamento.', status: 'Concluída', criadoEm: new Date().toISOString() }];
|
||||
|
||||
const wait = () => new Promise((ok) => setTimeout(ok, 250));
|
||||
const clone = <T,>(data: T): T => JSON.parse(JSON.stringify(data));
|
||||
const next = (items: Array<{ id: number }>) => items.length ? Math.max(...items.map((i) => i.id)) + 1 : 1;
|
||||
const requireStore = (lojaId: number) => { if (!stores.some((s) => s.id === lojaId)) throw new Error('Loja inválida.'); };
|
||||
const requireOven = (fornoId: number, lojaId: number) => { if (!ovens.some((o) => o.id === fornoId && o.lojaId === lojaId)) throw new Error('Forno inválido para esta loja.'); };
|
||||
const requireParts = (ids: number[]) => { const valid = new Set(parts.map((p) => p.id)); if (!ids.length || ids.some((id) => !valid.has(id))) throw new Error('Selecione uma ou mais peças válidas.'); };
|
||||
|
||||
export const fakeApi = {
|
||||
async login(usuario: string, senha: string): Promise<AuthUser> { await wait(); const found = users.find((u) => u.usuario === usuario.trim() && u.senha === senha.trim()); if (!found) throw new Error('Usuário ou senha inválidos. Use tecnico/123456.'); const safe: AuthUser = { id: found.id, nome: found.nome, usuario: found.usuario, perfil: found.perfil }; return clone(safe); },
|
||||
async bootstrap(): Promise<Bootstrap> { await wait(); return clone({ stores, ovens, parts, serviceOrders, ovenPartLinks, maintenances }); },
|
||||
async createStore(input: Omit<Store, 'id'>) { await wait(); if (!input.descricao.trim() || !input.endereco.trim()) throw new Error('Informe descrição e endereço.'); const item = { ...input, id: next(stores) }; stores = [...stores, item]; return clone(item); },
|
||||
async createPart(input: { descricao: string; localizacaoRef: PartLocation['ref'] }) { await wait(); const loc = PART_LOCATIONS.find((l) => l.ref === input.localizacaoRef); if (!loc || !input.descricao.trim()) throw new Error('Informe descrição e localização.'); const item = part(next(parts), input.descricao.trim(), loc); parts = [...parts, item]; return clone(item); },
|
||||
async createOven(input: Omit<Oven, 'id'>) { await wait(); requireStore(input.lojaId); if (!input.descricao.trim() || !input.modelo.trim()) throw new Error('Informe descrição e modelo.'); const item = { ...input, id: next(ovens) }; ovens = [...ovens, item]; return clone(item); },
|
||||
async createServiceOrder(input: Omit<ServiceOrder, 'id' | 'status' | 'criadoEm'>) { await wait(); requireStore(input.lojaId); requireOven(input.fornoId, input.lojaId); if (!input.titulo.trim()) throw new Error('Informe o título.'); const item: ServiceOrder = { ...input, id: next(serviceOrders), status: 'Aberta', criadoEm: new Date().toISOString() }; serviceOrders = [item, ...serviceOrders]; return clone(item); },
|
||||
async saveOvenParts(fornoId: number, pecaIds: number[]) { await wait(); if (!ovens.some((o) => o.id === fornoId)) throw new Error('Forno inválido.'); requireParts(pecaIds); const ids = Array.from(new Set(pecaIds)); ovenPartLinks = ovenPartLinks.filter((l) => l.fornoId !== fornoId).concat({ fornoId, pecaIds: ids }); return clone({ fornoId, pecaIds: ids }); },
|
||||
async createMaintenance(input: Omit<Maintenance, 'id' | 'status' | 'criadoEm'>) { await wait(); requireStore(input.lojaId); requireOven(input.fornoId, input.lojaId); requireParts(input.pecaIds); if (!input.tecnico.trim()) throw new Error('Informe o técnico.'); const item: Maintenance = { ...input, id: next(maintenances), status: 'Registrada', criadoEm: new Date().toISOString() }; maintenances = [item, ...maintenances]; return clone(item); },
|
||||
};
|
||||
5
src/assets/README.md
Normal file
5
src/assets/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Assets
|
||||
|
||||
Pasta para imagens, ícones, fontes e estilos compartilhados do app Expo.
|
||||
|
||||
- `styles/theme.ts`: tema visual do app.
|
||||
33
src/assets/styles/theme.ts
Normal file
33
src/assets/styles/theme.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const colors = {
|
||||
bg: '#F8FAFC',
|
||||
surface: '#FFFFFF',
|
||||
text: '#111827',
|
||||
muted: '#6B7280',
|
||||
border: '#E5E7EB',
|
||||
accent: '#2563EB',
|
||||
accentSoft: '#DBEAFE',
|
||||
danger: '#DC2626',
|
||||
};
|
||||
|
||||
export const s = StyleSheet.create({
|
||||
screen: { flex: 1, backgroundColor: colors.bg },
|
||||
content: { padding: 24, gap: 14 },
|
||||
title: { color: colors.text, fontSize: 28, fontWeight: '800', lineHeight: 34 },
|
||||
subtitle: { color: colors.muted, fontSize: 15, lineHeight: 22 },
|
||||
label: { color: colors.text, fontWeight: '800', marginBottom: 6 },
|
||||
input: { minHeight: 48, backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 14, paddingHorizontal: 14, color: colors.text },
|
||||
area: { minHeight: 96, paddingTop: 12, textAlignVertical: 'top' },
|
||||
row: { flexDirection: 'row', gap: 10, alignItems: 'center', justifyContent: 'space-between' },
|
||||
card: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border, borderRadius: 18, padding: 16, gap: 6 },
|
||||
selected: { borderColor: colors.accent, backgroundColor: colors.accentSoft },
|
||||
meta: { color: colors.accent, fontSize: 12, fontWeight: '800', textTransform: 'uppercase' },
|
||||
cardTitle: { color: colors.text, fontSize: 17, fontWeight: '800' },
|
||||
cardSub: { color: colors.muted, fontSize: 14, lineHeight: 20 },
|
||||
btn: { minHeight: 48, borderRadius: 14, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 16, backgroundColor: colors.text },
|
||||
btnSecondary: { backgroundColor: colors.surface, borderWidth: 1, borderColor: colors.border },
|
||||
btnText: { color: colors.surface, fontWeight: '800' },
|
||||
btnTextSecondary: { color: colors.text },
|
||||
empty: { padding: 20, alignItems: 'center', gap: 8 },
|
||||
});
|
||||
59
src/components/ui.tsx
Normal file
59
src/components/ui.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Pressable, ScrollView, Text, TextInput, TextInputProps, View } from 'react-native';
|
||||
import { s } from '../assets/styles/theme';
|
||||
|
||||
type ButtonProps = { title: string; onPress: () => void; secondary?: boolean; disabled?: boolean };
|
||||
export function Button({ title, onPress, secondary, disabled }: ButtonProps) {
|
||||
return (
|
||||
<Pressable disabled={disabled} onPress={onPress} style={[s.btn, secondary && s.btnSecondary, disabled && { opacity: 0.55 }]}>
|
||||
<Text style={[s.btnText, secondary && s.btnTextSecondary]}>{title}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
type CardProps = { title: string; subtitle?: string; meta?: string; onPress?: () => void; selected?: boolean; children?: ReactNode };
|
||||
export function Card({ title, subtitle, meta, onPress, selected, children }: CardProps) {
|
||||
const content = (
|
||||
<>
|
||||
{meta ? <Text style={s.meta}>{meta}</Text> : null}
|
||||
<Text style={s.cardTitle}>{title}</Text>
|
||||
{subtitle ? <Text style={s.cardSub}>{subtitle}</Text> : null}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
if (onPress) return <Pressable onPress={onPress} style={[s.card, selected && s.selected]}>{content}</Pressable>;
|
||||
return <View style={[s.card, selected && s.selected]}>{content}</View>;
|
||||
}
|
||||
|
||||
type InputProps = TextInputProps & { label: string; value: string; onChangeText: (value: string) => void };
|
||||
export function Input({ label, multiline, style, ...props }: InputProps) {
|
||||
return (
|
||||
<View>
|
||||
<Text style={s.label}>{label}</Text>
|
||||
<TextInput multiline={multiline} style={[s.input, multiline && s.area, style]} {...props} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function Screen({ children }: { children: ReactNode }) {
|
||||
return <ScrollView style={s.screen} contentContainerStyle={s.content} keyboardShouldPersistTaps="handled">{children}</ScrollView>;
|
||||
}
|
||||
|
||||
export function Header({ title, subtitle, back, action }: { title: string; subtitle?: string; back?: () => void; action?: ReactNode }) {
|
||||
return (
|
||||
<View style={{ gap: 12 }}>
|
||||
<View style={s.row}>
|
||||
{back ? <Button title="Voltar" onPress={back} secondary /> : <View />}
|
||||
{action}
|
||||
</View>
|
||||
<View>
|
||||
<Text style={s.title}>{title}</Text>
|
||||
{subtitle ? <Text style={s.subtitle}>{subtitle}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function Empty({ text }: { text: string }) {
|
||||
return <View style={s.empty}><Text style={s.subtitle}>{text}</Text></View>;
|
||||
}
|
||||
51
src/navigation/AppNavigator.tsx
Normal file
51
src/navigation/AppNavigator.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { SafeAreaView, StatusBar } from 'react-native';
|
||||
import { colors } from '../assets/styles/theme';
|
||||
import { AuthUser } from '../types';
|
||||
import { AppScreenProps, ROUTES, RouteName, RouteParams, RouteState } from './routes';
|
||||
import LoginScreen from '../pages/login';
|
||||
import HomeScreen from '../pages/home';
|
||||
import OrdemServicoScreen from '../pages/ordem-de-serviço';
|
||||
import NovaOrdemScreen from '../pages/ordem-de-serviço/nova-ordem';
|
||||
import LojasScreen from '../pages/lojas';
|
||||
import NovaLojaScreen from '../pages/lojas/nova-loja';
|
||||
import PecasScreen from '../pages/peças';
|
||||
import NovaPecaScreen from '../pages/peças/nova-peça';
|
||||
import FornosScreen from '../pages/fornos';
|
||||
import NovoFornoScreen from '../pages/fornos/novo-forno';
|
||||
import PecasFornoScreen from '../pages/peças-forno';
|
||||
import NovaPecaFornoScreen from '../pages/peças-forno/nova-peça';
|
||||
import ManutencaoScreen from '../pages/manutençao';
|
||||
import NovaManutencaoScreen from '../pages/manutençao/nova-manutenções';
|
||||
|
||||
const screens: Record<RouteName, React.ComponentType<AppScreenProps>> = {
|
||||
[ROUTES.LOGIN]: LoginScreen,
|
||||
[ROUTES.HOME]: HomeScreen,
|
||||
[ROUTES.ORDEM_SERVICO]: OrdemServicoScreen,
|
||||
[ROUTES.NOVA_ORDEM]: NovaOrdemScreen,
|
||||
[ROUTES.LOJAS]: LojasScreen,
|
||||
[ROUTES.NOVA_LOJA]: NovaLojaScreen,
|
||||
[ROUTES.PECAS]: PecasScreen,
|
||||
[ROUTES.NOVA_PECA]: NovaPecaScreen,
|
||||
[ROUTES.FORNOS]: FornosScreen,
|
||||
[ROUTES.NOVO_FORNO]: NovoFornoScreen,
|
||||
[ROUTES.PECAS_FORNO]: PecasFornoScreen,
|
||||
[ROUTES.NOVA_PECA_FORNO]: NovaPecaFornoScreen,
|
||||
[ROUTES.MANUTENCAO]: ManutencaoScreen,
|
||||
[ROUTES.NOVA_MANUTENCAO]: NovaManutencaoScreen,
|
||||
};
|
||||
|
||||
export default function AppNavigator() {
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [stack, setStack] = useState<RouteState[]>([{ name: ROUTES.LOGIN }]);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const route = stack[stack.length - 1];
|
||||
const navigate = useCallback((name: RouteName, params?: RouteParams) => setStack((old) => [...old, { name, params }]), []);
|
||||
const resetTo = useCallback((name: RouteName, params?: RouteParams) => setStack([{ name, params }]), []);
|
||||
const goBack = useCallback(() => setStack((old) => old.length > 1 ? old.slice(0, -1) : old), []);
|
||||
const bumpRefresh = useCallback(() => setRefreshKey((n) => n + 1), []);
|
||||
useEffect(() => { if (!user && route.name !== ROUTES.LOGIN) resetTo(ROUTES.LOGIN); }, [resetTo, route.name, user]);
|
||||
const Screen = screens[route.name];
|
||||
const props = useMemo(() => ({ route, user, setUser, navigate, resetTo, goBack, refreshKey, bumpRefresh }), [route, user, navigate, resetTo, goBack, refreshKey, bumpRefresh]);
|
||||
return <SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }}><StatusBar barStyle="dark-content" /><Screen {...props} /></SafeAreaView>;
|
||||
}
|
||||
33
src/navigation/routes.ts
Normal file
33
src/navigation/routes.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { AuthUser } from '../types';
|
||||
|
||||
export const ROUTES = {
|
||||
LOGIN: 'login',
|
||||
HOME: 'home',
|
||||
ORDEM_SERVICO: 'ordem-de-serviço',
|
||||
NOVA_ORDEM: 'ordem-de-serviço/nova-ordem',
|
||||
LOJAS: 'lojas',
|
||||
NOVA_LOJA: 'lojas/nova-loja',
|
||||
PECAS: 'peças',
|
||||
NOVA_PECA: 'peças/nova-peça',
|
||||
FORNOS: 'fornos',
|
||||
NOVO_FORNO: 'fornos/novo-forno',
|
||||
PECAS_FORNO: 'peças-forno',
|
||||
NOVA_PECA_FORNO: 'peças-forno/nova-peça',
|
||||
MANUTENCAO: 'manutençao',
|
||||
NOVA_MANUTENCAO: 'manutençao/nova-manutenções',
|
||||
} as const;
|
||||
|
||||
export type RouteName = (typeof ROUTES)[keyof typeof ROUTES];
|
||||
export type RouteParams = { fornoId?: number };
|
||||
export type RouteState = { name: RouteName; params?: RouteParams };
|
||||
|
||||
export type AppScreenProps = {
|
||||
route: RouteState;
|
||||
user: AuthUser | null;
|
||||
setUser: (user: AuthUser | null) => void;
|
||||
navigate: (name: RouteName, params?: RouteParams) => void;
|
||||
resetTo: (name: RouteName, params?: RouteParams) => void;
|
||||
goBack: () => void;
|
||||
refreshKey: number;
|
||||
bumpRefresh: () => void;
|
||||
};
|
||||
53
src/pages/_screens.tsx
Normal file
53
src/pages/_screens.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Text } from 'react-native';
|
||||
import { fakeApi, PART_LOCATIONS } from '../api/fakeApi';
|
||||
import { Button, Card, Empty, Header, Input, Screen } from '../components/ui';
|
||||
import { AppScreenProps, ROUTES } from '../navigation/routes';
|
||||
import { Maintenance, Oven, OvenPartLink, Part, PartLocation, ServiceOrder, Store } from '../types';
|
||||
|
||||
type Data = { stores: Store[]; ovens: Oven[]; parts: Part[]; serviceOrders: ServiceOrder[]; ovenPartLinks: OvenPartLink[]; maintenances: Maintenance[] };
|
||||
const blank: Data = { stores: [], ovens: [], parts: [], serviceOrders: [], ovenPartLinks: [], maintenances: [] };
|
||||
function useData(refreshKey: number) { const [data, setData] = useState<Data>(blank); useEffect(() => { fakeApi.bootstrap().then(setData); }, [refreshKey]); return data; }
|
||||
const names = (data: Data) => ({ store: (id: number) => data.stores.find((s) => s.id === id)?.descricao ?? 'Loja', oven: (id: number) => data.ovens.find((o) => o.id === id)?.descricao ?? 'Forno', parts: (ids: number[]) => ids.map((id) => data.parts.find((p) => p.id === id)?.descricao).filter(Boolean).join(', ') || 'Sem peças' });
|
||||
function toggle(list: number[], id: number) { return list.includes(id) ? list.filter((item) => item !== id) : [...list, id]; }
|
||||
|
||||
export function LoginScreen({ setUser, resetTo }: AppScreenProps) {
|
||||
const [usuario, setUsuario] = useState('tecnico'); const [senha, setSenha] = useState('123456'); const [loading, setLoading] = useState(false);
|
||||
async function submit() { try { setLoading(true); const user = await fakeApi.login(usuario, senha); setUser(user); resetTo(ROUTES.HOME); } catch (e) { Alert.alert('Login não autorizado', e instanceof Error ? e.message : 'Tente novamente.'); } finally { setLoading(false); } }
|
||||
return <Screen><Header title="Entrar" subtitle="Login validado pela API fake. Use tecnico/123456 ou admin/123456." /><Input label="Usuário" value={usuario} onChangeText={setUsuario} autoCapitalize="none" /><Input label="Senha" value={senha} onChangeText={setSenha} secureTextEntry /><Button title={loading ? 'Validando...' : 'Entrar'} onPress={submit} disabled={loading} /></Screen>;
|
||||
}
|
||||
|
||||
export function HomeScreen({ user, navigate, setUser, resetTo }: AppScreenProps) {
|
||||
const modules = [[ROUTES.LOJAS, 'Lojas', 'Lista e cadastro'], [ROUTES.ORDEM_SERVICO, 'Ordens de serviço', 'Lista e nova OS'], [ROUTES.PECAS, 'Peças', 'Catálogo e localização'], [ROUTES.FORNOS, 'Fornos', 'Vinculados a lojas'], [ROUTES.PECAS_FORNO, 'Peças por forno', 'Associação muitos-para-muitos'], [ROUTES.MANUTENCAO, 'Manutenção', 'Registro com peças usadas']] as const;
|
||||
return <Screen><Header title="Home" subtitle={`Bem-vindo, ${user?.nome ?? 'usuário'}.`} action={<Button title="Sair" secondary onPress={() => { setUser(null); resetTo(ROUTES.LOGIN); }} />} />{modules.map(([route, title, sub]) => <Card key={route} meta="Abrir" title={title} subtitle={sub} onPress={() => navigate(route)} />)}</Screen>;
|
||||
}
|
||||
|
||||
export function LojasScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); return <Screen><Header title="Lojas" subtitle="Lista de lojas cadastradas." back={goBack} action={<Button title="Nova loja" onPress={() => navigate(ROUTES.NOVA_LOJA)} />} />{data.stores.map((s) => <Card key={s.id} meta={`Loja #${s.id}`} title={s.descricao} subtitle={`${s.endereco}
|
||||
Contato: ${s.nomeContato} • ${s.numeroContato}`} />)}{!data.stores.length && <Empty text="Nenhuma loja." />}</Screen>; }
|
||||
|
||||
export function NovaLojaScreen({ goBack, bumpRefresh }: AppScreenProps) { const [descricao, setDescricao] = useState(''); const [endereco, setEndereco] = useState(''); const [nomeContato, setNomeContato] = useState(''); const [numeroContato, setNumeroContato] = useState(''); async function submit() { try { await fakeApi.createStore({ descricao, endereco, nomeContato, numeroContato }); bumpRefresh(); Alert.alert('Loja salva'); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Confira os dados.'); } } return <Screen><Header title="Nova loja" subtitle="Descrição, endereço e contato." back={goBack} /><Input label="Descrição" value={descricao} onChangeText={setDescricao} /><Input label="Endereço" value={endereco} onChangeText={setEndereco} /><Input label="Nome do contato" value={nomeContato} onChangeText={setNomeContato} /><Input label="Número do contato" value={numeroContato} onChangeText={setNumeroContato} keyboardType="phone-pad" /><Button title="Salvar loja" onPress={submit} /></Screen>; }
|
||||
|
||||
export function PecasScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); return <Screen><Header title="Peças" subtitle="Referência = localização.ref + 00 + id." back={goBack} action={<Button title="Nova peça" onPress={() => navigate(ROUTES.NOVA_PECA)} />} />{data.parts.map((p) => <Card key={p.id} meta={p.referencia} title={p.descricao} subtitle={`${p.localizacao.descricao} (${p.localizacao.ref})`} />)}</Screen>; }
|
||||
|
||||
export function NovaPecaScreen({ goBack, bumpRefresh }: AppScreenProps) { const [descricao, setDescricao] = useState(''); const [localizacaoRef, setLocalizacaoRef] = useState<PartLocation['ref']>('CC'); async function submit() { try { const p = await fakeApi.createPart({ descricao, localizacaoRef }); bumpRefresh(); Alert.alert('Peça salva', `Referência: ${p.referencia}`); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Confira os dados.'); } } return <Screen><Header title="Nova peça" subtitle="Escolha a localização para gerar a referência." back={goBack} /><Input label="Descrição" value={descricao} onChangeText={setDescricao} />{PART_LOCATIONS.map((l) => <Card key={l.ref} selected={l.ref === localizacaoRef} title={l.descricao} subtitle={`Ref: ${l.ref}`} onPress={() => setLocalizacaoRef(l.ref)} />)}<Button title="Salvar peça" onPress={submit} /></Screen>; }
|
||||
|
||||
export function FornosScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const n = names(data); return <Screen><Header title="Fornos" subtitle="Fornos vinculados a uma loja." back={goBack} action={<Button title="Novo forno" onPress={() => navigate(ROUTES.NOVO_FORNO)} />} />{data.ovens.map((o) => <Card key={o.id} meta={`Forno #${o.id}`} title={o.descricao} subtitle={`Loja: ${n.store(o.lojaId)}
|
||||
Modelo: ${o.modelo} • Série: ${o.numeroSerie}`} />)}</Screen>; }
|
||||
|
||||
export function NovoFornoScreen({ goBack, bumpRefresh, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const [lojaId, setLojaId] = useState<number | null>(null); const [descricao, setDescricao] = useState(''); const [modelo, setModelo] = useState(''); const [numeroSerie, setNumeroSerie] = useState(''); useEffect(() => { if (!lojaId && data.stores[0]) setLojaId(data.stores[0].id); }, [data.stores, lojaId]); async function submit() { try { if (!lojaId) throw new Error('Escolha a loja.'); await fakeApi.createOven({ lojaId, descricao, modelo, numeroSerie }); bumpRefresh(); Alert.alert('Forno salvo'); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Confira os dados.'); } } return <Screen><Header title="Novo forno" subtitle="Escolha a loja onde o forno será cadastrado." back={goBack} />{data.stores.map((s) => <Card key={s.id} selected={s.id === lojaId} title={s.descricao} subtitle={s.endereco} onPress={() => setLojaId(s.id)} />)}<Input label="Descrição" value={descricao} onChangeText={setDescricao} /><Input label="Modelo" value={modelo} onChangeText={setModelo} /><Input label="Número de série" value={numeroSerie} onChangeText={setNumeroSerie} /><Button title="Salvar forno" onPress={submit} /></Screen>; }
|
||||
|
||||
export function PecasFornoScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const n = names(data); const partNames = (fornoId: number) => n.parts(data.ovenPartLinks.find((l) => l.fornoId === fornoId)?.pecaIds ?? []); return <Screen><Header title="Peças por forno" subtitle="Clique no forno para associar uma ou várias peças." back={goBack} />{data.ovens.map((o) => <Card key={o.id} meta="Editar peças" title={o.descricao} subtitle={`Loja: ${n.store(o.lojaId)}
|
||||
Peças: ${partNames(o.id)}`} onPress={() => navigate(ROUTES.NOVA_PECA_FORNO, { fornoId: o.id })} />)}</Screen>; }
|
||||
|
||||
export function NovaPecaFornoScreen({ route, goBack, bumpRefresh, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const fornoId = route.params?.fornoId; const oven = data.ovens.find((o) => o.id === fornoId); const [selected, setSelected] = useState<number[]>([]); useEffect(() => { setSelected(data.ovenPartLinks.find((l) => l.fornoId === fornoId)?.pecaIds ?? []); }, [data.ovenPartLinks, fornoId]); async function submit() { try { if (!fornoId) throw new Error('Forno não informado.'); await fakeApi.saveOvenParts(fornoId, selected); bumpRefresh(); Alert.alert('Associação salva'); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Selecione ao menos uma peça.'); } } return <Screen><Header title="Adicionar peças" subtitle={oven?.descricao ?? 'Forno'} back={goBack} />{data.parts.map((p) => <Card key={p.id} selected={selected.includes(p.id)} meta={p.referencia} title={p.descricao} subtitle={p.localizacao.descricao} onPress={() => setSelected((old) => toggle(old, p.id))} />)}<Button title="Salvar peças do forno" onPress={submit} /></Screen>; }
|
||||
|
||||
export function OrdemServicoScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const n = names(data); return <Screen><Header title="Ordens de serviço" subtitle="Lista de OS e formulário de nova ordem." back={goBack} action={<Button title="Nova OS" onPress={() => navigate(ROUTES.NOVA_ORDEM)} />} />{data.serviceOrders.map((o) => <Card key={o.id} meta={o.status} title={o.titulo} subtitle={`${n.store(o.lojaId)} • ${n.oven(o.fornoId)}
|
||||
${o.descricao}`} />)}</Screen>; }
|
||||
|
||||
export function NovaOrdemScreen({ goBack, bumpRefresh, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const [lojaId, setLojaId] = useState<number | null>(null); const [fornoId, setFornoId] = useState<number | null>(null); const [titulo, setTitulo] = useState(''); const [descricao, setDescricao] = useState(''); useEffect(() => { if (!lojaId && data.stores[0]) setLojaId(data.stores[0].id); }, [data.stores, lojaId]); const ovens = useMemo(() => data.ovens.filter((o) => o.lojaId === lojaId), [data.ovens, lojaId]); useEffect(() => { setFornoId(ovens[0]?.id ?? null); }, [ovens]); async function submit() { try { if (!lojaId || !fornoId) throw new Error('Escolha loja e forno.'); await fakeApi.createServiceOrder({ lojaId, fornoId, titulo, descricao }); bumpRefresh(); Alert.alert('OS criada'); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Confira os dados.'); } } return <Screen><Header title="Nova ordem" subtitle="Escolha loja e forno." back={goBack} />{data.stores.map((s) => <Card key={s.id} selected={s.id === lojaId} title={s.descricao} subtitle={s.endereco} onPress={() => setLojaId(s.id)} />)}<Text>Forno</Text>{ovens.map((o) => <Card key={o.id} selected={o.id === fornoId} title={o.descricao} subtitle={o.modelo} onPress={() => setFornoId(o.id)} />)}<Input label="Título" value={titulo} onChangeText={setTitulo} /><Input label="Descrição" value={descricao} onChangeText={setDescricao} multiline /><Button title="Criar OS" onPress={submit} /></Screen>; }
|
||||
|
||||
export function ManutencaoScreen({ navigate, goBack, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const n = names(data); return <Screen><Header title="Manutenções" subtitle="Histórico com loja, forno e peças usadas." back={goBack} action={<Button title="Nova manutenção" onPress={() => navigate(ROUTES.NOVA_MANUTENCAO)} />} />{data.maintenances.map((m) => <Card key={m.id} meta={m.status} title={`${n.store(m.lojaId)} • ${n.oven(m.fornoId)}`} subtitle={`Técnico: ${m.tecnico}
|
||||
Peças: ${n.parts(m.pecaIds)}
|
||||
${m.observacao}`} />)}</Screen>; }
|
||||
|
||||
export function NovaManutencaoScreen({ user, goBack, bumpRefresh, refreshKey }: AppScreenProps) { const data = useData(refreshKey); const [lojaId, setLojaId] = useState<number | null>(null); const [fornoId, setFornoId] = useState<number | null>(null); const [pecaIds, setPecaIds] = useState<number[]>([]); const [tecnico, setTecnico] = useState(user?.nome ?? ''); const [observacao, setObservacao] = useState(''); useEffect(() => { if (!lojaId && data.stores[0]) setLojaId(data.stores[0].id); }, [data.stores, lojaId]); const ovens = useMemo(() => data.ovens.filter((o) => o.lojaId === lojaId), [data.ovens, lojaId]); useEffect(() => { setFornoId(ovens[0]?.id ?? null); }, [ovens]); async function submit() { try { if (!lojaId || !fornoId) throw new Error('Escolha loja e forno.'); await fakeApi.createMaintenance({ lojaId, fornoId, pecaIds, tecnico, observacao }); bumpRefresh(); Alert.alert('Manutenção registrada'); goBack(); } catch (e) { Alert.alert('Erro', e instanceof Error ? e.message : 'Confira os dados.'); } } return <Screen><Header title="Nova manutenção" subtitle="Escolha loja, forno e peças usadas." back={goBack} />{data.stores.map((s) => <Card key={s.id} selected={s.id === lojaId} title={s.descricao} subtitle={s.endereco} onPress={() => setLojaId(s.id)} />)}<Text>Forno</Text>{ovens.map((o) => <Card key={o.id} selected={o.id === fornoId} title={o.descricao} subtitle={o.modelo} onPress={() => setFornoId(o.id)} />)}<Text>Peças usadas</Text>{data.parts.map((p) => <Card key={p.id} selected={pecaIds.includes(p.id)} meta={p.referencia} title={p.descricao} subtitle={p.localizacao.descricao} onPress={() => setPecaIds((old) => toggle(old, p.id))} />)}<Input label="Técnico" value={tecnico} onChangeText={setTecnico} /><Input label="Observação" value={observacao} onChangeText={setObservacao} multiline /><Button title="Registrar manutenção" onPress={submit} /></Screen>; }
|
||||
1
src/pages/fornos/index.tsx
Normal file
1
src/pages/fornos/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { FornosScreen as default } from '../_screens';
|
||||
1
src/pages/fornos/novo-forno/index.tsx
Normal file
1
src/pages/fornos/novo-forno/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovoFornoScreen as default } from '../../_screens';
|
||||
1
src/pages/home/index.tsx
Normal file
1
src/pages/home/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { HomeScreen as default } from '../_screens';
|
||||
1
src/pages/login/index.tsx
Normal file
1
src/pages/login/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { LoginScreen as default } from '../_screens';
|
||||
1
src/pages/lojas/index.tsx
Normal file
1
src/pages/lojas/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { LojasScreen as default } from '../_screens';
|
||||
1
src/pages/lojas/nova-loja/index.tsx
Normal file
1
src/pages/lojas/nova-loja/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovaLojaScreen as default } from '../../_screens';
|
||||
1
src/pages/manutençao/index.tsx
Normal file
1
src/pages/manutençao/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { ManutencaoScreen as default } from '../_screens';
|
||||
1
src/pages/manutençao/nova-manutenções/index.tsx
Normal file
1
src/pages/manutençao/nova-manutenções/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovaManutencaoScreen as default } from '../../_screens';
|
||||
1
src/pages/ordem-de-serviço/index.tsx
Normal file
1
src/pages/ordem-de-serviço/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { OrdemServicoScreen as default } from '../_screens';
|
||||
1
src/pages/ordem-de-serviço/nova-ordem/index.tsx
Normal file
1
src/pages/ordem-de-serviço/nova-ordem/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovaOrdemScreen as default } from '../../_screens';
|
||||
1
src/pages/peças-forno/index.tsx
Normal file
1
src/pages/peças-forno/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { PecasFornoScreen as default } from '../_screens';
|
||||
1
src/pages/peças-forno/nova-peça/index.tsx
Normal file
1
src/pages/peças-forno/nova-peça/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovaPecaFornoScreen as default } from '../../_screens';
|
||||
1
src/pages/peças/index.tsx
Normal file
1
src/pages/peças/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { PecasScreen as default } from '../_screens';
|
||||
1
src/pages/peças/nova-peça/index.tsx
Normal file
1
src/pages/peças/nova-peça/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NovaPecaScreen as default } from '../../_screens';
|
||||
49
src/types/index.ts
Normal file
49
src/types/index.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export type AuthUser = { id: number; nome: string; usuario: string; perfil: 'admin' | 'tecnico' };
|
||||
|
||||
export type Store = {
|
||||
id: number;
|
||||
descricao: string;
|
||||
endereco: string;
|
||||
nomeContato: string;
|
||||
numeroContato: string;
|
||||
};
|
||||
|
||||
export type PartLocation = { descricao: string; ref: 'CC' | 'PCU' | 'PCE' | 'GV' | 'EE' };
|
||||
|
||||
export type Part = {
|
||||
id: number;
|
||||
descricao: string;
|
||||
localizacao: PartLocation;
|
||||
referencia: string;
|
||||
};
|
||||
|
||||
export type Oven = {
|
||||
id: number;
|
||||
lojaId: number;
|
||||
descricao: string;
|
||||
modelo: string;
|
||||
numeroSerie: string;
|
||||
};
|
||||
|
||||
export type ServiceOrder = {
|
||||
id: number;
|
||||
lojaId: number;
|
||||
fornoId: number;
|
||||
titulo: string;
|
||||
descricao: string;
|
||||
status: 'Aberta' | 'Em andamento' | 'Concluída';
|
||||
criadoEm: string;
|
||||
};
|
||||
|
||||
export type OvenPartLink = { fornoId: number; pecaIds: number[] };
|
||||
|
||||
export type Maintenance = {
|
||||
id: number;
|
||||
lojaId: number;
|
||||
fornoId: number;
|
||||
pecaIds: number[];
|
||||
tecnico: string;
|
||||
observacao: string;
|
||||
status: 'Registrada' | 'Concluída';
|
||||
criadoEm: string;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user