feat: Redesign cost center modal + document modal pattern

- Completely redesigned Cost Center create/edit modal with elegant wizard-style UI
- Added preview card, visual settings section, keyword tags with auto-assign badge
- Added missing i18n translations for costCenters (namePlaceholder, descPlaceholder, etc.)
- Documented modal design pattern in copilot-instructions.md for future reference
- Pattern includes: colors, structure, labels, cards, tags, switch components
This commit is contained in:
marcoitaloesp-ai 2025-12-18 19:20:20 +00:00 committed by GitHub
parent 3ebb19e9c6
commit a90ff9d013
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 294 additions and 98 deletions

View File

@ -117,6 +117,115 @@ sshpass -p 'Master9354' ssh -o StrictHostKeyChecking=no root@213.165.93.60 "mysq
❌ NUNCA usar `ssh root@213.165.93.60` sem sshpass (vai travar esperando senha) ❌ NUNCA usar `ssh root@213.165.93.60` sem sshpass (vai travar esperando senha)
## 🎨 Padrão Visual de Modais
**TODOS os modais de formulário devem seguir este padrão elegante:**
### Estrutura Base
```jsx
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
{/* Header sem borda */}
<div className="modal-header border-0 pb-0">
<div>
<h5 className="modal-title text-white mb-1">
<i className="bi bi-icon me-2 text-primary"></i>
Título
</h5>
<p className="text-slate-400 mb-0 small">Subtítulo</p>
</div>
<button className="btn-close btn-close-white" onClick={onClose}></button>
</div>
{/* Body com scroll */}
<div className="modal-body pt-3" style={{ maxHeight: '65vh', overflowY: 'auto' }}>
{/* Preview Card - SEMPRE no topo */}
<div className="mb-4 p-3 rounded-3" style={{ background: '#0f172a' }}>
{/* Preview visual do item sendo criado/editado */}
</div>
{/* Campos em cards com background #0f172a */}
{/* Labels com ícones coloridos */}
{/* Badges "Opcional" quando necessário */}
</div>
{/* Footer sem borda */}
<div className="modal-footer border-0">
<button className="btn btn-outline-secondary px-4">Cancelar</button>
<button className="btn btn-primary px-4">Salvar</button>
</div>
</div>
</div>
</div>
```
### Cores do Sistema
- **Background modal**: `#1e293b`
- **Background campos/cards**: `#0f172a`
- **Texto principal**: `text-white`
- **Texto secundário**: `text-slate-400`
- **Texto desabilitado**: `text-slate-500`
### Labels com Ícones
```jsx
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-type me-2 text-primary"></i>
Nome *
</label>
```
### Badge Opcional
```jsx
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>
{t('common.optional')}
</span>
```
### Seleção Visual com Cards
Para seleções (categorias, ícones), usar cards clicáveis:
```jsx
<div
onClick={() => handleSelect(item)}
className="p-2 rounded text-center"
style={{
background: isSelected ? 'rgba(59, 130, 246, 0.15)' : '#0f172a',
cursor: 'pointer',
border: isSelected ? '2px solid #3b82f6' : '2px solid transparent'
}}
>
<i className={`bi ${icon} d-block mb-1`} style={{ color }}></i>
<small className="text-white">{label}</small>
</div>
```
### Seção de Keywords/Tags
```jsx
<div className="p-3 rounded" style={{ background: '#0f172a' }}>
<div className="input-group mb-2">
<input className="form-control bg-dark text-white border-0" />
<button className="btn btn-primary px-3">
<i className="bi bi-plus-lg"></i>
</button>
</div>
<div className="d-flex flex-wrap gap-2">
{/* Tags com cor do item */}
</div>
</div>
```
### Switch de Status
```jsx
<div className="form-check form-switch">
<input type="checkbox" className="form-check-input" role="switch" />
<label className="form-check-label text-white">
<i className={`bi ${isActive ? 'bi-check-circle text-success' : 'bi-x-circle text-secondary'} me-2`}></i>
{isActive ? 'Activo' : 'Inactivo'}
</label>
</div>
```
## Documentação ## Documentação
Consulte `.DIRETRIZES_DESENVOLVIMENTO_v5` para regras completas. Consulte `.DIRETRIZES_DESENVOLVIMENTO_v5` para regras completas.

View File

@ -412,8 +412,13 @@
"budget": "Budget", "budget": "Budget",
"keywords": "Keywords", "keywords": "Keywords",
"addKeyword": "Add Keyword", "addKeyword": "Add Keyword",
"keywordPlaceholder": "Type a keyword", "keywordPlaceholder": "Type and press Enter...",
"keywordHelp": "Keywords help automatically assign transactions", "keywordHelp": "E.g.: \"UBER\", \"iFood\" - Transactions with these words are auto-assigned",
"noKeywords": "No keywords. Transactions will be assigned manually.",
"namePlaceholder": "E.g.: Project Alpha, Marketing Dept...",
"descPlaceholder": "Describe the purpose of this cost center...",
"visualSettings": "Appearance",
"autoAssignLabel": "Auto-assign",
"createSuccess": "Cost center created successfully", "createSuccess": "Cost center created successfully",
"updateSuccess": "Cost center updated successfully", "updateSuccess": "Cost center updated successfully",
"deleteSuccess": "Cost center deleted successfully", "deleteSuccess": "Cost center deleted successfully",

View File

@ -415,8 +415,13 @@
"budget": "Presupuesto", "budget": "Presupuesto",
"keywords": "Palabras Clave", "keywords": "Palabras Clave",
"addKeyword": "Agregar Palabra Clave", "addKeyword": "Agregar Palabra Clave",
"keywordPlaceholder": "Escribe una palabra clave", "keywordPlaceholder": "Escribe y presiona Enter...",
"keywordHelp": "Las palabras clave ayudan a asignar transacciones automáticamente", "keywordHelp": "Ej: \"UBER\", \"iFood\" - Transacciones con estas palabras se asignan automáticamente",
"noKeywords": "Sin palabras clave. Las transacciones se asignarán manualmente.",
"namePlaceholder": "Ej: Proyecto Alpha, Dpto. Marketing...",
"descPlaceholder": "Describe el propósito de este centro de costo...",
"visualSettings": "Apariencia",
"autoAssignLabel": "Auto-asignación",
"createSuccess": "Centro de costo creado correctamente", "createSuccess": "Centro de costo creado correctamente",
"updateSuccess": "Centro de costo actualizado correctamente", "updateSuccess": "Centro de costo actualizado correctamente",
"deleteSuccess": "Centro de costo eliminado correctamente", "deleteSuccess": "Centro de costo eliminado correctamente",

View File

@ -417,8 +417,13 @@
"budget": "Orçamento", "budget": "Orçamento",
"keywords": "Palavras-chave", "keywords": "Palavras-chave",
"addKeyword": "Adicionar Palavra-chave", "addKeyword": "Adicionar Palavra-chave",
"keywordPlaceholder": "Digite uma palavra-chave", "keywordPlaceholder": "Digite e pressione Enter...",
"keywordHelp": "Palavras-chave ajudam a atribuir transações automaticamente", "keywordHelp": "Ex: \"UBER\", \"iFood\" - Transações com essas palavras são atribuídas automaticamente",
"noKeywords": "Sem palavras-chave. Transações serão atribuídas manualmente.",
"namePlaceholder": "Ex: Projeto Alpha, Dpto. Marketing...",
"descPlaceholder": "Descreva o propósito deste centro de custo...",
"visualSettings": "Aparência",
"autoAssignLabel": "Auto-atribuição",
"createSuccess": "Centro de custo criado com sucesso", "createSuccess": "Centro de custo criado com sucesso",
"updateSuccess": "Centro de custo atualizado com sucesso", "updateSuccess": "Centro de custo atualizado com sucesso",
"deleteSuccess": "Centro de custo excluído com sucesso", "deleteSuccess": "Centro de custo excluído com sucesso",

View File

@ -357,75 +357,211 @@ const CostCenters = () => {
</div> </div>
)} )}
{/* Modal de Criar/Editar */} {/* Modal de Criar/Editar - Design Elegante */}
{showModal && ( {showModal && (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.7)' }}> <div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-lg modal-dialog-centered"> <div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content" style={{ background: '#1e293b' }}> <div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
<div className="modal-header border-bottom" style={{ borderColor: '#334155 !important' }}> {/* Header elegante */}
<h5 className="modal-title text-white"> <div className="modal-header border-0 pb-0">
<i className={`bi ${selectedItem ? 'bi-pencil' : 'bi-plus-circle'} me-2`}></i> <div>
<h5 className="modal-title text-white mb-1">
<i className={`bi ${selectedItem ? 'bi-pencil-square' : 'bi-plus-circle-dotted'} me-2 text-success`}></i>
{selectedItem ? t('costCenters.editCostCenter') : t('costCenters.newCostCenter')} {selectedItem ? t('costCenters.editCostCenter') : t('costCenters.newCostCenter')}
</h5> </h5>
<p className="text-slate-400 mb-0 small">
{t('costCenters.title')}
</p>
</div>
<button type="button" className="btn-close btn-close-white" onClick={handleCloseModal}></button> <button type="button" className="btn-close btn-close-white" onClick={handleCloseModal}></button>
</div> </div>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="modal-body"> <div className="modal-body pt-3" style={{ maxHeight: '65vh', overflowY: 'auto' }}>
<div className="row g-3">
{/* Nome */} {/* Preview Card */}
<div className="mb-4 p-3 rounded-3" style={{ background: '#0f172a' }}>
<div className="d-flex align-items-center">
<div
className="rounded-circle d-flex align-items-center justify-content-center me-3"
style={{
width: 50,
height: 50,
background: `${formData.color}25`,
border: `2px solid ${formData.color}`,
}}
>
<i className={`bi ${formData.icon}`} style={{ fontSize: '1.3rem', color: formData.color }}></i>
</div>
<div>
<h6 className="text-white mb-0">{formData.name || t('costCenters.newCostCenter')}</h6>
<small className="text-slate-400">
{formData.code ? `${t('costCenters.code')}: ${formData.code}` : t('common.description')}
</small>
</div>
<div className="ms-auto">
{formData.is_active ? (
<span className="badge bg-success bg-opacity-25 text-success">{t('common.active')}</span>
) : (
<span className="badge bg-secondary bg-opacity-25 text-secondary">{t('common.inactive')}</span>
)}
</div>
</div>
</div>
{/* Nome e Código - Linha principal */}
<div className="row g-3 mb-4">
<div className="col-md-8"> <div className="col-md-8">
<label className="form-label text-slate-300">{t('common.name')} *</label> <label className="form-label text-white fw-medium mb-2">
<i className="bi bi-type me-2 text-primary"></i>
{t('common.name')} *
</label>
<input <input
type="text" type="text"
className="form-control bg-dark text-white border-secondary" className="form-control bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
placeholder="Ex: Projeto Alpha, Departamento RH..." placeholder={t('costCenters.namePlaceholder') || 'Ej: Proyecto Alpha, Dpto. Marketing...'}
required required
autoFocus
/> />
</div> </div>
{/* Código */}
<div className="col-md-4"> <div className="col-md-4">
<label className="form-label text-slate-300">{t('costCenters.code')}</label> <label className="form-label text-white fw-medium mb-2">
<i className="bi bi-hash me-2 text-warning"></i>
{t('costCenters.code')}
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
</label>
<input <input
type="text" type="text"
className="form-control bg-dark text-white border-secondary" className="form-control bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="code" name="code"
value={formData.code} value={formData.code}
onChange={handleChange} onChange={handleChange}
placeholder="Ex: CC001" placeholder="CC001"
maxLength="20" maxLength="20"
/> />
</div> </div>
</div>
{/* Cor */} {/* Visual - Cor e Ícone */}
<div className="col-md-3"> <div className="mb-4">
<label className="form-label text-slate-300">{t('common.color')}</label> <label className="form-label text-white fw-medium mb-2">
<i className="bi bi-palette me-2 text-success"></i>
{t('costCenters.visualSettings') || t('categories.visualSettings') || 'Apariencia'}
</label>
<div className="row g-3">
<div className="col-4">
<div className="p-3 rounded text-center" style={{ background: '#0f172a' }}>
<label className="text-slate-400 small d-block mb-2">{t('common.color')}</label>
<input <input
type="color" type="color"
className="form-control form-control-color bg-dark border-secondary w-100" className="form-control form-control-color mx-auto border-0"
style={{ width: 50, height: 50, cursor: 'pointer', background: 'transparent' }}
name="color" name="color"
value={formData.color} value={formData.color}
onChange={handleChange} onChange={handleChange}
/> />
</div> </div>
</div>
{/* Ícone */} <div className="col-8">
<div className="col-md-5"> <div className="p-3 rounded h-100" style={{ background: '#0f172a' }}>
<label className="form-label text-slate-300">{t('common.icon')}</label> <label className="text-slate-400 small d-block mb-2">{t('common.icon')}</label>
<IconSelector <IconSelector
value={formData.icon} value={formData.icon}
onChange={(icon) => setFormData(prev => ({ ...prev, icon }))} onChange={(icon) => setFormData(prev => ({ ...prev, icon }))}
type="costCenter" type="costCenter"
/> />
</div> </div>
</div>
</div>
</div>
{/* Descrição */}
<div className="mb-4">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-text-paragraph me-2 text-secondary"></i>
{t('common.description')}
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
</label>
<textarea
className="form-control bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="description"
value={formData.description}
onChange={handleChange}
rows="2"
placeholder={t('costCenters.descPlaceholder') || 'Describe el propósito de este centro de costo...'}
></textarea>
</div>
{/* Palavras-chave - Seção destacada */}
<div className="mb-3">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-key me-2 text-warning"></i>
{t('costCenters.keywords')}
<span className="badge bg-warning text-dark ms-2" style={{ fontSize: '0.65rem' }}>
{t('costCenters.autoAssignLabel') || 'Auto-asignación'}
</span>
</label>
<div className="p-3 rounded" style={{ background: '#0f172a' }}>
<div className="input-group mb-2">
<input
type="text"
className="form-control bg-dark text-white border-0"
style={{ background: '#1e293b' }}
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
onKeyPress={handleKeywordKeyPress}
placeholder={t('costCenters.keywordPlaceholder') || 'Escribe y presiona Enter...'}
/>
<button
type="button"
className="btn btn-success px-3"
onClick={handleAddKeyword}
>
<i className="bi bi-plus-lg"></i>
</button>
</div>
<div className="d-flex flex-wrap gap-2">
{formData.keywords.map((keyword, index) => (
<span
key={index}
className="badge d-flex align-items-center py-2 px-3"
style={{
backgroundColor: formData.color + '25',
color: formData.color,
fontSize: '0.85rem'
}}
>
{keyword}
<button
type="button"
className="btn-close ms-2"
style={{ fontSize: '8px', filter: 'brightness(1.5)' }}
onClick={() => handleRemoveKeyword(keyword)}
></button>
</span>
))}
{formData.keywords.length === 0 && (
<small className="text-slate-500">
<i className="bi bi-info-circle me-1"></i>
{t('costCenters.noKeywords') || 'Sin palabras clave. Las transacciones se asignarán manualmente.'}
</small>
)}
</div>
</div>
<small className="text-slate-500 mt-2 d-block">
<i className="bi bi-lightbulb me-1"></i>
{t('costCenters.keywordHelp')}
</small>
</div>
{/* Status */} {/* Status */}
<div className="col-md-4"> <div className="form-check form-switch">
<label className="form-label text-slate-300">&nbsp;</label>
<div className="form-check mt-2">
<input <input
type="checkbox" type="checkbox"
className="form-check-input" className="form-check-input"
@ -433,94 +569,30 @@ const CostCenters = () => {
name="is_active" name="is_active"
checked={formData.is_active} checked={formData.is_active}
onChange={handleChange} onChange={handleChange}
role="switch"
/> />
<label className="form-check-label text-slate-300" htmlFor="is_active"> <label className="form-check-label text-white" htmlFor="is_active">
{t('common.active')} <i className={`bi ${formData.is_active ? 'bi-check-circle text-success' : 'bi-x-circle text-secondary'} me-2`}></i>
{formData.is_active ? t('common.active') : t('common.inactive')}
</label> </label>
</div> </div>
</div> </div>
{/* Descrição */} {/* Footer elegante */}
<div className="col-12"> <div className="modal-footer border-0">
<label className="form-label text-slate-300">{t('common.description')}</label> <button type="button" className="btn btn-outline-secondary px-4" onClick={handleCloseModal}>
<textarea <i className="bi bi-x-lg me-2"></i>
className="form-control bg-dark text-white border-secondary"
name="description"
value={formData.description}
onChange={handleChange}
rows="2"
placeholder="Descreva o propósito deste centro de custo..."
></textarea>
</div>
{/* Palavras-chave */}
<div className="col-12">
<label className="form-label text-slate-300">
<i className="bi bi-key me-1"></i>
{t('costCenters.keywordHelp')}
</label>
<div className="input-group mb-2">
<input
type="text"
className="form-control bg-dark text-white border-secondary"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
onKeyPress={handleKeywordKeyPress}
placeholder="Digite uma palavra-chave e pressione Enter..."
/>
<button
type="button"
className="btn btn-outline-success"
onClick={handleAddKeyword}
>
<i className="bi bi-plus"></i>
</button>
</div>
<div className="d-flex flex-wrap gap-2">
{formData.keywords.map((keyword, index) => (
<span
key={index}
className="badge d-flex align-items-center"
style={{
backgroundColor: formData.color + '25',
color: formData.color,
}}
>
{keyword}
<button
type="button"
className="btn-close btn-close-white ms-2"
style={{ fontSize: '8px' }}
onClick={() => handleRemoveKeyword(keyword)}
></button>
</span>
))}
{formData.keywords.length === 0 && (
<small className="text-slate-500">
{t('common.noData')}
</small>
)}
</div>
<small className="text-slate-500 mt-2 d-block">
Ex: "UBER", "iFood", "Supermercado XYZ" - Quando estas palavras aparecerem na
descrição de uma transação, este centro de custo será sugerido automaticamente.
</small>
</div>
</div>
</div>
<div className="modal-footer border-top" style={{ borderColor: '#334155 !important' }}>
<button type="button" className="btn btn-outline-light" onClick={handleCloseModal}>
{t('common.cancel')} {t('common.cancel')}
</button> </button>
<button type="submit" className="btn btn-success" disabled={saving}> <button type="submit" className="btn btn-success px-4" disabled={saving || !formData.name.trim()}>
{saving ? ( {saving ? (
<> <>
<span className="spinner-border spinner-border-sm me-2"></span> <span className="spinner-border spinner-border-sm me-2"></span>
{t('common.loading')} {t('common.saving')}
</> </>
) : ( ) : (
<> <>
<i className="bi bi-check-lg me-2"></i> <i className={`bi ${selectedItem ? 'bi-check-lg' : 'bi-plus-lg'} me-2`}></i>
{selectedItem ? t('common.save') : t('common.create')} {selectedItem ? t('common.save') : t('common.create')}
</> </>
)} )}