From a824cb1d079e433764f8943832e62105a937ee35 Mon Sep 17 00:00:00 2001 From: godax84 Date: Thu, 19 Dec 2024 09:04:35 -0800 Subject: [PATCH] Carregar ficheiros para "/" --- README.md | 206 +++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ++ search_view.py | 184 +++++++++++++++++++++++++++++++++ statistics_view.py | 130 ++++++++++++++++++++++++ tag_manager.py | 246 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 772 insertions(+) create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 search_view.py create mode 100644 statistics_view.py create mode 100644 tag_manager.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..edbd849 --- /dev/null +++ b/README.md @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..45a00f3 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/search_view.py b/search_view.py new file mode 100644 index 0000000..eb41c62 --- /dev/null +++ b/search_view.py @@ -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() diff --git a/statistics_view.py b/statistics_view.py new file mode 100644 index 0000000..7fe1646 --- /dev/null +++ b/statistics_view.py @@ -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() diff --git a/tag_manager.py b/tag_manager.py new file mode 100644 index 0000000..28c8239 --- /dev/null +++ b/tag_manager.py @@ -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()