feat: permitir edição de palavras-chave do centro de custo Geral (sistema)

This commit is contained in:
marco 2025-12-19 16:22:11 +01:00
parent b61ac7c7fa
commit 366254312c
4 changed files with 218 additions and 1 deletions

View File

@ -209,6 +209,49 @@ public function destroy(int $id): JsonResponse
]); ]);
} }
/**
* Atualizar palavras-chave de um centro de custo (incluindo de sistema)
*/
public function updateKeywords(Request $request, int $id): JsonResponse
{
$costCenter = CostCenter::where('user_id', Auth::id())->findOrFail($id);
$validated = $request->validate([
'keywords' => 'required|array',
'keywords.*' => 'string|max:100',
]);
DB::beginTransaction();
try {
// Remover antigas
$costCenter->keywords()->delete();
// Adicionar novas
foreach ($validated['keywords'] as $keyword) {
$costCenter->keywords()->create([
'keyword' => trim($keyword),
'is_case_sensitive' => false,
'is_active' => true,
]);
}
DB::commit();
return response()->json([
'success' => true,
'message' => 'Palavras-chave atualizadas com sucesso',
'data' => $costCenter->fresh()->load('keywords'),
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Erro ao atualizar palavras-chave: ' . $e->getMessage(),
], 500);
}
}
/** /**
* Adicionar palavra-chave a um centro de custo * Adicionar palavra-chave a um centro de custo
*/ */

View File

@ -94,6 +94,7 @@
// ============================================ // ============================================
Route::apiResource('cost-centers', CostCenterController::class); Route::apiResource('cost-centers', CostCenterController::class);
Route::post('cost-centers/{id}/keywords', [CostCenterController::class, 'addKeyword']); Route::post('cost-centers/{id}/keywords', [CostCenterController::class, 'addKeyword']);
Route::put('cost-centers/{id}/keywords', [CostCenterController::class, 'updateKeywords']);
Route::delete('cost-centers/{id}/keywords/{keywordId}', [CostCenterController::class, 'removeKeyword']); Route::delete('cost-centers/{id}/keywords/{keywordId}', [CostCenterController::class, 'removeKeyword']);
Route::post('cost-centers/match', [CostCenterController::class, 'matchByText']); Route::post('cost-centers/match', [CostCenterController::class, 'matchByText']);

View File

@ -12,9 +12,12 @@ const CostCenters = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showKeywordsModal, setShowKeywordsModal] = useState(false);
const [selectedItem, setSelectedItem] = useState(null); const [selectedItem, setSelectedItem] = useState(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [newKeyword, setNewKeyword] = useState(''); const [newKeyword, setNewKeyword] = useState('');
const [systemKeywords, setSystemKeywords] = useState([]);
const [newSystemKeyword, setNewSystemKeyword] = useState('');
const [openDropdownId, setOpenDropdownId] = useState(null); const [openDropdownId, setOpenDropdownId] = useState(null);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
@ -153,6 +156,58 @@ const CostCenters = () => {
} }
}; };
// === Funções para gerenciar keywords de centro de custo de sistema ===
const handleOpenKeywordsModal = (item) => {
setSelectedItem(item);
setSystemKeywords(item.keywords?.map(k => k.keyword) || []);
setNewSystemKeyword('');
setShowKeywordsModal(true);
};
const handleCloseKeywordsModal = () => {
setShowKeywordsModal(false);
setSelectedItem(null);
setSystemKeywords([]);
setNewSystemKeyword('');
};
const handleAddSystemKeyword = () => {
const keyword = newSystemKeyword.trim();
if (keyword && !systemKeywords.includes(keyword)) {
setSystemKeywords(prev => [...prev, keyword]);
setNewSystemKeyword('');
}
};
const handleRemoveSystemKeyword = (keyword) => {
setSystemKeywords(prev => prev.filter(k => k !== keyword));
};
const handleSystemKeywordKeyPress = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddSystemKeyword();
}
};
const handleSaveSystemKeywords = async () => {
if (!selectedItem) return;
setSaving(true);
try {
const response = await costCenterService.updateKeywords(selectedItem.id, systemKeywords);
if (response.success) {
toast.success(t('costCenters.keywordsUpdated') || 'Palavras-chave atualizadas com sucesso');
handleCloseKeywordsModal();
loadCostCenters();
}
} catch (error) {
toast.error(error.response?.data?.message || t('costCenters.keywordsError') || 'Erro ao atualizar palavras-chave');
} finally {
setSaving(false);
}
};
const handleDeleteClick = (item) => { const handleDeleteClick = (item) => {
setSelectedItem(item); setSelectedItem(item);
setShowDeleteModal(true); setShowDeleteModal(true);
@ -267,7 +322,16 @@ const CostCenters = () => {
)} )}
</div> </div>
</div> </div>
{!item.is_system && ( {item.is_system ? (
<button
className="btn btn-outline-warning btn-sm"
onClick={() => handleOpenKeywordsModal(item)}
title={t('costCenters.editKeywords') || 'Editar palavras-chave'}
>
<i className="bi bi-key me-1"></i>
{t('costCenters.keywords')}
</button>
) : (
<div className="dropdown" ref={openDropdownId === item.id ? dropdownRef : null} style={{ position: 'relative' }}> <div className="dropdown" ref={openDropdownId === item.id ? dropdownRef : null} style={{ position: 'relative' }}>
<button <button
className="btn btn-link text-slate-400 p-0" className="btn btn-link text-slate-400 p-0"
@ -614,6 +678,109 @@ const CostCenters = () => {
confirmText={t('common.delete')} confirmText={t('common.delete')}
loading={saving} loading={saving}
/> />
{/* Modal de Edição de Keywords para Centro de Custo de Sistema */}
{showKeywordsModal && selectedItem && (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content border-0" style={{ background: '#1e293b' }}>
<div className="modal-header border-0 pb-0">
<div>
<h5 className="modal-title text-white mb-1">
<i className="bi bi-key me-2 text-warning"></i>
{t('costCenters.editKeywords') || 'Editar Palavras-chave'}
</h5>
<p className="text-slate-400 mb-0 small">
{selectedItem.name}
</p>
</div>
<button type="button" className="btn-close btn-close-white" onClick={handleCloseKeywordsModal}></button>
</div>
<div className="modal-body pt-3">
<p className="text-slate-400 small mb-3">
<i className="bi bi-info-circle me-1"></i>
{t('costCenters.keywordsHelp') || 'Palavras-chave são usadas para atribuir automaticamente transações a este centro de custo.'}
</p>
<div className="p-3 rounded" style={{ background: '#0f172a' }}>
<div className="input-group mb-3">
<input
type="text"
className="form-control bg-dark text-white border-0"
style={{ background: '#1e293b' }}
value={newSystemKeyword}
onChange={(e) => setNewSystemKeyword(e.target.value)}
onKeyPress={handleSystemKeywordKeyPress}
placeholder={t('costCenters.keywordPlaceholder') || 'Digite e pressione Enter...'}
/>
<button
type="button"
className="btn btn-warning px-3"
onClick={handleAddSystemKeyword}
>
<i className="bi bi-plus-lg"></i>
</button>
</div>
<div className="d-flex flex-wrap gap-2" style={{ minHeight: '50px' }}>
{systemKeywords.map((keyword, index) => (
<span
key={index}
className="badge d-flex align-items-center py-2 px-3"
style={{
backgroundColor: selectedItem.color + '25',
color: selectedItem.color,
fontSize: '0.85rem'
}}
>
{keyword}
<button
type="button"
className="btn-close ms-2"
style={{ fontSize: '8px', filter: 'brightness(1.5)' }}
onClick={() => handleRemoveSystemKeyword(keyword)}
></button>
</span>
))}
{systemKeywords.length === 0 && (
<small className="text-slate-500 w-100 text-center py-3">
<i className="bi bi-inbox me-2"></i>
{t('costCenters.noKeywords') || 'Nenhuma palavra-chave definida'}
</small>
)}
</div>
</div>
</div>
<div className="modal-footer border-0">
<button type="button" className="btn btn-outline-secondary px-4" onClick={handleCloseKeywordsModal}>
<i className="bi bi-x-lg me-2"></i>
{t('common.cancel')}
</button>
<button
type="button"
className="btn btn-warning px-4"
onClick={handleSaveSystemKeywords}
disabled={saving}
>
{saving ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.saving')}
</>
) : (
<>
<i className="bi bi-check-lg me-2"></i>
{t('common.save')}
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -212,6 +212,12 @@ export const costCenterService = {
return response.data; return response.data;
}, },
// Atualizar todas as palavras-chave (inclui centros de custo de sistema)
updateKeywords: async (id, keywords) => {
const response = await api.put(`/cost-centers/${id}/keywords`, { keywords });
return response.data;
},
// Remover palavra-chave // Remover palavra-chave
removeKeyword: async (id, keywordId) => { removeKeyword: async (id, keywordId) => {
const response = await api.delete(`/cost-centers/${id}/keywords/${keywordId}`); const response = await api.delete(`/cost-centers/${id}/keywords/${keywordId}`);