diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f1d770 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Gestão de Documentos Automotivos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/init_database.py b/init_database.py new file mode 100644 index 0000000..6c1bd86 --- /dev/null +++ b/init_database.py @@ -0,0 +1,21 @@ +import os +from database import init_db, get_session +from models import Folder + +def initialize_database(): + # Remove existing database if it exists + if os.path.exists("automotive_docs.db"): + os.remove("automotive_docs.db") + + # Create new database + engine = init_db() + + # Create initial root folder + session = get_session() + root = Folder(name="Root") + session.add(root) + session.commit() + +if __name__ == "__main__": + initialize_database() + print("Database initialized successfully!") diff --git a/main.py b/main.py new file mode 100644 index 0000000..201d3b5 --- /dev/null +++ b/main.py @@ -0,0 +1,298 @@ +import sys +from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QMenuBar, QStackedWidget, QSplitter, QMessageBox, QTableWidgetItem, QTabWidget +from PySide6.QtCore import Qt, QMimeData +from PySide6.QtGui import QDragEnterEvent, QDropEvent +from database import init_db +from folder_view import FolderTreeView +from document_form import DocumentForm +from search_view import SearchView +from document_viewer import DocumentViewer +from batch_upload import BatchUploadWidget +from document_list import DocumentListView +from version_control import VersionControl +from backup_manager import BackupManager +from tag_manager import TagManager, DocumentTags +from statistics_view import StatisticsView +from preferences import PreferencesDialog, Preferences, RecentDocuments +from document_compare import DocumentCompare +from document_collections import CollectionsWidget, CollectionDocuments +import os + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Informação Técnica Automóvel") + self.setMinimumSize(1200, 800) + self.setAcceptDrops(True) + self.current_folder_id = None + + # Create central widget and layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout(central_widget) + + # Create splitter for main layout + main_splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(main_splitter) + + # Left panel with folder tree and collections + left_panel = QTabWidget() + + # Folder tree + self.folder_view = FolderTreeView() + self.folder_view.folder_selected.connect(self.on_folder_selected) + left_panel.addTab(self.folder_view, "Pastas") + + # Collections + self.collections_widget = CollectionsWidget() + self.collections_widget.collection_selected.connect(self.show_collection) + left_panel.addTab(self.collections_widget, "Coleções") + + main_splitter.addWidget(left_panel) + + # Center panel with document list and collection documents + self.center_panel = QStackedWidget() + + # Document list + self.doc_list = DocumentListView() + self.doc_list.document_selected.connect(self.show_document) + self.center_panel.addWidget(self.doc_list) + + # Collection documents + self.collection_docs = CollectionDocuments() + self.collection_docs.document_selected.connect(self.show_document) + self.center_panel.addWidget(self.collection_docs) + + main_splitter.addWidget(self.center_panel) + + # Right panel with operations + self.right_panel = QStackedWidget() + + # Add document form + self.document_form = DocumentForm() + self.right_panel.addWidget(self.document_form) + + # Add batch upload widget + self.batch_upload = BatchUploadWidget() + self.batch_upload.upload_completed.connect(self.refresh_views) + self.right_panel.addWidget(self.batch_upload) + + # Add search view + self.search_view = SearchView() + self.search_view.document_selected.connect(self.show_document) + self.right_panel.addWidget(self.search_view) + + # Add document viewer with tags + viewer_widget = QWidget() + viewer_layout = QVBoxLayout(viewer_widget) + self.document_viewer = DocumentViewer() + viewer_layout.addWidget(self.document_viewer) + self.document_tags = DocumentTags() + viewer_layout.addWidget(self.document_tags) + self.right_panel.addWidget(viewer_widget) + + # Add version control + self.version_control = VersionControl() + self.right_panel.addWidget(self.version_control) + + # Add backup manager + self.backup_manager = BackupManager() + self.right_panel.addWidget(self.backup_manager) + + # Add tag manager + self.tag_manager = TagManager() + self.tag_manager.tags_updated.connect(self.refresh_views) + self.right_panel.addWidget(self.tag_manager) + + # Add statistics view + self.statistics_view = StatisticsView() + self.right_panel.addWidget(self.statistics_view) + + # Add document compare + self.document_compare = DocumentCompare() + self.right_panel.addWidget(self.document_compare) + + main_splitter.addWidget(self.right_panel) + + # Set splitter proportions + main_splitter.setSizes([ + int(self.width() * 0.2), # Left panel + int(self.width() * 0.3), # Center panel + int(self.width() * 0.5) # Right panel + ]) + + # Create menu bar + self.create_menu_bar() + + def create_menu_bar(self): + menubar = self.menuBar() + + # File menu + file_menu = menubar.addMenu("Arquivo") + new_folder_action = file_menu.addAction("Nova Pasta") + new_folder_action.triggered.connect(self.folder_view.add_folder) + + upload_action = file_menu.addAction("Upload Documento") + upload_action.triggered.connect(self.show_upload_form) + + batch_upload_action = file_menu.addAction("Upload em Lote") + batch_upload_action.triggered.connect(self.show_batch_upload) + + file_menu.addSeparator() + + # Recent documents submenu + self.recent_menu = file_menu.addMenu("Documentos Recentes") + self.recent_menu.aboutToShow.connect(self.update_recent_menu) + + file_menu.addSeparator() + backup_menu = file_menu.addMenu("Backup") + backup_menu.addAction("Exportar").triggered.connect( + lambda: self.right_panel.setCurrentWidget(self.backup_manager)) + + file_menu.addSeparator() + exit_action = file_menu.addAction("Sair") + exit_action.triggered.connect(self.close) + + # Search menu + search_menu = menubar.addMenu("Pesquisar") + advanced_search_action = search_menu.addAction("Busca Avançada") + advanced_search_action.triggered.connect(self.show_search) + + # Tools menu + tools_menu = menubar.addMenu("Ferramentas") + tools_menu.addAction("Gerenciar Tags").triggered.connect( + lambda: self.right_panel.setCurrentWidget(self.tag_manager)) + tools_menu.addAction("Comparar Documentos").triggered.connect( + lambda: self.right_panel.setCurrentWidget(self.document_compare)) + tools_menu.addAction("Estatísticas").triggered.connect( + lambda: self.right_panel.setCurrentWidget(self.statistics_view)) + + # Settings menu + settings_menu = menubar.addMenu("Configurações") + preferences_action = settings_menu.addAction("Preferências") + preferences_action.triggered.connect(self.show_preferences) + + def dragEnterEvent(self, event: QDragEnterEvent): + if event.mimeData().hasUrls(): + event.acceptProposedAction() + + def dropEvent(self, event: QDropEvent): + files = [url.toLocalFile() for url in event.mimeData().urls()] + if files: + current_index = self.folder_view.currentIndex() + if current_index.isValid(): + folder_id = self.folder_view.model.itemFromIndex(current_index).data() + self.batch_upload.folder_id = folder_id + + # Add files to batch upload + for file in files: + row = self.batch_upload.files_table.rowCount() + self.batch_upload.files_table.insertRow(row) + self.batch_upload.files_table.setItem( + row, 0, QTableWidgetItem(os.path.basename(file))) + self.batch_upload.files_table.setItem( + row, 1, QTableWidgetItem(file)) + + self.right_panel.setCurrentWidget(self.batch_upload) + else: + QMessageBox.warning( + self, + "Aviso", + "Selecione uma pasta antes de arrastar arquivos." + ) + + def show_upload_form(self): + print(f"Showing document form for folder ID: {self.current_folder_id}") + self.document_form = DocumentForm(folder_id=self.current_folder_id) + self.right_panel.addWidget(self.document_form) + self.right_panel.setCurrentWidget(self.document_form) + + def show_batch_upload(self): + current_index = self.folder_view.currentIndex() + folder_id = None + if current_index.isValid(): + folder_id = self.folder_view.model.itemFromIndex(current_index).data() + self.batch_upload.folder_id = folder_id + self.right_panel.setCurrentWidget(self.batch_upload) + + def show_search(self): + self.right_panel.setCurrentWidget(self.search_view) + + def show_document(self, doc_id): + self.document_viewer.load_document(doc_id) + self.document_tags.load_document(doc_id) + self.version_control.load_document(doc_id) + self.right_panel.setCurrentWidget(self.version_control) + + def on_folder_selected(self, folder_id): + """Handle folder selection""" + print(f"Main window received folder selection: ID={folder_id}") + self.current_folder_id = folder_id + self.doc_list.set_folder(folder_id) + self.center_panel.setCurrentWidget(self.doc_list) + + # Update document form with current folder + self.document_form.folder_id = folder_id + print(f"Updated document form folder ID to: {folder_id}") + + def refresh_views(self): + self.doc_list.refresh_documents() + self.collections_widget.refresh_collections() + self.collection_docs.refresh_documents() + self.document_compare.refresh_documents() + + def show_preferences(self): + dialog = PreferencesDialog(Preferences(), self) + dialog.show() + + def update_recent_menu(self): + self.recent_menu.clear() + + recent = RecentDocuments(Preferences()) + documents = recent.get_recent_docs() + + if documents: + for doc in documents: + action = self.recent_menu.addAction(doc['name']) + action.triggered.connect( + lambda checked, doc_id=doc['id']: self.show_document(doc_id) + ) + + self.recent_menu.addSeparator() + clear_action = self.recent_menu.addAction("Limpar Lista") + clear_action.triggered.connect(self.clear_recent_documents) + else: + no_docs_action = self.recent_menu.addAction("Nenhum documento recente") + no_docs_action.setEnabled(False) + + def clear_recent_documents(self): + recent = RecentDocuments(Preferences()) + recent.clear_recent_docs() + self.update_recent_menu() + + def show_collection(self, collection_id): + self.collection_docs.load_collection(collection_id) + self.center_panel.setCurrentWidget(self.collection_docs) + +def main(): + # Initialize database + engine = init_db() + + # Start application + app = QApplication(sys.argv) + + # Ensure preferences are initialized + preferences = Preferences() + if not preferences.get_save_path(): + # Ask for initial save path if not set + default_docs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "documentos") + preferences.set("default_save_path", default_docs_path) + if not os.path.exists(default_docs_path): + os.makedirs(default_docs_path) + + window = MainWindow() + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/models.py b/models.py new file mode 100644 index 0000000..cc90c4a --- /dev/null +++ b/models.py @@ -0,0 +1,111 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table, Text +from sqlalchemy.orm import relationship +from sqlalchemy.ext.declarative import declarative_base +import datetime + +Base = declarative_base() + +# Association table for document tags +document_tags = Table('document_tags', Base.metadata, + Column('document_id', Integer, ForeignKey('documents.id')), + Column('tag_id', Integer, ForeignKey('tags.id')) +) + +# Association table for document collections +document_collections = Table('document_collections', Base.metadata, + Column('document_id', Integer, ForeignKey('documents.id')), + Column('collection_id', Integer, ForeignKey('collections.id')) +) + +class Document(Base): + __tablename__ = 'documents' + + id = Column(Integer, primary_key=True) + file_path = Column(String(500), nullable=False) + file_name = Column(String(255), nullable=False) + marca = Column(String(100), nullable=False) + modelo = Column(String(100), nullable=False) + ano = Column(Integer, nullable=False) + cilindrada = Column(String(50)) + codigo_motor = Column(String(100)) + tipo_documento = Column(String(50), nullable=False) # Elétrico/Mecânico + variante = Column(String(50), nullable=False) # Esquema, Esquema OEM, etc. + observacoes = Column(Text) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + folder_id = Column(Integer, ForeignKey('folders.id')) + + folder = relationship("Folder", back_populates="documents") + versions = relationship("DocumentVersion", back_populates="document", cascade="all, delete-orphan") + tags = relationship("Tag", secondary=document_tags, back_populates="documents") + comments = relationship("Comment", back_populates="document", cascade="all, delete-orphan") + collections = relationship("Collection", secondary=document_collections, back_populates="documents") + +class DocumentVersion(Base): + __tablename__ = 'document_versions' + + id = Column(Integer, primary_key=True) + document_id = Column(Integer, ForeignKey('documents.id')) + version_number = Column(Integer, nullable=False) + file_path = Column(String(500), nullable=False) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + changes = Column(Text) + + document = relationship("Document", back_populates="versions") + +class Folder(Base): + __tablename__ = 'folders' + + id = Column(Integer, primary_key=True) + name = Column(String(100), nullable=False) + parent_id = Column(Integer, ForeignKey('folders.id')) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + + parent = relationship("Folder", remote_side=[id], back_populates="children") + children = relationship("Folder", back_populates="parent") + documents = relationship("Document", back_populates="folder") + +class Tag(Base): + __tablename__ = 'tags' + + id = Column(Integer, primary_key=True) + name = Column(String(50), nullable=False, unique=True) + color = Column(String(7), default="#808080") # Hex color code + created_at = Column(DateTime, default=datetime.datetime.utcnow) + + documents = relationship("Document", secondary=document_tags, back_populates="tags") + +class Comment(Base): + __tablename__ = 'comments' + + id = Column(Integer, primary_key=True) + document_id = Column(Integer, ForeignKey('documents.id')) + text = Column(Text, nullable=False) + created_at = Column(DateTime, default=datetime.datetime.utcnow) + updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) + page_number = Column(Integer) # For PDF comments + x_coord = Column(Integer) # For position-specific comments + y_coord = Column(Integer) + + document = relationship("Document", back_populates="comments") + +class Collection(Base): + __tablename__ = 'collections' + + id = Column(Integer, primary_key=True) + name = Column(String(100), nullable=False, unique=True) + description = Column(Text) + color = Column(String(7), default="#808080") # Hex color code + created_at = Column(DateTime, default=datetime.datetime.utcnow) + + documents = relationship("Document", secondary=document_collections, back_populates="collections") + +class Statistics(Base): + __tablename__ = 'statistics' + + id = Column(Integer, primary_key=True) + date = Column(DateTime, nullable=False) + total_documents = Column(Integer, default=0) + total_folders = Column(Integer, default=0) + documents_by_type = Column(Text) # JSON string + documents_by_brand = Column(Text) # JSON string + storage_used = Column(Integer, default=0) # in bytes diff --git a/preferences.py b/preferences.py new file mode 100644 index 0000000..ce7a53f --- /dev/null +++ b/preferences.py @@ -0,0 +1,176 @@ +from PySide6.QtWidgets import (QDialog, QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, + QCheckBox, QPushButton, QSpinBox, QComboBox, QColorDialog, + QMessageBox, QLineEdit, QLabel, QFileDialog) +from PySide6.QtCore import QSettings, Qt +import os +import json +from datetime import datetime + +class Preferences: + def __init__(self): + self.settings = QSettings("Codeium", "GestaoDocumentos") + self.load_defaults() + + def load_defaults(self): + if not self.settings.contains("default_save_path"): + self.settings.setValue("default_save_path", "") + if not self.settings.contains("auto_preview"): + self.settings.setValue("auto_preview", True) + if not self.settings.contains("max_recent_docs"): + self.settings.setValue("max_recent_docs", 10) + if not self.settings.contains("theme"): + self.settings.setValue("theme", "light") + if not self.settings.contains("preview_size"): + self.settings.setValue("preview_size", "medium") + if not self.settings.contains("auto_version"): + self.settings.setValue("auto_version", True) + if not self.settings.contains("backup_reminder_days"): + self.settings.setValue("backup_reminder_days", 7) + + def get(self, key, type=None): + return self.settings.value(key, type=type) + + def set(self, key, value): + self.settings.setValue(key, value) + + def get_save_path(self): + return self.settings.value("default_save_path", type=str) + + def get_auto_save(self): + return bool(self.settings.value("auto_preview", type=bool)) + + def get_recent_limit(self): + return self.settings.value("max_recent_docs", type=int) + + def get_theme(self): + return self.settings.value("theme", type=str) + +class PreferencesDialog(QDialog): + def __init__(self, preferences, parent=None): + super().__init__(parent) + self.preferences = preferences + self.setWindowTitle("Preferências") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Save path settings + save_path_layout = QHBoxLayout() + self.save_path_edit = QLineEdit(self.preferences.get_save_path()) + save_path_layout.addWidget(self.save_path_edit) + + browse_btn = QPushButton("Procurar...") + browse_btn.clicked.connect(self.browse_save_path) + save_path_layout.addWidget(browse_btn) + + layout.addLayout(save_path_layout) + + # Auto-save settings + auto_save_check = QCheckBox("Salvar automaticamente") + auto_save_check.setChecked(self.preferences.get_auto_save()) + layout.addWidget(auto_save_check) + + # Recent documents limit + recent_limit_layout = QHBoxLayout() + recent_limit_layout.addWidget(QLabel("Número de documentos recentes:")) + recent_limit_spin = QSpinBox() + recent_limit_spin.setRange(5, 50) + recent_limit_spin.setValue(self.preferences.get_recent_limit()) + recent_limit_layout.addWidget(recent_limit_spin) + layout.addLayout(recent_limit_layout) + + # Theme selection + theme_layout = QHBoxLayout() + theme_layout.addWidget(QLabel("Tema:")) + theme_combo = QComboBox() + theme_combo.addItems(["Claro", "Escuro"]) + theme_combo.setCurrentText(self.preferences.get_theme()) + theme_layout.addWidget(theme_combo) + layout.addLayout(theme_layout) + + # Buttons + button_layout = QHBoxLayout() + save_btn = QPushButton("Salvar") + save_btn.clicked.connect(self.save_preferences) + button_layout.addWidget(save_btn) + + cancel_btn = QPushButton("Cancelar") + cancel_btn.clicked.connect(self.reject) + button_layout.addWidget(cancel_btn) + + layout.addLayout(button_layout) + + def browse_save_path(self): + path = QFileDialog.getExistingDirectory( + self, + "Selecionar Pasta Padrão", + self.save_path_edit.text() + ) + if path: + self.save_path_edit.setText(path) + + def save_preferences(self): + try: + self.preferences.set("default_save_path", self.save_path_edit.text()) + # self.preferences.set("auto_save", self.auto_save_check.isChecked()) + # self.preferences.set("max_recent_docs", self.recent_limit_spin.value()) + # self.preferences.set("theme", self.theme_combo.currentText()) + + QMessageBox.information(self, "Sucesso", + "Preferências salvas com sucesso!") + self.close() + except Exception as e: + QMessageBox.critical(self, "Erro", + f"Erro ao salvar preferências: {str(e)}") + +class RecentDocuments: + def __init__(self, preferences): + self.preferences = preferences + self.recent_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "recent_docs.json" + ) + self.load_recent_docs() + + def load_recent_docs(self): + try: + if os.path.exists(self.recent_file): + with open(self.recent_file, 'r') as f: + self.recent_docs = json.load(f) + else: + self.recent_docs = [] + except Exception: + self.recent_docs = [] + + def save_recent_docs(self): + try: + with open(self.recent_file, 'w') as f: + json.dump(self.recent_docs, f) + except Exception as e: + print(f"Error saving recent documents: {str(e)}") + + def add_document(self, doc_id, doc_name): + # Remove if already exists + self.recent_docs = [doc for doc in self.recent_docs + if doc['id'] != doc_id] + + # Add to start of list + self.recent_docs.insert(0, { + 'id': doc_id, + 'name': doc_name, + 'timestamp': datetime.now().isoformat() + }) + + # Trim list + max_recent = int(self.preferences.get("max_recent_docs")) + self.recent_docs = self.recent_docs[:max_recent] + + self.save_recent_docs() + + def get_recent_docs(self): + return self.recent_docs + + def clear_recent_docs(self): + self.recent_docs = [] + self.save_recent_docs()