Revert "Polish user management and filters"

This reverts commit df8e6d3d88.
This commit is contained in:
Cauê Faleiros
2026-06-09 15:58:43 -03:00
parent df8e6d3d88
commit ed969b8c2c
7 changed files with 96 additions and 202 deletions

View File

@@ -13,7 +13,7 @@ const {
const { recordActivity } = require('../services/activityService');
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, created_at';
const MIN_PASSWORD_LENGTH = 6;
const MIN_PASSWORD_LENGTH = 8;
const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
const router = express.Router();

View File

@@ -20,8 +20,6 @@ services:
- SMTP_PASS=${SMTP_PASS}
- MAIL_FROM=${MAIL_FROM}
- SMTP_DEBUG=${SMTP_DEBUG:-false}
volumes:
- uploads_data_local:/app/uploads
command: ["node", "index.js"]
depends_on:
- db
@@ -39,4 +37,3 @@ services:
volumes:
db_data_local:
uploads_data_local:

View File

@@ -21,8 +21,6 @@ services:
- SMTP_DEBUG=${SMTP_DEBUG:-false}
ports:
- "3001:3001"
volumes:
- uploads_data:/app/uploads
deploy:
replicas: 1
restart_policy:
@@ -67,7 +65,6 @@ services:
volumes:
db_data:
uploads_data:
configs:
init_sql:

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fasto</title>
<title>Fasto | Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { Calendar, ChevronDown } from 'lucide-react';
import React, { useRef } from 'react';
import { Calendar } from 'lucide-react';
import { DateRange } from '../types';
interface DateRangePickerProps {
@@ -8,22 +8,9 @@ interface DateRangePickerProps {
}
export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const startRef = useRef<HTMLInputElement>(null);
const endRef = useRef<HTMLInputElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const formatDateForInput = (date: Date) => {
// Format to local YYYY-MM-DD to avoid timezone shifts
const year = date.getFullYear();
@@ -36,23 +23,6 @@ export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onC
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: 'America/Sao_Paulo' });
};
const startOfDay = (date: Date) => {
const result = new Date(date);
result.setHours(0, 0, 0, 0);
return result;
};
const endOfDay = (date: Date) => {
const result = new Date(date);
result.setHours(23, 59, 59, 999);
return result;
};
const setRange = (start: Date, end: Date) => {
onChange({ start: startOfDay(start), end: endOfDay(end) });
setIsOpen(false);
};
const parseLocalDate = (value: string) => {
// Split "YYYY-MM-DD" and create date in local timezone to avoid UTC midnight shift
if (!value) return null;
@@ -63,14 +33,16 @@ export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onC
const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newStart = parseLocalDate(e.target.value);
if (newStart && !isNaN(newStart.getTime())) {
onChange({ ...dateRange, start: startOfDay(newStart) });
onChange({ ...dateRange, start: newStart });
}
};
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEnd = parseLocalDate(e.target.value);
if (newEnd && !isNaN(newEnd.getTime())) {
onChange({ ...dateRange, end: endOfDay(newEnd) });
// Set to end of day to ensure the query includes the whole day
newEnd.setHours(23, 59, 59, 999);
onChange({ ...dateRange, end: newEnd });
}
};
@@ -88,146 +60,44 @@ export const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onC
}
};
const today = startOfDay(new Date());
const shortcuts = [
{
label: 'Hoje',
onClick: () => setRange(today, today),
},
{
label: 'Ontem',
onClick: () => {
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
setRange(yesterday, yesterday);
},
},
{
label: 'Últimos 7 dias',
onClick: () => {
const start = new Date(today);
start.setDate(today.getDate() - 6);
setRange(start, today);
},
},
{
label: 'Últimos 30 dias',
onClick: () => {
const start = new Date(today);
start.setDate(today.getDate() - 29);
setRange(start, today);
},
},
{
label: 'Este Mês',
onClick: () => setRange(new Date(today.getFullYear(), today.getMonth(), 1), today),
},
{
label: 'Mês Passado',
onClick: () => setRange(
new Date(today.getFullYear(), today.getMonth() - 1, 1),
new Date(today.getFullYear(), today.getMonth(), 0)
),
},
{
label: 'Últimos 90 dias',
onClick: () => {
const start = new Date(today);
start.setDate(today.getDate() - 89);
setRange(start, today);
},
},
{
label: 'Este Ano',
onClick: () => setRange(new Date(today.getFullYear(), 0, 1), today),
},
{
label: 'Todo o Período',
onClick: () => setRange(new Date(0), today),
},
];
return (
<div ref={containerRef} className="relative">
<button
type="button"
onClick={() => setIsOpen(open => !open)}
className="flex min-w-[224px] items-center justify-between gap-2 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm font-medium text-zinc-700 outline-none transition-all hover:border-zinc-300 focus:ring-2 focus:ring-brand-yellow/20 dark:border-dark-border dark:bg-dark-bg dark:text-zinc-200 dark:hover:border-dark-border"
>
<span className="flex items-center gap-2">
<Calendar size={16} className="text-zinc-500 dark:text-dark-muted shrink-0" />
<span>{formatShortDate(dateRange.start)} - {formatShortDate(dateRange.end)}</span>
</span>
<ChevronDown size={14} className={`text-zinc-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute right-0 top-full z-50 mt-2 w-56 overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-dark-border dark:bg-dark-card">
<div className="p-4">
<div className="mb-3 text-xs font-bold uppercase tracking-[0.18em] text-zinc-500 dark:text-dark-muted">
Período Customizado
</div>
<div className="space-y-2">
<div className="grid grid-cols-[26px_1fr] items-center gap-2">
<label className="text-xs font-medium text-zinc-700 dark:text-dark-text">De:</label>
<button
type="button"
onClick={() => openPicker(startRef)}
className="relative h-7 rounded-lg border border-zinc-200 bg-zinc-50 px-3 text-center text-xs text-zinc-800 dark:border-dark-border dark:bg-dark-input dark:text-dark-text"
>
{formatShortDate(dateRange.start)}
<input
ref={startRef}
type="date"
value={formatDateForInput(dateRange.start)}
onChange={handleStartChange}
className="absolute inset-0 h-full w-full opacity-0"
tabIndex={-1}
/>
</button>
</div>
<div className="grid grid-cols-[26px_1fr] items-center gap-2">
<label className="text-xs font-medium text-zinc-700 dark:text-dark-text">Até:</label>
<button
type="button"
onClick={() => openPicker(endRef)}
className="relative h-7 rounded-lg border border-zinc-200 bg-zinc-50 px-3 text-center text-xs text-zinc-800 dark:border-dark-border dark:bg-dark-input dark:text-dark-text"
>
{formatShortDate(dateRange.end)}
<input
ref={endRef}
type="date"
value={formatDateForInput(dateRange.end)}
onChange={handleEndChange}
className="absolute inset-0 h-full w-full opacity-0"
tabIndex={-1}
/>
</button>
</div>
</div>
</div>
<div className="border-t border-zinc-100 p-4 dark:border-dark-border">
<div className="mb-2 text-xs font-bold uppercase tracking-[0.18em] text-zinc-500 dark:text-dark-muted">
Atalhos
</div>
<div className="space-y-1">
{shortcuts.map(shortcut => (
<button
key={shortcut.label}
type="button"
onClick={shortcut.onClick}
className="block w-full rounded-lg px-0 py-1.5 text-left text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:text-dark-muted dark:hover:text-dark-text"
>
{shortcut.label}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 bg-white dark:bg-dark-bg border border-zinc-200 dark:border-dark-border px-3 py-2 rounded-lg shadow-sm hover:border-zinc-300 dark:hover:border-dark-border transition-colors">
<Calendar size={16} className="text-zinc-500 dark:text-dark-muted shrink-0" />
<div className="flex items-center gap-2 text-sm font-medium text-zinc-700 dark:text-zinc-200">
{/* Start Date */}
<div
className="relative cursor-pointer hover:text-brand-yellow transition-colors"
onClick={() => openPicker(startRef)}
>
{formatShortDate(dateRange.start)}
<input
ref={startRef}
type="date"
value={formatDateForInput(dateRange.start)}
onChange={handleStartChange}
className="absolute opacity-0 w-0 h-0 overflow-hidden"
/>
</div>
)}
<span className="text-zinc-400 dark:text-dark-muted font-normal text-xs">até</span>
{/* End Date */}
<div
className="relative cursor-pointer hover:text-brand-yellow transition-colors"
onClick={() => openPicker(endRef)}
>
{formatShortDate(dateRange.end)}
<input
ref={endRef}
type="date"
value={formatDateForInput(dateRange.end)}
onChange={handleEndChange}
className="absolute opacity-0 w-0 h-0 overflow-hidden"
/>
</div>
</div>
</div>
);
};
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, Link } from 'react-router-dom';
import { Hexagon, Lock, Mail, ArrowRight, Loader2, Eye, EyeOff, AlertCircle } from 'lucide-react';
import { login, logout } from '../services/dataService';
@@ -108,6 +108,11 @@ export const Login: React.FC = () => {
<label htmlFor="password" senior-admin-password className="block text-sm font-medium text-zinc-700 dark:text-zinc-300">
Senha
</label>
<div className="text-sm">
<Link to="/forgot-password" size="14" className="font-medium text-brand-yellow hover:text-yellow-600 transition-colors">
Esqueceu a senha?
</Link>
</div>
</div>
<div className="mt-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Users, Plus, Search, X, Edit, Trash2, Loader2, AlertTriangle } from 'lucide-react';
import { Users, Plus, Search, X, Edit, Trash2, Loader2, AlertTriangle, KeyRound } from 'lucide-react';
import { getUsers, getTeams, createMember, deleteUser, updateUser, getUserById, getTenants } from '../services/dataService';
import { User, Tenant } from '../types';
@@ -25,7 +25,8 @@ export const TeamManagement: React.FC = () => {
team_id: '',
status: 'active' as any,
tenant_id: '',
password: ''
password: '',
confirmPassword: ''
});
const loadData = async () => {
@@ -69,10 +70,15 @@ export const TeamManagement: React.FC = () => {
const tid = localStorage.getItem('ctms_tenant_id') || '';
const finalTenantId = currentUser?.role === 'super_admin' ? formData.tenant_id : tid;
const password = formData.password.trim();
const confirmPassword = formData.confirmPassword.trim();
if (password) {
if (password.length < 6) {
alert('A senha deve ter pelo menos 6 caracteres.');
if (password || confirmPassword) {
if (password.length < 8) {
alert('A senha deve ter pelo menos 8 caracteres.');
return;
}
if (password !== confirmPassword) {
alert('As senhas não conferem.');
return;
}
}
@@ -143,7 +149,7 @@ export const TeamManagement: React.FC = () => {
<p className="text-zinc-500 dark:text-dark-muted text-sm">Visualize e gerencie as funções dos membros da sua organização.</p>
</div>
{canManage && (
<button onClick={() => { setEditingUser(null); setFormData({name:'', email:'', role:'agent', team_id:'', status:'active', tenant_id:'', password:''}); setIsModalOpen(true); }} className="bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 px-4 py-2 rounded-lg flex items-center gap-2 text-sm font-bold shadow-sm hover:opacity-90 transition-all">
<button onClick={() => { setEditingUser(null); setFormData({name:'', email:'', role:'agent', team_id:'', status:'active', tenant_id:'', password:'', confirmPassword:''}); setIsModalOpen(true); }} className="bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 px-4 py-2 rounded-lg flex items-center gap-2 text-sm font-bold shadow-sm hover:opacity-90 transition-all">
<Plus size={16} /> Adicionar Membro
</button>
)}
@@ -216,7 +222,7 @@ export const TeamManagement: React.FC = () => {
{canManage && (
<td className="px-6 py-4 text-right">
<div className="flex justify-end gap-2 transition-opacity">
<button onClick={() => { setEditingUser(user); setFormData({name:user.name, email:user.email, role:user.role as any, team_id:user.team_id||'', status:user.status as any, tenant_id: user.tenant_id||'', password:''}); setIsModalOpen(true); }} className="p-2 hover:bg-zinc-100 dark:hover:bg-dark-border text-zinc-400 hover:text-zinc-900 dark:hover:text-dark-text rounded-lg transition-colors"><Edit size={16} /></button>
<button onClick={() => { setEditingUser(user); setFormData({name:user.name, email:user.email, role:user.role as any, team_id:user.team_id||'', status:user.status as any, tenant_id: user.tenant_id||'', password:'', confirmPassword:''}); setIsModalOpen(true); }} className="p-2 hover:bg-zinc-100 dark:hover:bg-dark-border text-zinc-400 hover:text-zinc-900 dark:hover:text-dark-text rounded-lg transition-colors"><Edit size={16} /></button>
<button onClick={() => { setUserToDelete(user); setDeleteConfirmName(''); setIsDeleteModalOpen(true); }} className="p-2 hover:bg-red-50 dark:hover:bg-red-900/30 text-zinc-400 hover:text-red-600 dark:hover:text-red-400 rounded-lg transition-colors"><Trash2 size={16} /></button>
</div>
</td>
@@ -254,19 +260,6 @@ export const TeamManagement: React.FC = () => {
required
/>
</div>
{canManagePasswords && (
<div>
<label className="text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase mb-1 block">Senha</label>
<input
type="password"
value={formData.password}
onChange={e => setFormData({...formData, password:e.target.value})}
className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text outline-none focus:ring-2 focus:ring-brand-yellow/20"
placeholder={editingUser ? 'Deixe em branco para manter a senha atual' : 'Opcional'}
autoComplete="new-password"
/>
</div>
)}
{currentUser?.role === 'super_admin' && (
<div>
<label className="text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase mb-1 block">Organização</label>
@@ -309,6 +302,38 @@ export const TeamManagement: React.FC = () => {
<option value="inactive">Inativo</option>
</select>
</div>
{canManagePasswords && (
<div className="space-y-4 rounded-xl border border-zinc-200 dark:border-dark-border bg-zinc-50 dark:bg-dark-bg/50 p-4">
<div className="flex items-center gap-2 text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase">
<KeyRound size={14} className="text-brand-yellow" />
{editingUser ? 'Alterar senha' : 'Senha inicial'}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase mb-1 block">Senha</label>
<input
type="password"
value={formData.password}
onChange={e => setFormData({...formData, password:e.target.value})}
className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text outline-none focus:ring-2 focus:ring-brand-yellow/20"
placeholder={editingUser ? 'Deixe em branco para manter' : 'Opcional'}
autoComplete="new-password"
/>
</div>
<div>
<label className="text-xs font-bold text-zinc-500 dark:text-dark-muted uppercase mb-1 block">Confirmar</label>
<input
type="password"
value={formData.confirmPassword}
onChange={e => setFormData({...formData, confirmPassword:e.target.value})}
className="w-full bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border p-3 rounded-lg text-sm text-zinc-900 dark:text-dark-text outline-none focus:ring-2 focus:ring-brand-yellow/20"
placeholder="Repita a senha"
autoComplete="new-password"
/>
</div>
</div>
</div>
)}
<div className="flex justify-end gap-3 pt-6 border-t dark:border-dark-border mt-6">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-6 py-2.5 text-sm font-medium text-zinc-500 dark:text-dark-muted hover:bg-zinc-100 dark:hover:bg-dark-border rounded-lg transition-colors">Cancelar</button>
<button type="submit" disabled={isSaving} className="bg-zinc-900 dark:bg-brand-yellow text-white dark:text-zinc-950 px-8 py-2.5 rounded-lg text-sm font-bold flex items-center gap-2 hover:opacity-90 transition-all">{isSaving ? <Loader2 className="animate-spin" size={16} /> : 'Salvar Alterações'}</button>