navigate(`/users/${item.user.id}`)}
+ onClick={() => navigate(`/users/${item.user.slug || item.user.id}`)}
>
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index b25d315..3c2e2a4 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -2,6 +2,7 @@ services:
app:
build: .
container_name: fasto-app-local
+ restart: unless-stopped
ports:
- "3001:3001"
environment:
@@ -27,6 +28,7 @@ services:
db:
image: mysql:8.0
container_name: fasto-db-local
+ restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root_password}
MYSQL_DATABASE: ${DB_NAME:-agenciac_comia}
diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx
index e3e019d..3218f86 100644
--- a/pages/Dashboard.tsx
+++ b/pages/Dashboard.tsx
@@ -25,6 +25,7 @@ interface SellerStats {
export const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState ([]);
+ const [prevData, setPrevData] = useState([]);
const [users, setUsers] = useState([]);
const [teams, setTeams] = useState([]);
const [currentUser, setCurrentUser] = useState(null);
@@ -48,16 +49,24 @@ export const Dashboard: React.FC = () => {
const storedUserId = localStorage.getItem('ctms_user_id');
if (!tenantId) return;
+ // Calculate previous date range for accurate period-over-period trend
+ const duration = filters.dateRange.end.getTime() - filters.dateRange.start.getTime();
+ const prevStart = new Date(filters.dateRange.start.getTime() - duration);
+ const prevEnd = new Date(filters.dateRange.start.getTime());
+ const prevFilters = { ...filters, dateRange: { start: prevStart, end: prevEnd } };
+
// Fetch users, attendances, teams and current user in parallel
- const [fetchedUsers, fetchedData, fetchedTeams, me] = await Promise.all([
+ const [fetchedUsers, fetchedData, prevFetchedData, fetchedTeams, me] = await Promise.all([
getUsers(tenantId),
getAttendances(tenantId, filters),
+ getAttendances(tenantId, prevFilters),
getTeams(tenantId),
storedUserId ? getUserById(storedUserId) : null
]);
setUsers(fetchedUsers);
setData(fetchedData);
+ setPrevData(prevFetchedData);
setTeams(fetchedTeams);
if (me) setCurrentUser(me);
} catch (error) {
@@ -74,6 +83,48 @@ export const Dashboard: React.FC = () => {
const avgScore = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.score, 0) / data.length).toFixed(1) : "0";
const avgResponseTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.first_response_time_min, 0) / data.length).toFixed(0) : "0";
const avgHandleTime = data.length > 0 ? (data.reduce((acc, curr) => acc + curr.handling_time_min, 0) / data.length).toFixed(0) : "0";
+
+ // --- Dynamic Trends Calculation ---
+ const trends = useMemo(() => {
+ if (data.length === 0 && prevData.length === 0) {
+ return { leads: null, score: null, resp: null };
+ }
+
+ const calcAvg = (arr: Attendance[], key: keyof Attendance) =>
+ arr.length ? arr.reduce((acc, curr) => acc + (curr[key] as number), 0) / arr.length : 0;
+
+ // Leads Trend (%)
+ const recentLeads = data.length;
+ const prevLeads = prevData.length;
+ let leadsTrend = 0;
+ if (prevLeads > 0) {
+ leadsTrend = ((recentLeads - prevLeads) / prevLeads) * 100;
+ } else if (recentLeads > 0) {
+ leadsTrend = 100;
+ }
+
+ // Score Trend (Absolute point difference)
+ const recentScore = calcAvg(data, 'score');
+ const prevScore = calcAvg(prevData, 'score');
+ let scoreTrend = 0;
+ if (prevData.length > 0) {
+ scoreTrend = recentScore - prevScore;
+ } else {
+ scoreTrend = recentScore;
+ }
+
+ // Response Time Trend (%)
+ const recentResp = calcAvg(data, 'first_response_time_min');
+ const prevResp = calcAvg(prevData, 'first_response_time_min');
+ let respTrend = 0;
+ if (prevResp > 0) {
+ respTrend = ((recentResp - prevResp) / prevResp) * 100;
+ } else if (recentResp > 0) {
+ respTrend = 100; // Time increased from 0, which is worse
+ }
+
+ return { leads: leadsTrend, score: scoreTrend, resp: respTrend };
+ }, [data, prevData]);
// --- Chart Data: Funnel ---
const funnelData = useMemo(() => {
@@ -245,25 +296,25 @@ export const Dashboard: React.FC = () => {
0 ? 'up' : trends.leads < 0 ? 'down' : 'neutral'}
+ trendValue={`${Math.abs(trends.leads).toFixed(1)}%`}
icon={Users}
- colorClass="text-yellow-600"
+ colorClass="text-brand-yellow"
/>
75 ? 'up' : 'down'}
- trendValue="2.1"
+ trend={trends.score > 0 ? 'up' : trends.score < 0 ? 'down' : 'neutral'}
+ trendValue={Math.abs(trends.score).toFixed(1)}
icon={TrendingUp}
- colorClass="text-zinc-600"
+ colorClass="text-zinc-500"
/>
0 ? 'down' : 'neutral'} // Faster response is better (up)
+ trendValue={`${Math.abs(trends.resp).toFixed(1)}%`}
icon={Clock}
colorClass="text-orange-500"
/>
diff --git a/pages/Login.tsx b/pages/Login.tsx
index 3a006ec..c9ea423 100644
--- a/pages/Login.tsx
+++ b/pages/Login.tsx
@@ -58,12 +58,6 @@ export const Login: React.FC = () => {
Acesse sua conta
-
- Ou{' '}
-
- registre sua nova organização
-
-
diff --git a/pages/ResetPassword.tsx b/pages/ResetPassword.tsx
index 70094fd..31a90ab 100644
--- a/pages/ResetPassword.tsx
+++ b/pages/ResetPassword.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom';
import { Hexagon, Lock, ArrowRight, Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
import { resetPassword } from '../services/dataService';
@@ -26,7 +26,7 @@ export const ResetPassword: React.FC = () => {
setError('');
try {
- await resetPassword(password, token);
+ await resetPassword(password, token); // No name sent here
setIsSuccess(true);
setTimeout(() => navigate('/login'), 3000);
} catch (err: any) {
diff --git a/pages/SetupAccount.tsx b/pages/SetupAccount.tsx
new file mode 100644
index 0000000..07d0fd9
--- /dev/null
+++ b/pages/SetupAccount.tsx
@@ -0,0 +1,156 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate, useLocation, Link } from 'react-router-dom';
+import { Hexagon, Lock, ArrowRight, Loader2, CheckCircle2, AlertCircle, User } from 'lucide-react';
+import { resetPassword } from '../services/dataService';
+
+export const ResetPassword: React.FC = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const query = new URLSearchParams(location.search);
+ const token = query.get('token') || '';
+
+ const [name, setName] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (password !== confirmPassword) {
+ setError('As senhas não coincidem.');
+ return;
+ }
+
+ setIsLoading(true);
+ setError('');
+
+ try {
+ await resetPassword(password, token, name);
+ setIsSuccess(true);
+ setTimeout(() => navigate('/login'), 3000);
+ } catch (err: any) {
+ setError(err.message || 'Erro ao redefinir senha. O link pode estar expirado.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Finalize seu cadastro
+
+
+
+
+
+ {isSuccess ? (
+
+
+
+
+ Tudo pronto!
+
+ Seu perfil foi atualizado com sucesso. Redirecionando para o login...
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
diff --git a/pages/SuperAdmin.tsx b/pages/SuperAdmin.tsx
index 0cd5c34..7ef59eb 100644
--- a/pages/SuperAdmin.tsx
+++ b/pages/SuperAdmin.tsx
@@ -1,9 +1,9 @@
import React, { useState, useMemo } from 'react';
import {
Building2, Users, MessageSquare, Plus, Search,
- Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X
+ Edit, Trash2, ChevronDown, ChevronUp, ChevronsUpDown, X, CheckCircle2
} from 'lucide-react';
-import { getTenants, createTenant, deleteTenant } from '../services/dataService';
+import { getTenants, createTenant, deleteTenant, updateTenant } from '../services/dataService';
import { Tenant } from '../types';
import { DateRangePicker } from '../components/DateRangePicker';
import { KPICard } from '../components/KPICard';
@@ -91,21 +91,44 @@ export const SuperAdmin: React.FC = () => {
}
};
+ const [successMessage, setSuccessMessage] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+
const handleSaveTenant = async (e: React.FormEvent) => {
e.preventDefault();
+ setErrorMessage('');
+ setSuccessMessage('');
const form = e.target as HTMLFormElement;
const name = (form.elements.namedItem('name') as HTMLInputElement).value;
const slug = (form.elements.namedItem('slug') as HTMLInputElement).value;
const admin_email = (form.elements.namedItem('admin_email') as HTMLInputElement).value;
const status = (form.elements.namedItem('status') as HTMLSelectElement).value;
- const success = await createTenant({ name, slug, admin_email, status });
- if (success) {
- setIsModalOpen(false);
- setEditingTenant(null);
- loadTenants();
- alert('Organização salva com sucesso!');
+
+ if (editingTenant) {
+ const success = await updateTenant(editingTenant.id, { name, slug, admin_email, status });
+ if (success) {
+ setSuccessMessage('Organização atualizada com sucesso!');
+ loadTenants();
+ setTimeout(() => {
+ setIsModalOpen(false);
+ setSuccessMessage('');
+ setEditingTenant(null);
+ }, 2000);
+ } else {
+ setErrorMessage('Erro ao atualizar organização.');
+ }
} else {
- alert('Erro ao salvar organização.');
+ const result = await createTenant({ name, slug, admin_email, status });
+ if (result.success) {
+ setSuccessMessage(result.message || 'Organização criada com sucesso!');
+ loadTenants();
+ setTimeout(() => {
+ setIsModalOpen(false);
+ setSuccessMessage('');
+ }, 3000);
+ } else {
+ setErrorMessage(result.message || 'Erro ao salvar organização.');
+ }
}
};
@@ -132,7 +155,7 @@ export const SuperAdmin: React.FC = () => {
Painel Super Admin
Gerencie organizações e visualize estatísticas globais.
- |