Carregar ficheiros para "/"
This commit is contained in:
206
README.md
Normal file
206
README.md
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
# Informação Técnica Automóvel
|
||||||
|
|
||||||
|
Um sistema desktop robusto para gerenciar documentos técnicos automotivos, desenvolvido com Python e PySide6.
|
||||||
|
|
||||||
|
## 🆕 Atualizações Recentes
|
||||||
|
|
||||||
|
### Dezembro 2024
|
||||||
|
- Renomeado o programa para "Informação Técnica Automóvel"
|
||||||
|
- Adicionada funcionalidade de abrir documentos com duplo clique
|
||||||
|
- Implementada busca avançada em Observações
|
||||||
|
- Melhorada a associação de documentos com pastas específicas
|
||||||
|
- Adicionado menu de contexto para documentos
|
||||||
|
- Corrigida a visualização de documentos nas pastas corretas
|
||||||
|
- Melhorada a interface de usuário com ordenação de colunas
|
||||||
|
- Adicionado destaque visual para resultados de busca em Observações
|
||||||
|
|
||||||
|
### Recursos Atualizados
|
||||||
|
- **Abertura de Documentos**:
|
||||||
|
- Duplo clique para abrir documentos
|
||||||
|
- Menu de contexto com opção "Abrir Documento"
|
||||||
|
- Abertura automática no aplicativo padrão do sistema
|
||||||
|
|
||||||
|
- **Busca Aprimorada**:
|
||||||
|
- Busca em todos os campos, incluindo Observações
|
||||||
|
- Destaque visual para termos encontrados
|
||||||
|
- Busca insensível a maiúsculas/minúsculas
|
||||||
|
- Suporte a busca parcial de palavras
|
||||||
|
|
||||||
|
- **Organização de Pastas**:
|
||||||
|
- Associação automática de documentos à pasta mais específica
|
||||||
|
- Estrutura hierárquica de pastas aprimorada
|
||||||
|
- Navegação intuitiva entre pastas
|
||||||
|
|
||||||
|
## 🚀 Funcionalidades
|
||||||
|
|
||||||
|
### 📁 Gerenciamento de Documentos
|
||||||
|
- Upload individual e em lote de documentos
|
||||||
|
- Organização em estrutura de pastas
|
||||||
|
- Suporte para diversos formatos de arquivo (PDF, imagens, etc.)
|
||||||
|
- Metadados detalhados (marca, modelo, ano, tipo, etc.)
|
||||||
|
- Visualização integrada de documentos
|
||||||
|
- Exportação e importação de documentos
|
||||||
|
|
||||||
|
### 🏷️ Organização e Categorização
|
||||||
|
- Sistema de tags coloridas
|
||||||
|
- Coleções personalizáveis
|
||||||
|
- Categorização por marca/modelo
|
||||||
|
- Estrutura hierárquica de pastas
|
||||||
|
- Filtros avançados
|
||||||
|
|
||||||
|
### 📊 Versionamento e Histórico
|
||||||
|
- Controle de versão de documentos
|
||||||
|
- Histórico de alterações
|
||||||
|
- Restauração de versões anteriores
|
||||||
|
- Comparação entre versões
|
||||||
|
- Registro de modificações
|
||||||
|
|
||||||
|
### 🔍 Busca e Visualização
|
||||||
|
- Busca avançada por metadados
|
||||||
|
- Visualização integrada de PDFs
|
||||||
|
- Pré-visualização de imagens
|
||||||
|
- Zoom e navegação de páginas
|
||||||
|
- Comparação visual de documentos
|
||||||
|
|
||||||
|
### 💬 Colaboração
|
||||||
|
- Sistema de comentários
|
||||||
|
- Anotações por página
|
||||||
|
- Tags compartilhadas
|
||||||
|
- Coleções colaborativas
|
||||||
|
- Exportação de relatórios
|
||||||
|
|
||||||
|
### 📈 Análise e Estatísticas
|
||||||
|
- Dashboard de estatísticas
|
||||||
|
- Gráficos de uso
|
||||||
|
- Relatórios personalizados
|
||||||
|
- Análise de espaço usado
|
||||||
|
- Métricas de documentos
|
||||||
|
|
||||||
|
### ⚙️ Recursos Avançados
|
||||||
|
- Backup automático
|
||||||
|
- Preferências personalizáveis
|
||||||
|
- Temas claro/escuro
|
||||||
|
- Documentos recentes
|
||||||
|
- Arrastar e soltar arquivos
|
||||||
|
|
||||||
|
## 🛠️ Tecnologias Utilizadas
|
||||||
|
|
||||||
|
- **Python**: Linguagem principal
|
||||||
|
- **PySide6**: Interface gráfica
|
||||||
|
- **SQLAlchemy**: ORM para banco de dados
|
||||||
|
- **PyMuPDF**: Manipulação de PDFs
|
||||||
|
- **OpenCV**: Processamento de imagens
|
||||||
|
- **NumPy**: Operações numéricas
|
||||||
|
- **Pillow**: Processamento de imagens
|
||||||
|
|
||||||
|
## 📋 Requisitos
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Dependências listadas em `requirements.txt`
|
||||||
|
|
||||||
|
## 🚀 Instalação
|
||||||
|
|
||||||
|
1. Clone o repositório:
|
||||||
|
```bash
|
||||||
|
git clone [URL_DO_REPOSITÓRIO]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Instale as dependências:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Execute o aplicativo:
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗂️ Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
gestao-documentos/
|
||||||
|
├── main.py # Ponto de entrada do aplicativo
|
||||||
|
├── models.py # Modelos do banco de dados
|
||||||
|
├── database.py # Configuração do banco de dados
|
||||||
|
├── document_form.py # Formulário de documentos
|
||||||
|
├── document_viewer.py # Visualizador de documentos
|
||||||
|
├── folder_view.py # Visualização de pastas
|
||||||
|
├── search_view.py # Interface de busca
|
||||||
|
├── version_control.py # Controle de versão
|
||||||
|
├── backup_manager.py # Gerenciamento de backup
|
||||||
|
├── tag_manager.py # Gerenciamento de tags
|
||||||
|
├── comments.py # Sistema de comentários
|
||||||
|
├── collections.py # Sistema de coleções
|
||||||
|
├── document_compare.py # Comparação de documentos
|
||||||
|
├── statistics_view.py # Visualização de estatísticas
|
||||||
|
├── preferences.py # Preferências do usuário
|
||||||
|
└── requirements.txt # Dependências do projeto
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Principais Recursos
|
||||||
|
|
||||||
|
### Sistema de Arquivos
|
||||||
|
- Estrutura hierárquica de pastas
|
||||||
|
- Suporte para múltiplos formatos
|
||||||
|
- Upload em lote
|
||||||
|
- Organização flexível
|
||||||
|
|
||||||
|
### Metadados
|
||||||
|
- Informações detalhadas do veículo
|
||||||
|
- Dados técnicos do documento
|
||||||
|
- Tags personalizáveis
|
||||||
|
- Histórico de modificações
|
||||||
|
|
||||||
|
### Versionamento
|
||||||
|
- Controle de versões
|
||||||
|
- Histórico de alterações
|
||||||
|
- Comparação entre versões
|
||||||
|
- Restauração de versões
|
||||||
|
|
||||||
|
### Busca
|
||||||
|
- Busca por metadados
|
||||||
|
- Filtros avançados
|
||||||
|
- Resultados categorizados
|
||||||
|
- Histórico de busca
|
||||||
|
|
||||||
|
### Visualização
|
||||||
|
- Visualizador integrado
|
||||||
|
- Zoom e navegação
|
||||||
|
- Anotações e marcações
|
||||||
|
- Comparação lado a lado
|
||||||
|
|
||||||
|
### Organização
|
||||||
|
- Sistema de tags
|
||||||
|
- Coleções personalizadas
|
||||||
|
- Categorização automática
|
||||||
|
- Filtros inteligentes
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- Exportação de documentos
|
||||||
|
- Backup incremental
|
||||||
|
- Restauração seletiva
|
||||||
|
- Verificação de integridade
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
- Design moderno
|
||||||
|
- Temas personalizáveis
|
||||||
|
- Atalhos de teclado
|
||||||
|
- Arrastar e soltar
|
||||||
|
|
||||||
|
## 🤝 Contribuição
|
||||||
|
|
||||||
|
1. Faça um Fork do projeto
|
||||||
|
2. Crie uma Branch para sua Feature (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push para a Branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Abra um Pull Request
|
||||||
|
|
||||||
|
## 📝 Licença
|
||||||
|
|
||||||
|
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para mais detalhes.
|
||||||
|
|
||||||
|
## 🎉 Agradecimentos
|
||||||
|
|
||||||
|
- Equipe de desenvolvimento
|
||||||
|
- Contribuidores
|
||||||
|
- Comunidade open source
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
PySide6>=6.5.0
|
||||||
|
SQLAlchemy>=2.0.0
|
||||||
|
PyMuPDF>=1.23.0
|
||||||
|
opencv-python>=4.8.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
Pillow>=10.0.0
|
||||||
184
search_view.py
Normal file
184
search_view.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||||
|
QLineEdit, QComboBox, QPushButton, QTableWidget,
|
||||||
|
QTableWidgetItem, QSpinBox, QHeaderView, QMenu,
|
||||||
|
QMessageBox)
|
||||||
|
from PySide6.QtCore import Qt, Signal
|
||||||
|
from database import get_session
|
||||||
|
from models import Document
|
||||||
|
import os
|
||||||
|
from sqlalchemy import or_
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
|
|
||||||
|
class SearchView(QWidget):
|
||||||
|
document_selected = Signal(int) # Signal emitted when document is selected
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Search form
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
|
||||||
|
# Search fields
|
||||||
|
self.marca_edit = QLineEdit()
|
||||||
|
self.modelo_edit = QLineEdit()
|
||||||
|
self.ano_spin = QSpinBox()
|
||||||
|
self.ano_spin.setRange(1900, 2100)
|
||||||
|
self.ano_spin.setSpecialValueText(" ") # Empty string for no filter
|
||||||
|
|
||||||
|
self.tipo_combo = QComboBox()
|
||||||
|
self.tipo_combo.addItems(["", "Elétrico", "Mecânico"])
|
||||||
|
|
||||||
|
self.variante_combo = QComboBox()
|
||||||
|
self.variante_combo.addItems([
|
||||||
|
"",
|
||||||
|
"Esquema",
|
||||||
|
"Esquema OEM",
|
||||||
|
"TSB",
|
||||||
|
"Esquemas Varios",
|
||||||
|
"Documentação de Instruções"
|
||||||
|
])
|
||||||
|
|
||||||
|
self.observacoes_edit = QLineEdit()
|
||||||
|
self.observacoes_edit.setPlaceholderText("Digite sua busca...")
|
||||||
|
|
||||||
|
form_layout.addRow("Marca:", self.marca_edit)
|
||||||
|
form_layout.addRow("Modelo:", self.modelo_edit)
|
||||||
|
form_layout.addRow("Ano:", self.ano_spin)
|
||||||
|
form_layout.addRow("Tipo:", self.tipo_combo)
|
||||||
|
form_layout.addRow("Variante:", self.variante_combo)
|
||||||
|
form_layout.addRow("Observações:", self.observacoes_edit)
|
||||||
|
|
||||||
|
# Search button
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.search_button = QPushButton("Pesquisar")
|
||||||
|
self.search_button.clicked.connect(self.perform_search)
|
||||||
|
self.clear_button = QPushButton("Limpar")
|
||||||
|
self.clear_button.clicked.connect(self.clear_search)
|
||||||
|
button_layout.addWidget(self.search_button)
|
||||||
|
button_layout.addWidget(self.clear_button)
|
||||||
|
|
||||||
|
# Add layouts to main layout
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
# Results table
|
||||||
|
self.results_table = QTableWidget()
|
||||||
|
self.results_table.setColumnCount(7)
|
||||||
|
self.results_table.setHorizontalHeaderLabels([
|
||||||
|
"Marca", "Modelo", "Ano", "Tipo", "Variante",
|
||||||
|
"Nome do Arquivo", "Observações"
|
||||||
|
])
|
||||||
|
self.results_table.horizontalHeader().setSectionResizeMode(
|
||||||
|
QHeaderView.ResizeToContents)
|
||||||
|
self.results_table.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self.results_table.customContextMenuRequested.connect(
|
||||||
|
self.show_context_menu)
|
||||||
|
layout.addWidget(self.results_table)
|
||||||
|
|
||||||
|
def perform_search(self):
|
||||||
|
session = get_session()
|
||||||
|
query = session.query(Document)
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if self.marca_edit.text():
|
||||||
|
query = query.filter(Document.marca.ilike(f"%{self.marca_edit.text()}%"))
|
||||||
|
|
||||||
|
if self.modelo_edit.text():
|
||||||
|
query = query.filter(Document.modelo.ilike(f"%{self.modelo_edit.text()}%"))
|
||||||
|
|
||||||
|
if self.ano_spin.value() != self.ano_spin.minimum():
|
||||||
|
query = query.filter(Document.ano == self.ano_spin.value())
|
||||||
|
|
||||||
|
if self.tipo_combo.currentText():
|
||||||
|
query = query.filter(Document.tipo_documento == self.tipo_combo.currentText())
|
||||||
|
|
||||||
|
if self.variante_combo.currentText():
|
||||||
|
query = query.filter(Document.variante == self.variante_combo.currentText())
|
||||||
|
|
||||||
|
if self.observacoes_edit.text():
|
||||||
|
query = query.filter(Document.observacoes.ilike(f"%{self.observacoes_edit.text()}%"))
|
||||||
|
|
||||||
|
# Get results
|
||||||
|
documents = query.all()
|
||||||
|
|
||||||
|
# Update table
|
||||||
|
valid_documents = []
|
||||||
|
for doc in documents:
|
||||||
|
if os.path.exists(doc.file_path):
|
||||||
|
valid_documents.append(doc)
|
||||||
|
else:
|
||||||
|
# Optional: Remove documents with missing files
|
||||||
|
session.delete(doc)
|
||||||
|
|
||||||
|
if len(documents) != len(valid_documents):
|
||||||
|
session.commit()
|
||||||
|
documents = valid_documents
|
||||||
|
|
||||||
|
self.results_table.setRowCount(len(documents))
|
||||||
|
for row, doc in enumerate(documents):
|
||||||
|
self.results_table.setItem(row, 0, QTableWidgetItem(doc.marca))
|
||||||
|
self.results_table.setItem(row, 1, QTableWidgetItem(doc.modelo))
|
||||||
|
self.results_table.setItem(row, 2, QTableWidgetItem(str(doc.ano)))
|
||||||
|
self.results_table.setItem(row, 3, QTableWidgetItem(doc.tipo_documento))
|
||||||
|
self.results_table.setItem(row, 4, QTableWidgetItem(doc.variante))
|
||||||
|
self.results_table.setItem(row, 5, QTableWidgetItem(doc.file_name))
|
||||||
|
self.results_table.setItem(row, 6, QTableWidgetItem(doc.observacoes))
|
||||||
|
|
||||||
|
# Store document ID in the first column
|
||||||
|
self.results_table.item(row, 0).setData(Qt.UserRole, doc.id)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def clear_search(self):
|
||||||
|
self.marca_edit.clear()
|
||||||
|
self.modelo_edit.clear()
|
||||||
|
self.ano_spin.setValue(self.ano_spin.minimum())
|
||||||
|
self.tipo_combo.setCurrentIndex(0)
|
||||||
|
self.variante_combo.setCurrentIndex(0)
|
||||||
|
self.observacoes_edit.clear()
|
||||||
|
self.results_table.setRowCount(0)
|
||||||
|
|
||||||
|
def show_context_menu(self, position):
|
||||||
|
menu = QMenu()
|
||||||
|
open_action = menu.addAction("Abrir Documento")
|
||||||
|
open_folder_action = menu.addAction("Abrir Pasta")
|
||||||
|
|
||||||
|
action = menu.exec_(self.results_table.mapToGlobal(position))
|
||||||
|
|
||||||
|
current_row = self.results_table.currentRow()
|
||||||
|
if current_row >= 0:
|
||||||
|
doc_id = self.results_table.item(current_row, 0).data(Qt.UserRole)
|
||||||
|
|
||||||
|
if action == open_action:
|
||||||
|
self.open_document(doc_id)
|
||||||
|
elif action == open_folder_action:
|
||||||
|
self.open_document_folder(doc_id)
|
||||||
|
|
||||||
|
def open_document(self, doc_id):
|
||||||
|
session = get_session()
|
||||||
|
document = session.query(Document).get(doc_id)
|
||||||
|
|
||||||
|
if document and os.path.exists(document.file_path):
|
||||||
|
self.document_selected.emit(doc_id)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Erro",
|
||||||
|
"Não foi possível abrir o documento.")
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def open_document_folder(self, doc_id):
|
||||||
|
session = get_session()
|
||||||
|
document = session.query(Document).get(doc_id)
|
||||||
|
|
||||||
|
if document and os.path.exists(document.file_path):
|
||||||
|
folder_path = os.path.dirname(document.file_path)
|
||||||
|
os.startfile(folder_path)
|
||||||
|
else:
|
||||||
|
QMessageBox.warning(self, "Erro",
|
||||||
|
"Não foi possível abrir a pasta do documento.")
|
||||||
|
|
||||||
|
session.close()
|
||||||
130
statistics_view.py
Normal file
130
statistics_view.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
|
QLabel, QTableWidget, QTableWidgetItem,
|
||||||
|
QHeaderView)
|
||||||
|
from PySide6.QtCore import Qt
|
||||||
|
from PySide6.QtCharts import QChart, QChartView, QPieSeries, QBarSeries, QBarSet
|
||||||
|
from PySide6.QtGui import QPainter
|
||||||
|
from database import get_session
|
||||||
|
from models import Document, Folder, Statistics
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class StatisticsView(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Refresh button
|
||||||
|
self.refresh_btn = QPushButton("Atualizar Estatísticas")
|
||||||
|
self.refresh_btn.clicked.connect(self.update_statistics)
|
||||||
|
layout.addWidget(self.refresh_btn)
|
||||||
|
|
||||||
|
# Summary layout
|
||||||
|
summary_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Total documents
|
||||||
|
self.total_docs_label = QLabel("Total de Documentos: 0")
|
||||||
|
summary_layout.addWidget(self.total_docs_label)
|
||||||
|
|
||||||
|
# Total folders
|
||||||
|
self.total_folders_label = QLabel("Total de Pastas: 0")
|
||||||
|
summary_layout.addWidget(self.total_folders_label)
|
||||||
|
|
||||||
|
# Storage used
|
||||||
|
self.storage_label = QLabel("Espaço Utilizado: 0 MB")
|
||||||
|
summary_layout.addWidget(self.storage_label)
|
||||||
|
|
||||||
|
layout.addLayout(summary_layout)
|
||||||
|
|
||||||
|
# Charts layout
|
||||||
|
charts_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
# Document types chart
|
||||||
|
self.types_chart = QChart()
|
||||||
|
self.types_chart.setTitle("Documentos por Tipo")
|
||||||
|
types_view = QChartView(self.types_chart)
|
||||||
|
types_view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
charts_layout.addWidget(types_view)
|
||||||
|
|
||||||
|
# Brands chart
|
||||||
|
self.brands_chart = QChart()
|
||||||
|
self.brands_chart.setTitle("Documentos por Marca")
|
||||||
|
brands_view = QChartView(self.brands_chart)
|
||||||
|
brands_view.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
charts_layout.addWidget(brands_view)
|
||||||
|
|
||||||
|
layout.addLayout(charts_layout)
|
||||||
|
|
||||||
|
self.update_statistics()
|
||||||
|
|
||||||
|
def update_statistics(self):
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Count documents and folders
|
||||||
|
total_docs = session.query(Document).count()
|
||||||
|
total_folders = session.query(Folder).count()
|
||||||
|
|
||||||
|
# Calculate storage used
|
||||||
|
storage_used = 0
|
||||||
|
for doc in session.query(Document).all():
|
||||||
|
if os.path.exists(doc.file_path):
|
||||||
|
storage_used += os.path.getsize(doc.file_path)
|
||||||
|
for version in doc.versions:
|
||||||
|
if os.path.exists(version.file_path):
|
||||||
|
storage_used += os.path.getsize(version.file_path)
|
||||||
|
|
||||||
|
# Update labels
|
||||||
|
self.total_docs_label.setText(f"Total de Documentos: {total_docs}")
|
||||||
|
self.total_folders_label.setText(f"Total de Pastas: {total_folders}")
|
||||||
|
self.storage_label.setText(
|
||||||
|
f"Espaço Utilizado: {storage_used / (1024*1024):.2f} MB")
|
||||||
|
|
||||||
|
# Document types statistics
|
||||||
|
types_series = QPieSeries()
|
||||||
|
types_count = {}
|
||||||
|
for doc in session.query(Document).all():
|
||||||
|
types_count[doc.tipo_documento] = types_count.get(
|
||||||
|
doc.tipo_documento, 0) + 1
|
||||||
|
|
||||||
|
for doc_type, count in types_count.items():
|
||||||
|
types_series.append(f"{doc_type} ({count})", count)
|
||||||
|
|
||||||
|
self.types_chart.removeAllSeries()
|
||||||
|
self.types_chart.addSeries(types_series)
|
||||||
|
|
||||||
|
# Brands statistics
|
||||||
|
brands_series = QBarSeries()
|
||||||
|
brands_count = {}
|
||||||
|
for doc in session.query(Document).all():
|
||||||
|
brands_count[doc.marca] = brands_count.get(doc.marca, 0) + 1
|
||||||
|
|
||||||
|
brands_set = QBarSet("Marcas")
|
||||||
|
brands = list(brands_count.keys())
|
||||||
|
values = [brands_count[brand] for brand in brands]
|
||||||
|
brands_set.append(values)
|
||||||
|
|
||||||
|
brands_series.append(brands_set)
|
||||||
|
self.brands_chart.removeAllSeries()
|
||||||
|
self.brands_chart.addSeries(brands_series)
|
||||||
|
|
||||||
|
# Save statistics
|
||||||
|
stats = Statistics(
|
||||||
|
date=datetime.utcnow(),
|
||||||
|
total_documents=total_docs,
|
||||||
|
total_folders=total_folders,
|
||||||
|
documents_by_type=json.dumps(types_count),
|
||||||
|
documents_by_brand=json.dumps(brands_count),
|
||||||
|
storage_used=storage_used
|
||||||
|
)
|
||||||
|
session.add(stats)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating statistics: {str(e)}")
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
246
tag_manager.py
Normal file
246
tag_manager.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
|
||||||
|
QTableWidget, QTableWidgetItem, QHeaderView,
|
||||||
|
QMessageBox, QInputDialog, QColorDialog, QLabel)
|
||||||
|
from PySide6.QtCore import Qt, Signal
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
|
from database import get_session
|
||||||
|
from models import Tag, Document
|
||||||
|
import json
|
||||||
|
|
||||||
|
class TagManager(QWidget):
|
||||||
|
tags_updated = Signal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
self.add_tag_btn = QPushButton("Nova Tag")
|
||||||
|
self.add_tag_btn.clicked.connect(self.add_tag)
|
||||||
|
self.edit_tag_btn = QPushButton("Editar Tag")
|
||||||
|
self.edit_tag_btn.clicked.connect(self.edit_tag)
|
||||||
|
self.delete_tag_btn = QPushButton("Excluir Tag")
|
||||||
|
self.delete_tag_btn.clicked.connect(self.delete_tag)
|
||||||
|
|
||||||
|
button_layout.addWidget(self.add_tag_btn)
|
||||||
|
button_layout.addWidget(self.edit_tag_btn)
|
||||||
|
button_layout.addWidget(self.delete_tag_btn)
|
||||||
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
|
# Tags table
|
||||||
|
self.tags_table = QTableWidget()
|
||||||
|
self.tags_table.setColumnCount(3)
|
||||||
|
self.tags_table.setHorizontalHeaderLabels([
|
||||||
|
"Nome", "Cor", "Documentos"
|
||||||
|
])
|
||||||
|
header = self.tags_table.horizontalHeader()
|
||||||
|
header.setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
layout.addWidget(self.tags_table)
|
||||||
|
|
||||||
|
self.refresh_tags()
|
||||||
|
|
||||||
|
def refresh_tags(self):
|
||||||
|
session = get_session()
|
||||||
|
tags = session.query(Tag).all()
|
||||||
|
|
||||||
|
self.tags_table.setRowCount(len(tags))
|
||||||
|
for row, tag in enumerate(tags):
|
||||||
|
# Name
|
||||||
|
self.tags_table.setItem(row, 0, QTableWidgetItem(tag.name))
|
||||||
|
|
||||||
|
# Color
|
||||||
|
color_item = QTableWidgetItem()
|
||||||
|
color_item.setBackground(QColor(tag.color))
|
||||||
|
self.tags_table.setItem(row, 1, color_item)
|
||||||
|
|
||||||
|
# Document count
|
||||||
|
doc_count = len(tag.documents)
|
||||||
|
self.tags_table.setItem(row, 2, QTableWidgetItem(str(doc_count)))
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def add_tag(self):
|
||||||
|
name, ok = QInputDialog.getText(
|
||||||
|
self,
|
||||||
|
"Nova Tag",
|
||||||
|
"Nome da tag:"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok and name:
|
||||||
|
color = QColorDialog.getColor()
|
||||||
|
if color.isValid():
|
||||||
|
try:
|
||||||
|
session = get_session()
|
||||||
|
|
||||||
|
# Check if tag already exists
|
||||||
|
if session.query(Tag).filter(Tag.name == name).first():
|
||||||
|
QMessageBox.warning(self, "Erro",
|
||||||
|
"Uma tag com este nome já existe.")
|
||||||
|
return
|
||||||
|
|
||||||
|
new_tag = Tag(
|
||||||
|
name=name,
|
||||||
|
color=color.name()
|
||||||
|
)
|
||||||
|
session.add(new_tag)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
self.refresh_tags()
|
||||||
|
self.tags_updated.emit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Erro",
|
||||||
|
f"Erro ao criar tag: {str(e)}")
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def edit_tag(self):
|
||||||
|
current_row = self.tags_table.currentRow()
|
||||||
|
if current_row < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_name = self.tags_table.item(current_row, 0).text()
|
||||||
|
new_name, ok = QInputDialog.getText(
|
||||||
|
self,
|
||||||
|
"Editar Tag",
|
||||||
|
"Nome da tag:",
|
||||||
|
text=current_name
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok and new_name:
|
||||||
|
color = QColorDialog.getColor()
|
||||||
|
if color.isValid():
|
||||||
|
try:
|
||||||
|
session = get_session()
|
||||||
|
tag = session.query(Tag).filter(Tag.name == current_name).first()
|
||||||
|
|
||||||
|
if tag:
|
||||||
|
tag.name = new_name
|
||||||
|
tag.color = color.name()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
self.refresh_tags()
|
||||||
|
self.tags_updated.emit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Erro",
|
||||||
|
f"Erro ao editar tag: {str(e)}")
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def delete_tag(self):
|
||||||
|
current_row = self.tags_table.currentRow()
|
||||||
|
if current_row < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
tag_name = self.tags_table.item(current_row, 0).text()
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Confirmar Exclusão",
|
||||||
|
f"Tem certeza que deseja excluir a tag '{tag_name}'?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No
|
||||||
|
)
|
||||||
|
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
try:
|
||||||
|
session = get_session()
|
||||||
|
tag = session.query(Tag).filter(Tag.name == tag_name).first()
|
||||||
|
|
||||||
|
if tag:
|
||||||
|
session.delete(tag)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
self.refresh_tags()
|
||||||
|
self.tags_updated.emit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Erro",
|
||||||
|
f"Erro ao excluir tag: {str(e)}")
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
class DocumentTags(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.current_document = None
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
self.tags_layout = QHBoxLayout()
|
||||||
|
layout.addLayout(self.tags_layout)
|
||||||
|
|
||||||
|
# Add tag button
|
||||||
|
self.add_tag_btn = QPushButton("Adicionar Tag")
|
||||||
|
self.add_tag_btn.clicked.connect(self.add_tag_to_document)
|
||||||
|
layout.addWidget(self.add_tag_btn)
|
||||||
|
|
||||||
|
def load_document(self, doc_id):
|
||||||
|
session = get_session()
|
||||||
|
self.current_document = session.query(Document).get(doc_id)
|
||||||
|
self.refresh_tags()
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def refresh_tags(self):
|
||||||
|
# Clear existing tags
|
||||||
|
for i in reversed(range(self.tags_layout.count())):
|
||||||
|
self.tags_layout.itemAt(i).widget().setParent(None)
|
||||||
|
|
||||||
|
if self.current_document:
|
||||||
|
session = get_session()
|
||||||
|
document = session.query(Document).get(self.current_document.id)
|
||||||
|
|
||||||
|
for tag in document.tags:
|
||||||
|
self.add_tag_label(tag)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def add_tag_label(self, tag):
|
||||||
|
label = QLabel(tag.name)
|
||||||
|
label.setStyleSheet(
|
||||||
|
f"background-color: {tag.color}; padding: 2px 5px; border-radius: 3px;"
|
||||||
|
)
|
||||||
|
self.tags_layout.addWidget(label)
|
||||||
|
|
||||||
|
def add_tag_to_document(self):
|
||||||
|
if not self.current_document:
|
||||||
|
return
|
||||||
|
|
||||||
|
session = get_session()
|
||||||
|
available_tags = session.query(Tag).all()
|
||||||
|
|
||||||
|
if not available_tags:
|
||||||
|
QMessageBox.warning(self, "Aviso",
|
||||||
|
"Não há tags disponíveis. Crie algumas tags primeiro.")
|
||||||
|
return
|
||||||
|
|
||||||
|
tag_names = [tag.name for tag in available_tags]
|
||||||
|
current_tags = [tag.name for tag in self.current_document.tags]
|
||||||
|
available_names = [n for n in tag_names if n not in current_tags]
|
||||||
|
|
||||||
|
if not available_names:
|
||||||
|
QMessageBox.information(self, "Aviso",
|
||||||
|
"Todas as tags já foram aplicadas a este documento.")
|
||||||
|
return
|
||||||
|
|
||||||
|
name, ok = QInputDialog.getItem(
|
||||||
|
self,
|
||||||
|
"Adicionar Tag",
|
||||||
|
"Selecione uma tag:",
|
||||||
|
available_names,
|
||||||
|
0,
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
if ok and name:
|
||||||
|
tag = session.query(Tag).filter(Tag.name == name).first()
|
||||||
|
document = session.query(Document).get(self.current_document.id)
|
||||||
|
document.tags.append(tag)
|
||||||
|
session.commit()
|
||||||
|
self.refresh_tags()
|
||||||
|
|
||||||
|
session.close()
|
||||||
Reference in New Issue
Block a user