Reapply password and date filter updates
All checks were successful
Build and Deploy / build-and-push (push) Successful in 3m58s
All checks were successful
Build and Deploy / build-and-push (push) Successful in 3m58s
This commit is contained in:
@@ -23,6 +23,7 @@ const {
|
||||
|
||||
const app = express();
|
||||
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, sound_enabled, created_at';
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
app.use(createCorsMiddleware({ allowedOrigins, isProduction }));
|
||||
app.use(express.json());
|
||||
@@ -447,8 +448,9 @@ apiRouter.get('/users/:idOrSlug', async (req, res) => {
|
||||
|
||||
// Convidar Novo Membro (Admin criando usuário)
|
||||
apiRouter.post('/users', requireRole(['admin', 'manager', 'super_admin']), async (req, res) => {
|
||||
const { name, email, role, team_id, tenant_id } = req.body;
|
||||
const { name, email, role, team_id, tenant_id, password } = req.body;
|
||||
const effectiveTenantId = req.user.role === 'super_admin' ? tenant_id : req.user.tenant_id;
|
||||
const shouldSetPassword = typeof password === 'string' && password.length > 0;
|
||||
|
||||
// Strict RBAC: Managers can only create agents and assign them to their own team
|
||||
let finalRole = role || 'agent';
|
||||
@@ -460,20 +462,30 @@ apiRouter.post('/users', requireRole(['admin', 'manager', 'super_admin']), async
|
||||
finalTeamId = req.user.team_id; // Force assignment to manager's team
|
||||
}
|
||||
try {
|
||||
if (shouldSetPassword && req.user.role !== 'admin' && req.user.role !== 'super_admin') {
|
||||
return res.status(403).json({ error: 'Apenas admins podem definir senha de usuários.' });
|
||||
}
|
||||
if (shouldSetPassword && password.length < MIN_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: `A senha deve ter pelo menos ${MIN_PASSWORD_LENGTH} caracteres.` });
|
||||
}
|
||||
// 1. Verificar se e-mail já existe
|
||||
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
|
||||
if (existing.length > 0) return res.status(400).json({ error: 'E-mail já cadastrado.' });
|
||||
|
||||
const uid = `u_${crypto.randomUUID().split('-')[0]}`;
|
||||
const slug = `${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-${crypto.randomBytes(4).toString('hex')}`;
|
||||
const placeholderHash = 'pending_setup'; // Usuário não pode logar com isso
|
||||
const passwordHash = shouldSetPassword ? await bcrypt.hash(password, 10) : 'pending_setup';
|
||||
|
||||
// 2. Criar Usuário
|
||||
await pool.query(
|
||||
'INSERT INTO users (id, tenant_id, team_id, name, email, password_hash, slug, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[uid, effectiveTenantId, finalTeamId, name, email, placeholderHash, slug, finalRole, 'active']
|
||||
[uid, effectiveTenantId, finalTeamId, name, email, passwordHash, slug, finalRole, 'active']
|
||||
);
|
||||
|
||||
if (shouldSetPassword) {
|
||||
return res.status(201).json({ id: uid, message: 'Membro criado com senha definida.' });
|
||||
}
|
||||
|
||||
// 3. Gerar Token de Setup de Senha (reusando lógica de reset)
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
await pool.query(
|
||||
@@ -511,12 +523,19 @@ apiRouter.post('/users', requireRole(['admin', 'manager', 'super_admin']), async
|
||||
});
|
||||
|
||||
apiRouter.put('/users/:id', async (req, res) => {
|
||||
const { name, bio, role, team_id, status, email, sound_enabled } = req.body;
|
||||
const { name, bio, role, team_id, status, email, sound_enabled, password } = req.body;
|
||||
const shouldUpdatePassword = typeof password === 'string' && password.length > 0;
|
||||
try {
|
||||
const [existing] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
||||
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
if (!canUpdateUser(req.user, existing[0])) return res.status(403).json({ error: 'Acesso negado.' });
|
||||
if (shouldUpdatePassword && req.user.role !== 'admin' && req.user.role !== 'super_admin') {
|
||||
return res.status(403).json({ error: 'Apenas admins podem alterar senha de usuários.' });
|
||||
}
|
||||
if (shouldUpdatePassword && password.length < MIN_PASSWORD_LENGTH) {
|
||||
return res.status(400).json({ error: `A senha deve ter pelo menos ${MIN_PASSWORD_LENGTH} caracteres.` });
|
||||
}
|
||||
|
||||
// Only Admins can change roles and teams. Managers can only edit basic info of their team members.
|
||||
const finalRole = canManageUserRoleOrTeam(req.user) && role !== undefined ? role : existing[0].role;
|
||||
@@ -535,6 +554,12 @@ apiRouter.put('/users/:id', async (req, res) => {
|
||||
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, finalSoundEnabled, req.params.id]
|
||||
);
|
||||
|
||||
if (shouldUpdatePassword) {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await pool.query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, req.params.id]);
|
||||
await pool.query('DELETE FROM password_resets WHERE email = ?', [finalEmail]);
|
||||
}
|
||||
|
||||
// Trigger Notification for Team Change
|
||||
if (finalTeamId && finalTeamId !== existing[0].team_id && existing[0].status === 'active') {
|
||||
const [team] = await pool.query('SELECT name FROM teams WHERE id = ?', [finalTeamId]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fasto | Management</title>
|
||||
<title>Fasto</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Calendar } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Calendar, ChevronDown } from 'lucide-react';
|
||||
import { DateRange } from '../types';
|
||||
|
||||
interface DateRangePickerProps {
|
||||
@@ -8,9 +8,22 @@ 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();
|
||||
@@ -23,6 +36,23 @@ 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;
|
||||
@@ -33,16 +63,14 @@ 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: newStart });
|
||||
onChange({ ...dateRange, start: startOfDay(newStart) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newEnd = parseLocalDate(e.target.value);
|
||||
if (newEnd && !isNaN(newEnd.getTime())) {
|
||||
// Set to end of day to ensure the query includes the whole day
|
||||
newEnd.setHours(23, 59, 59, 999);
|
||||
onChange({ ...dateRange, end: newEnd });
|
||||
onChange({ ...dateRange, end: endOfDay(newEnd) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,44 +88,146 @@ 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 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 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>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Hexagon, Lock, Mail, ArrowRight, Loader2, Eye, EyeOff, AlertCircle } from 'lucide-react';
|
||||
import { login, logout } from '../services/dataService';
|
||||
|
||||
@@ -108,11 +108,6 @@ 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">
|
||||
|
||||
@@ -24,7 +24,8 @@ export const TeamManagement: React.FC = () => {
|
||||
role: 'agent' as any,
|
||||
team_id: '',
|
||||
status: 'active' as any,
|
||||
tenant_id: ''
|
||||
tenant_id: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -67,12 +68,26 @@ export const TeamManagement: React.FC = () => {
|
||||
try {
|
||||
const tid = localStorage.getItem('ctms_tenant_id') || '';
|
||||
const finalTenantId = currentUser?.role === 'super_admin' ? formData.tenant_id : tid;
|
||||
const password = formData.password.trim();
|
||||
|
||||
if (password && password.length < 6) {
|
||||
alert('A senha deve ter pelo menos 6 caracteres.');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: any = {
|
||||
...formData,
|
||||
tenant_id: finalTenantId,
|
||||
password: undefined,
|
||||
};
|
||||
|
||||
if (password) payload.password = password;
|
||||
|
||||
if (editingUser) {
|
||||
const success = await updateUser(editingUser.id, { ...formData, tenant_id: finalTenantId });
|
||||
const success = await updateUser(editingUser.id, payload);
|
||||
if (success) { setIsModalOpen(false); loadData(); }
|
||||
} else {
|
||||
await createMember({ ...formData, tenant_id: finalTenantId });
|
||||
await createMember(payload);
|
||||
setIsModalOpen(false);
|
||||
loadData();
|
||||
}
|
||||
@@ -94,6 +109,7 @@ export const TeamManagement: React.FC = () => {
|
||||
if (loading && users.length === 0) return <div className="flex h-screen items-center justify-center text-zinc-400 dark:text-dark-muted transition-colors">Carregando...</div>;
|
||||
|
||||
const canManage = currentUser?.role === 'admin' || currentUser?.role === 'super_admin' || currentUser?.role === 'manager';
|
||||
const canManagePasswords = currentUser?.role === 'admin' || currentUser?.role === 'super_admin';
|
||||
const filtered = users.filter(u =>
|
||||
(u.name.toLowerCase().includes(searchTerm.toLowerCase()) || u.email.toLowerCase().includes(searchTerm.toLowerCase())) &&
|
||||
(tenantFilter === 'all' || u.tenant_id === tenantFilter)
|
||||
@@ -122,7 +138,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'}); 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:''}); 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>
|
||||
)}
|
||||
@@ -195,7 +211,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||''}); 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:''}); 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>
|
||||
@@ -233,6 +249,19 @@ 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>
|
||||
|
||||
Reference in New Issue
Block a user