feat(master): abrir visualizacao e edicao de clientes em popup e confirmacao para exclusao
This commit is contained in:
@@ -78,6 +78,91 @@
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Visualização (Popup) -->
|
||||
<div id="ver-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalVer()"></div>
|
||||
<section class="modal-panel">
|
||||
<div class="modal-head">
|
||||
<h2 id="ver-modal-titulo">Detalhes do cliente</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalVer()">Fechar</button>
|
||||
</div>
|
||||
<div class="grid cols">
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input id="ver-slug" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Nome</label>
|
||||
<input id="ver-nome" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>CNPJ</label>
|
||||
<input id="ver-cnpj" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Status</label>
|
||||
<div id="ver-status" style="margin-top: 8px;"></div>
|
||||
</div>
|
||||
<div class="field" style="grid-column: 1 / -1;">
|
||||
<label>Plano</label>
|
||||
<input id="ver-plano" readonly style="background: var(--bg-soft, #f8fafc); cursor: default;">
|
||||
</div>
|
||||
<div class="field" style="grid-column: 1 / -1;">
|
||||
<label>API Key de integração</label>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<input id="ver-api-key" readonly onclick="this.select()" style="flex: 1; background: var(--bg-soft, #f8fafc);">
|
||||
<button class="btn secondary" type="button" onclick="copiarApiKeyModal()">Copiar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="grid-column: 1 / -1; display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKeyModal()">Solicitar alteração de API Key</button>
|
||||
</div>
|
||||
<p style="grid-column: 1 / -1; font-size: 13px; color: var(--muted); margin: 4px 0 0;">
|
||||
<strong>Conta principal:</strong> utilize "Editar" para alterar os dados da empresa. Funcionários e contatos são gerenciados no acesso do próprio cliente.
|
||||
</p>
|
||||
<div class="modal-actions" style="grid-column: 1 / -1;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalVer()">Fechar</button>
|
||||
<button class="btn" type="button" onclick="abrirEditarDeVer()">Editar cliente</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Edição (Popup) -->
|
||||
<div id="editar-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalEditar()"></div>
|
||||
<section class="modal-panel">
|
||||
<div class="modal-head">
|
||||
<h2 id="editar-modal-titulo">Editar cliente</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalEditar()">Fechar</button>
|
||||
</div>
|
||||
<form id="editar-form" class="grid cols" onsubmit="salvarEdicaoModal(event)">
|
||||
<div id="editar-modal-error-msg" class="hidden" style="grid-column: 1 / -1; padding: 12px 16px; background: var(--danger-soft); color: var(--danger); border: 1px solid var(--danger); border-radius: var(--radius); font-size: 14px; font-weight: 600;"></div>
|
||||
<input type="hidden" name="id" id="editar-id">
|
||||
<div class="field">
|
||||
<label>Slug</label>
|
||||
<input id="editar-slug" readonly disabled style="background: var(--bg-soft, #f8fafc); opacity: 0.7; cursor: not-allowed;">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Nome</label>
|
||||
<input name="nome" id="editar-nome" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>CNPJ</label>
|
||||
<input name="cnpj" id="editar-cnpj">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Plano</label>
|
||||
<select name="plano_id" id="editar-plano-select"></select>
|
||||
</div>
|
||||
<div class="modal-actions" style="grid-column: 1 / -1;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalEditar()">Cancelar</button>
|
||||
<button class="btn" type="submit">Salvar alterações</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>Layout.mount('master','clientes');</script>
|
||||
<script src="/master/clientes.js"></script>
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
const empresaForm=document.querySelector('#empresa-form'),rows=document.querySelector('#rows'),busca=document.querySelector('#busca'),planoSelect=document.querySelector('#plano-select'),cadastroModal=document.querySelector('#cadastro-modal');let empresas=[],planos=[],modo=null,selecionado=null;
|
||||
let modoSlugAuto='sequencial';
|
||||
const empresaForm = document.querySelector('#empresa-form');
|
||||
const rows = document.querySelector('#rows');
|
||||
const busca = document.querySelector('#busca');
|
||||
const planoSelect = document.querySelector('#plano-select');
|
||||
const cadastroModal = document.querySelector('#cadastro-modal');
|
||||
const verModal = document.querySelector('#ver-modal');
|
||||
const editarModal = document.querySelector('#editar-modal');
|
||||
const editarForm = document.querySelector('#editar-form');
|
||||
|
||||
let empresas = [];
|
||||
let planos = [];
|
||||
let clienteVisualizandoId = null;
|
||||
let clienteEditandoId = null;
|
||||
let modoSlugAuto = 'sequencial';
|
||||
|
||||
const Icons = {
|
||||
edit: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
|
||||
@@ -13,7 +25,6 @@ function planoOptions(valor){return '<option value="">Sem plano</option>'+planos
|
||||
function renderPlanos(){planoSelect.innerHTML=planoOptions('')}
|
||||
function match(e,q){if(!q)return true;return [e.slug,e.nome,e.cnpj,e.status,e.plano,e.api_key].join(' ').toLowerCase().includes(q.toLowerCase())}
|
||||
function statusAction(e){const acao=e.status==='ativa'?'Bloquear':'Ativar';return `<button class="status-action" onclick="alterarStatus(${e.id},'${e.status==='ativa'?'bloqueada':'ativa'}')">${Util.badge(e.status)}<span>${acao}</span></button>`}
|
||||
function detalheRow(e){if(selecionado!==e.id||!modo)return'';if(modo==='ver')return `<tr><td colspan="5"><div class="inline-panel"><h2>${esc(e.nome)}</h2><p><strong>Slug:</strong> ${esc(e.slug)}</p><p><strong>CNPJ:</strong> ${esc(e.cnpj||'-')}</p><p><strong>Status:</strong> ${esc(e.status)}</p><p><strong>Plano:</strong> ${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</p><div class="field"><label>API Key de integração</label><input readonly value="${esc(e.api_key||'')}" onclick="this.select()"></div><div class="row compact"><button class="btn secondary" type="button" onclick="copiarApiKey(${e.id})">Copiar API Key</button><button class="btn secondary" type="button" onclick="solicitarAlteracaoApiKey(${e.id})">Solicitar alteração</button></div><p><strong>Conta principal:</strong> abra Editar para alterar dados da empresa; funcionarios ficam no acesso da empresa.</p><button class="btn secondary" onclick="fecharDetalhe()">Fechar</button></div></td></tr>`;return `<tr><td colspan="5"><form class="inline-panel grid cols" onsubmit="salvarEdicao(event,${e.id})"><div class="field"><label>Nome</label><input name="nome" value="${esc(e.nome||'')}" required></div><div class="field"><label>CNPJ</label><input name="cnpj" value="${esc(e.cnpj||'')}"></div><div class="field"><label>Plano</label><select name="plano_id">${planoOptions(e.plano_id)}</select></div><div class="row"><button class="btn">Salvar</button><button type="button" class="btn secondary" onclick="fecharDetalhe()">Cancelar</button></div></form></td></tr>`}
|
||||
|
||||
function proximoSlugSequencial(){
|
||||
const slugsExistentes = new Set(empresas.map(e => String(e.slug || '').trim().toLowerCase()));
|
||||
@@ -67,10 +78,10 @@ function render(){
|
||||
const q=busca.value.trim();
|
||||
const lista=empresas.filter(e=>match(e,q));
|
||||
rows.innerHTML=lista.length?lista.map(e=>`<tr>
|
||||
<td>${e.slug}</td>
|
||||
<td>${e.nome}</td>
|
||||
<td>${esc(e.slug)}</td>
|
||||
<td>${esc(e.nome)}</td>
|
||||
<td>${statusAction(e)}</td>
|
||||
<td>${e.plano?`${e.plano} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td>
|
||||
<td>${e.plano?`${esc(e.plano)} ${e.plano_valor_centavos?'- '+Util.money(e.plano_valor_centavos):''}`:'-'}</td>
|
||||
<td style="text-align: right;">
|
||||
<div class="table-actions">
|
||||
<button class="table-icon-btn" type="button" onclick="editar(${e.id})" title="Editar cliente" aria-label="Editar cliente">${Icons.edit}</button>
|
||||
@@ -78,7 +89,7 @@ function render(){
|
||||
<button class="table-icon-btn danger" type="button" onclick="excluir(${e.id})" title="Excluir cliente" aria-label="Excluir cliente">${Icons.trash}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>${detalheRow(e)}`).join(''):'<tr><td colspan="5">Nenhum cliente encontrado.</td></tr>';
|
||||
</tr>`).join(''):'<tr><td colspan="5">Nenhum cliente encontrado.</td></tr>';
|
||||
}
|
||||
|
||||
async function load(){try{const [pe,de]=await Promise.all([Api.get('/api/master/planos'),Api.get('/api/master/empresas')]);planos=pe.planos||[];empresas=de.empresas||[];renderPlanos();render()}catch(e){Util.toast(e.message,true)}}
|
||||
@@ -191,13 +202,130 @@ empresaForm.onsubmit=async e=>{
|
||||
}
|
||||
};
|
||||
|
||||
busca.oninput=render;
|
||||
window.ver=id=>{selecionado=selecionado===id&&modo==='ver'?null:id;modo=selecionado?'ver':null;render()};
|
||||
window.editar=id=>{selecionado=selecionado===id&&modo==='editar'?null:id;modo=selecionado?'editar':null;render()};
|
||||
window.fecharDetalhe=()=>{selecionado=null;modo=null;render()};
|
||||
window.copiarApiKey=async id=>{const e=empresas.find(x=>Number(x.id)===Number(id));if(!e?.api_key)return;try{await navigator.clipboard.writeText(e.api_key);Util.toast('API Key copiada')}catch(_){Util.toast('Selecione e copie a API Key manualmente',true)}};
|
||||
window.solicitarAlteracaoApiKey=id=>{const e=empresas.find(x=>Number(x.id)===Number(id));Util.toast('Solicitacao de alteracao registrada para '+(e?.nome||'cliente'))};
|
||||
window.salvarEdicao=async(ev,id)=>{ev.preventDefault();try{await Api.put(`/api/master/empresas/${id}`,Util.formData(ev.target));Util.toast('Cliente atualizado');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}};
|
||||
window.alterarStatus=async(id,status)=>{try{await Api.post(`/api/master/empresas/${id}/status`,{status});Util.toast(status==='ativa'?'Cliente ativado':'Cliente bloqueado');load()}catch(err){Util.toast(err.message,true)}};
|
||||
window.excluir=async id=>{try{await Api.request(`/api/master/empresas/${id}`,{method:'DELETE'});Util.toast('Cliente excluido');selecionado=null;modo=null;load()}catch(err){Util.toast(err.message,true)}};
|
||||
busca.oninput = render;
|
||||
|
||||
// Visualização em Popup Modal
|
||||
window.ver = (id) => {
|
||||
const emp = empresas.find(x => Number(x.id) === Number(id));
|
||||
if (!emp) return;
|
||||
clienteVisualizandoId = emp.id;
|
||||
document.querySelector('#ver-modal-titulo').textContent = emp.nome || 'Detalhes do cliente';
|
||||
document.querySelector('#ver-slug').value = emp.slug || '-';
|
||||
document.querySelector('#ver-nome').value = emp.nome || '-';
|
||||
document.querySelector('#ver-cnpj').value = emp.cnpj || '-';
|
||||
document.querySelector('#ver-status').innerHTML = Util.badge(emp.status);
|
||||
document.querySelector('#ver-plano').value = emp.plano ? `${emp.plano} ${emp.plano_valor_centavos ? '- ' + Util.money(emp.plano_valor_centavos) : ''}` : 'Sem plano';
|
||||
document.querySelector('#ver-api-key').value = emp.api_key || '';
|
||||
verModal?.classList.remove('hidden');
|
||||
};
|
||||
|
||||
window.fecharModalVer = () => {
|
||||
verModal?.classList.add('hidden');
|
||||
clienteVisualizandoId = null;
|
||||
};
|
||||
|
||||
window.copiarApiKeyModal = async () => {
|
||||
const apiKey = document.querySelector('#ver-api-key')?.value;
|
||||
if (!apiKey) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(apiKey);
|
||||
Util.toast('API Key copiada com sucesso!');
|
||||
} catch (_) {
|
||||
Util.toast('Selecione e copie a API Key manualmente', true);
|
||||
}
|
||||
};
|
||||
|
||||
window.solicitarAlteracaoApiKeyModal = () => {
|
||||
const emp = empresas.find(x => Number(x.id) === Number(clienteVisualizandoId));
|
||||
Util.toast('Solicitação de alteração registrada para ' + (emp?.nome || 'cliente'));
|
||||
};
|
||||
|
||||
window.abrirEditarDeVer = () => {
|
||||
const id = clienteVisualizandoId;
|
||||
fecharModalVer();
|
||||
if (id) editar(id);
|
||||
};
|
||||
|
||||
// Edição em Popup Modal
|
||||
window.editar = (id) => {
|
||||
const emp = empresas.find(x => Number(x.id) === Number(id));
|
||||
if (!emp) return;
|
||||
clienteEditandoId = emp.id;
|
||||
editarForm.reset();
|
||||
const errEl = document.querySelector('#editar-modal-error-msg');
|
||||
if (errEl) { errEl.textContent = ''; errEl.classList.add('hidden'); }
|
||||
document.querySelector('#editar-modal-titulo').textContent = `Editar: ${emp.nome}`;
|
||||
document.querySelector('#editar-id').value = emp.id;
|
||||
document.querySelector('#editar-slug').value = emp.slug || '';
|
||||
document.querySelector('#editar-nome').value = emp.nome || '';
|
||||
document.querySelector('#editar-cnpj').value = emp.cnpj || '';
|
||||
document.querySelector('#editar-plano-select').innerHTML = planoOptions(emp.plano_id);
|
||||
editarModal?.classList.remove('hidden');
|
||||
document.querySelector('#editar-nome')?.focus();
|
||||
};
|
||||
|
||||
window.fecharModalEditar = () => {
|
||||
editarModal?.classList.add('hidden');
|
||||
clienteEditandoId = null;
|
||||
};
|
||||
|
||||
window.salvarEdicaoModal = async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (!clienteEditandoId) return;
|
||||
try {
|
||||
const dados = Util.formData(editarForm);
|
||||
await Api.put(`/api/master/empresas/${clienteEditandoId}`, dados);
|
||||
Util.toast('Cliente atualizado com sucesso!');
|
||||
fecharModalEditar();
|
||||
load();
|
||||
} catch (err) {
|
||||
const errEl = document.querySelector('#editar-modal-error-msg');
|
||||
if (errEl) {
|
||||
errEl.textContent = err.message || 'Erro ao atualizar cliente';
|
||||
errEl.classList.remove('hidden');
|
||||
}
|
||||
Util.toast(err.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Exclusão com confirmação em Popup Modal
|
||||
window.excluir = async (id) => {
|
||||
const emp = empresas.find(x => Number(x.id) === Number(id));
|
||||
const nome = emp ? ` "${emp.nome}"` : '';
|
||||
if (!await Util.confirm(`Tem certeza que deseja excluir o cliente${nome}?`, 'Excluir cliente')) return;
|
||||
try {
|
||||
await Api.request(`/api/master/empresas/${id}`, { method: 'DELETE' });
|
||||
Util.toast('Cliente excluído com sucesso');
|
||||
load();
|
||||
} catch (err) {
|
||||
Util.toast(err.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
window.copiarApiKey = async (id) => {
|
||||
const e = empresas.find(x => Number(x.id) === Number(id));
|
||||
if (!e?.api_key) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(e.api_key);
|
||||
Util.toast('API Key copiada');
|
||||
} catch (_) {
|
||||
Util.toast('Selecione e copie a API Key manualmente', true);
|
||||
}
|
||||
};
|
||||
|
||||
window.solicitarAlteracaoApiKey = (id) => {
|
||||
const e = empresas.find(x => Number(x.id) === Number(id));
|
||||
Util.toast('Solicitação de alteração registrada para ' + (e?.nome || 'cliente'));
|
||||
};
|
||||
|
||||
window.alterarStatus = async (id, status) => {
|
||||
try {
|
||||
await Api.post(`/api/master/empresas/${id}/status`, { status });
|
||||
Util.toast(status === 'ativa' ? 'Cliente ativado' : 'Cliente bloqueado');
|
||||
load();
|
||||
} catch (err) {
|
||||
Util.toast(err.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
Reference in New Issue
Block a user