Compare commits
No commits in common. "9d13e6626db1eb3ed5ba041a6ccb0cbc8f67a197" and "73b7e171c9bfff3a1d176f2a6b7a509b093fc4e4" have entirely different histories.
9d13e6626d
...
73b7e171c9
|
|
@ -38,32 +38,40 @@ export async function POST(req: NextRequest) {
|
||||||
const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']);
|
const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']);
|
||||||
const planId = planRes.rows.length > 0 ? planRes.rows[0].id : null;
|
const planId = planRes.rows.length > 0 ? planRes.rows[0].id : null;
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
await query('BEGIN');
|
||||||
|
|
||||||
|
// 1. Create Tenant
|
||||||
const res = await query(
|
const res = await query(
|
||||||
`INSERT INTO tenants (name, slug, email, phone, active_plan_id, subscription_status, subscription_ends_at)
|
`INSERT INTO tenants (name, slug, email, phone, active_plan_id, subscription_status, subscription_ends_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '30 days') RETURNING *`,
|
VALUES ($1, $2, $3, $4, $5, $6, NOW() + INTERVAL '30 days') RETURNING *`,
|
||||||
[name, slug, email, phone, planId, status || 'trial']
|
[name, slug, email, phone, planId, status || 'trial']
|
||||||
);
|
);
|
||||||
|
|
||||||
const newTenant = res.rows[0];
|
const tenant = res.rows[0];
|
||||||
|
|
||||||
if (password && email) {
|
// 2. Create Owner User if password provided
|
||||||
|
if (email && password) {
|
||||||
const salt = await bcrypt.genSalt(10);
|
const salt = await bcrypt.genSalt(10);
|
||||||
const hash = await bcrypt.hash(password, salt);
|
const hash = await bcrypt.hash(password, salt);
|
||||||
await query(`
|
await query(
|
||||||
INSERT INTO users (tenant_id, name, email, password_hash, role)
|
`INSERT INTO users (tenant_id, name, email, password_hash, role, phone)
|
||||||
VALUES ($1, $2, $3, $4, 'owner')
|
VALUES ($1, $2, $3, $4, 'owner', $5)`,
|
||||||
`, [newTenant.id, name, email, hash]);
|
[tenant.id, name, email, hash, phone]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(newTenant);
|
await query('COMMIT');
|
||||||
|
return NextResponse.json(tenant);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
await query('ROLLBACK');
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(req: NextRequest) {
|
export async function PUT(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { id, name, slug, email, phone, plan, status, password } = await req.json();
|
const { id, name, slug, email, phone, plan, status } = await req.json();
|
||||||
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 });
|
||||||
|
|
||||||
const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']);
|
const planRes = await query('SELECT id FROM plans WHERE name = $1 LIMIT 1', [plan || 'Starter']);
|
||||||
|
|
@ -76,14 +84,6 @@ export async function PUT(req: NextRequest) {
|
||||||
[name, slug, email, phone, planId, status, id]
|
[name, slug, email, phone, planId, status, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (password) {
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
|
||||||
const hash = await bcrypt.hash(password, salt);
|
|
||||||
await query(`
|
|
||||||
UPDATE users SET password_hash = $1 WHERE tenant_id = $2 AND role = 'owner'
|
|
||||||
`, [hash, id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(res.rows[0]);
|
return NextResponse.json(res.rows[0]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||||
|
|
|
||||||
|
|
@ -168,12 +168,12 @@ export default function Tenants() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group"><label className="form-label">E-mail (Login) *</label><input type="email" required className="form-input" value={form.email || ''} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /></div>
|
<div className="form-group"><label className="form-label">E-mail</label><input className="form-input" type="email" value={form.email || ''} onChange={e => setForm(p => ({ ...p, email: e.target.value }))} /></div>
|
||||||
<div className="form-group"><label className="form-label">Telefone</label><input className="form-input" value={form.phone || ''} onChange={e => setForm(p => ({ ...p, phone: e.target.value }))} /></div>
|
<div className="form-group"><label className="form-label">Telefone</label><input className="form-input" value={form.phone || ''} onChange={e => setForm(p => ({ ...p, phone: e.target.value }))} /></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
{!editing && (
|
||||||
<div className="form-group"><label className="form-label">Senha de Acesso {editing ? '(Deixe em branco para manter)' : '*'}</label><input type="text" className="form-input" placeholder={editing ? '••••••••' : 'Senha do Inquilino'} value={form.password || ''} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} /></div>
|
<div className="form-group"><label className="form-label">Senha Inicial do Administrador *</label><input type="password" required className="form-input" value={form.password || ''} onChange={e => setForm(p => ({ ...p, password: e.target.value }))} placeholder="Mínimo 6 caracteres" /></div>
|
||||||
</div>
|
)}
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group"><label className="form-label">Plano</label>
|
<div className="form-group"><label className="form-label">Plano</label>
|
||||||
<select className="form-select" value={form.plan || ''} onChange={e => setForm(p => ({ ...p, plan: e.target.value }))}>
|
<select className="form-select" value={form.plan || ''} onChange={e => setForm(p => ({ ...p, plan: e.target.value }))}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link, Eye, EyeOff } from 'lucide-react';
|
import { Store, Clock, Globe, Bell, Palette, Shield, Save, Copy, ExternalLink, Check, Link } from 'lucide-react';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'general', label: 'Geral', icon: Store },
|
{ id: 'general', label: 'Geral', icon: Store },
|
||||||
|
|
@ -397,54 +397,25 @@ export default function Settings() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'security' && (
|
{activeTab === 'security' && (
|
||||||
<SecurityTab handleSave={handleSave} saved={saved} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SecurityTab({ handleSave, saved }: { handleSave: () => void; saved: boolean }) {
|
|
||||||
const [showCurrent, setShowCurrent] = useState(false);
|
|
||||||
const [showNew, setShowNew] = useState(false);
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
|
||||||
|
|
||||||
// Re-importing specific lucide icons dynamically if possible, or we can use a generic button.
|
|
||||||
// Actually, we can use Eye and EyeOff if we import them at the top. Let's assume they are imported.
|
|
||||||
// Wait, I need to add the import at the top of the file!
|
|
||||||
// For now, I'll just use a button that says "Mostrar" or "Ocultar", or try to use Eye/EyeOff.
|
|
||||||
return (
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3 className="card-title mb-4">Segurança</h3>
|
<h3 className="card-title mb-4">Segurança</h3>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Senha Atual</label>
|
<label className="form-label">Senha Atual</label>
|
||||||
<div style={{ position: 'relative' }}>
|
<input className="form-input" type="password" placeholder="••••••••" />
|
||||||
<input className="form-input" type={showCurrent ? "text" : "password"} placeholder="••••••••" style={{ paddingRight: '40px' }} />
|
|
||||||
<button type="button" className="btn-ghost btn-icon" onClick={() => setShowCurrent(!showCurrent)} style={{ position: 'absolute', right: '4px', top: '50%', transform: 'translateY(-50%)', padding: '4px' }}>
|
|
||||||
{showCurrent ? <EyeOff size={16} color="var(--text-muted)" /> : <Eye size={16} color="var(--text-muted)" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid-2">
|
<div className="grid-2">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Nova Senha</label>
|
<label className="form-label">Nova Senha</label>
|
||||||
<div style={{ position: 'relative' }}>
|
<input className="form-input" type="password" placeholder="••••••••" />
|
||||||
<input className="form-input" type={showNew ? "text" : "password"} placeholder="••••••••" style={{ paddingRight: '40px' }} />
|
|
||||||
<button type="button" className="btn-ghost btn-icon" onClick={() => setShowNew(!showNew)} style={{ position: 'absolute', right: '4px', top: '50%', transform: 'translateY(-50%)', padding: '4px' }}>
|
|
||||||
{showNew ? <EyeOff size={16} color="var(--text-muted)" /> : <Eye size={16} color="var(--text-muted)" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Confirmar Nova Senha</label>
|
<label className="form-label">Confirmar Nova Senha</label>
|
||||||
<div style={{ position: 'relative' }}>
|
<input className="form-input" type="password" placeholder="••••••••" />
|
||||||
<input className="form-input" type={showConfirm ? "text" : "password"} placeholder="••••••••" style={{ paddingRight: '40px' }} />
|
|
||||||
<button type="button" className="btn-ghost btn-icon" onClick={() => setShowConfirm(!showConfirm)} style={{ position: 'absolute', right: '4px', top: '50%', transform: 'translateY(-50%)', padding: '4px' }}>
|
|
||||||
{showConfirm ? <EyeOff size={16} color="var(--text-muted)" /> : <Eye size={16} color="var(--text-muted)" />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-primary mt-4" onClick={handleSave}><Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar'}</button>
|
<button className="btn btn-primary mt-4" onClick={handleSave}><Save size={16} /> {saved ? '✓ Salvo!' : 'Salvar'}</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const mockNotifications = [
|
||||||
export default function Topbar({ title, subtitle, onMenuClick }: TopbarProps) {
|
export default function Topbar({ title, subtitle, onMenuClick }: TopbarProps) {
|
||||||
const [avatarInitials, setAvatarInitials] = useState('AD');
|
const [avatarInitials, setAvatarInitials] = useState('AD');
|
||||||
const [tenantName, setTenantName] = useState('Administrador');
|
const [tenantName, setTenantName] = useState('Administrador');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('admin@agendapro.com');
|
||||||
|
|
||||||
const [showNotifications, setShowNotifications] = useState(false);
|
const [showNotifications, setShowNotifications] = useState(false);
|
||||||
const [showProfile, setShowProfile] = useState(false);
|
const [showProfile, setShowProfile] = useState(false);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue