diff --git a/README.md b/README.md index f2e32b4..645e828 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# codex -Repositorio codex open ai +# Audit360 Hotéis + +Aplicativo web em React + TypeScript para auditorias de hotéis. O projeto foi criado manualmente com Vite e utiliza um backend simulado via `localStorage`. + +## Scripts + +- `npm run dev` inicia o servidor de desenvolvimento (requer dependências instaladas). +- `npm run build` gera a versão de produção. + +## Funcionalidades + +- Autenticação simples de usuário (Supervisor, Gestora e Diretor). +- Dashboard com listagem de auditorias e botão para iniciar novas auditorias. +- Formulários de checklist com opções "Conforme", "Não conforme leve", "Não conforme grave" e upload de fotos. +- Relatórios filtrados por unidade, data e responsável. + +As informações são salvas no `localStorage` do navegador e as imagens ficam codificadas em base64. diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..f0adfba --- /dev/null +++ b/favicon.svg @@ -0,0 +1,5 @@ + + + + 360 + diff --git a/index.html b/index.html new file mode 100644 index 0000000..2f9f924 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Audit360 Hotéis + + +
+ + + diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..f0adfba --- /dev/null +++ b/logo.svg @@ -0,0 +1,5 @@ + + + + 360 + diff --git a/package.json b/package.json new file mode 100644 index 0000000..69ba02d --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "audit360-hoteis", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1" + }, + "devDependencies": { + "@types/react": "^18.2.14", + "@types/react-dom": "^18.2.7", + "@types/react-router-dom": "^5.3.3", + "typescript": "^5.4.5", + "vite": "^5.2.0", + "@vitejs/plugin-react": "^4.0.3" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..a0b49e5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,36 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import Login from './pages/Login'; +import Dashboard from './pages/Dashboard'; +import AuditForm from './pages/AuditForm'; +import Reports from './pages/Reports'; +import Header from './components/Header'; +import { useAuth } from './auth'; + +function PrivateRoute({ children }: { children: JSX.Element }) { + const { user } = useAuth(); + return user ? children : ; +} + +export default function App() { + const { user } = useAuth(); + return ( + <> + {user &&
} + + } /> + } + /> + } + /> + } + /> + + + ); +} diff --git a/src/auth.tsx b/src/auth.tsx new file mode 100644 index 0000000..b80a014 --- /dev/null +++ b/src/auth.tsx @@ -0,0 +1,73 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; + +export type Role = 'Supervisor' | 'Gestora' | 'Diretor'; + +export interface User { + name: string; + email: string; + role: Role; +} + +interface AuthContextValue { + user: User | null; + login: (email: string, password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(null!); + +const fakeUsers: Record = { + 'supervisor@example.com': { + name: 'Supervisor', + email: 'supervisor@example.com', + role: 'Supervisor', + password: '1234' + }, + 'gestora@example.com': { + name: 'Gestora', + email: 'gestora@example.com', + role: 'Gestora', + password: '1234' + }, + 'diretor@example.com': { + name: 'Diretor', + email: 'diretor@example.com', + role: 'Diretor', + password: '1234' + } +}; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + + useEffect(() => { + const stored = localStorage.getItem('audit360_user'); + if (stored) setUser(JSON.parse(stored)); + }, []); + + const login = async (email: string, password: string) => { + const u = fakeUsers[email]; + if (u && u.password === password) { + const { password: _, ...info } = u; + setUser(info); + localStorage.setItem('audit360_user', JSON.stringify(info)); + return true; + } + return false; + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('audit360_user'); + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + return useContext(AuthContext); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..18b8de5 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; +import { useAuth } from '../auth'; + +export default function Header() { + const { user, logout } = useAuth(); + return ( +
+

Audit360 Hotéis

+ +
+ {user && ( + <> + {user.name} - {user.email} + + + )} +
+
+ ); +} diff --git a/src/data/questions.ts b/src/data/questions.ts new file mode 100644 index 0000000..9835c9a --- /dev/null +++ b/src/data/questions.ts @@ -0,0 +1,19 @@ +export interface Question { + id: string; + text: string; +} + +export const limpeza: Question[] = [ + { id: 'l1', text: 'Quartos limpos' }, + { id: 'l2', text: 'Áreas comuns higienizadas' } +]; + +export const operacao: Question[] = [ + { id: 'o1', text: 'Atendimento cordial' }, + { id: 'o2', text: 'Tempo de espera adequado' } +]; + +export const manutencao: Question[] = [ + { id: 'm1', text: 'Equipamentos funcionando' }, + { id: 'm2', text: 'Estrutura sem danos' } +]; diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..4f316f6 --- /dev/null +++ b/src/index.css @@ -0,0 +1,62 @@ +body { + margin: 0; + font-family: Arial, Helvetica, sans-serif; + background-color: #f6f7f8; + color: #333; +} + +* { + box-sizing: border-box; +} + +header { + background: #003366; + color: white; + padding: 0.5rem 1rem; + display: flex; + align-items: center; + justify-content: space-between; +} + +header h1 { + font-size: 1.2rem; +} + +button { + cursor: pointer; +} + +nav a { + color: white; + margin-right: 1rem; + text-decoration: none; +} + +.container { + padding: 1rem; +} + +.audit-item { + padding: 0.5rem; + border-bottom: 1px solid #ccc; +} + +form { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +label { + font-weight: bold; +} + +@media (min-width: 600px) { + header h1 { + font-size: 1.5rem; + } + .container { + max-width: 800px; + margin: 0 auto; + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..f7c043d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; +import { AuthProvider } from './auth'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + +); diff --git a/src/pages/AuditForm.tsx b/src/pages/AuditForm.tsx new file mode 100644 index 0000000..a5a8689 --- /dev/null +++ b/src/pages/AuditForm.tsx @@ -0,0 +1,70 @@ +import { FormEvent, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { limpeza, operacao, manutencao, Question } from '../data/questions'; +import { Audit } from '../types'; +import { useAuth } from '../auth'; + +const options = ['Conforme', 'Não conforme leve', 'Não conforme grave'] as const; + +export default function AuditForm() { + const { type } = useParams(); + const navigate = useNavigate(); + const { user } = useAuth(); + const questions: Question[] = + type === 'limpeza' ? limpeza : type === 'operacao' ? operacao : manutencao; + + const [answers, setAnswers] = useState>({}); + const [photos, setPhotos] = useState>({}); + + const handleFile = (q: string, files: FileList | null) => { + if (files && files[0]) { + const reader = new FileReader(); + reader.onload = () => { + setPhotos((p) => ({ ...p, [q]: reader.result as string })); + }; + reader.readAsDataURL(files[0]); + } + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const stored = localStorage.getItem('audits'); + const audits: Audit[] = stored ? JSON.parse(stored) : []; + const audit: Audit = { + type: type || '', + date: new Date().toISOString().substring(0, 10), + responsible: user?.name || '', + answers: questions.map((q) => ({ + questionId: q.id, + status: (answers[q.id] as any) || 'Conforme', + photo: photos[q.id] + })) + }; + audits.push(audit); + localStorage.setItem('audits', JSON.stringify(audits)); + navigate('/'); + }; + + return ( +
+

Auditoria {type}

+
+ {questions.map((q) => ( +
+ + + handleFile(q.id, e.target.files)} /> +
+ ))} + +
+
+ ); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx new file mode 100644 index 0000000..5e8bc9c --- /dev/null +++ b/src/pages/Dashboard.tsx @@ -0,0 +1,31 @@ +import { Link } from 'react-router-dom'; +import { useEffect, useState } from 'react'; +import { Audit } from '../types'; + +export default function Dashboard() { + const [audits, setAudits] = useState([]); + + useEffect(() => { + const stored = localStorage.getItem('audits'); + if (stored) setAudits(JSON.parse(stored)); + }, []); + + return ( +
+

Auditorias

+
+ Nova Auditoria Limpeza + {' | '} + Nova Auditoria Operação + {' | '} + Nova Auditoria Manutenção +
+ {audits.map((a, i) => ( +
+ {a.type} - {a.date} - {a.responsible} +
+ ))} + {audits.length === 0 &&

Nenhuma auditoria registrada.

} +
+ ); +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..c6eebd7 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../auth'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const ok = await login(email, password); + if (ok) { + navigate('/'); + } else { + setError('Credenciais inválidas'); + } + }; + + return ( +
+

Login

+
+ + setEmail(e.target.value)} + required + /> + + setPassword(e.target.value)} + required + /> + {error && {error}} + +
+
+ ); +} diff --git a/src/pages/Reports.tsx b/src/pages/Reports.tsx new file mode 100644 index 0000000..aab2a5c --- /dev/null +++ b/src/pages/Reports.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { Audit } from '../types'; + +export default function Reports() { + const [audits, setAudits] = useState([]); + const [unit, setUnit] = useState(''); + const [responsible, setResponsible] = useState(''); + const [date, setDate] = useState(''); + + useEffect(() => { + const stored = localStorage.getItem('audits'); + if (stored) setAudits(JSON.parse(stored)); + }, []); + + const filtered = audits.filter((a) => { + return ( + (unit ? a.type === unit : true) && + (responsible ? a.responsible.includes(responsible) : true) && + (date ? a.date === date : true) + ); + }); + + return ( +
+

Relatórios

+
+ + + + setResponsible(e.target.value)} /> + + setDate(e.target.value)} /> +
+ {filtered.map((a, i) => ( +
+ {a.type} - {a.date} - {a.responsible} +
+ ))} + {filtered.length === 0 &&

Nenhum resultado.

} +
+ ); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..89e84f8 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,12 @@ +export interface AuditAnswer { + questionId: string; + status: 'Conforme' | 'Não conforme leve' | 'Não conforme grave'; + photo?: string; // base64 +} + +export interface Audit { + type: string; + date: string; + responsible: string; + answers: AuditAnswer[]; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3d0a51a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..9d31e2a --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..efe6335 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173 + } +});