-- ============================================================================
-- Arquivo: Main.hs
-- Descrição: Arquivo principal com TODAS as operações de I/O
-- Responsável: Aluno 3 (Jafte Carneiro Fagundes da Silva)
-- Objetivo: Gerenciar TODA a interação com arquivos, usuário e sistema
-- ============================================================================
import InventarioTipos
import LogicaNegocio
import IOPersistencia (Comando(..), parseComando, arquivoInventario, arquivoAuditoria)
import qualified Data.Map as Map
import Analise (formatarRelatorioCompleto, historicoPorItem, formatarLog)
import Data.Time (getCurrentTime, UTCTime, TimeZone(..))
import System.IO (hFlush, stdout)
import Control.Exception (catch, IOException)
-- FUNÇÕES DE INICIALIZAÇÃO
carregarInventario :: IO Inventario
carregarInventario = do
conteudo <- catch (readFile arquivoInventario)
handleArquivoNaoExiste
if null conteudo
then do
putStrLn "[INFO] Iniciando com inventario vazio."
return emptyInventario
else do
putStrLn "[INFO] Inventario carregado com sucesso!"
return (read conteudo :: Inventario)
where
handleArquivoNaoExiste :: IOException -> IO String
handleArquivoNaoExiste _ = do
putStrLn "[INFO] Arquivo Inventario.dat nao encontrado."
return ""
carregarLogs :: IO [LogEntry]
carregarLogs = do
conteudo <- catch (readFile arquivoAuditoria)
handleArquivoNaoExiste
if null conteudo
then do
putStrLn "[INFO] Iniciando com log de auditoria vazio."
return []
else do
putStrLn "[INFO] Log de auditoria carregado com sucesso!"
let linhas = lines conteudo
let parseLinha l = case reads l of
[(log, "")] -> Just log
_ -> Nothing
let logsMaybe = map parseLinha linhas
let logs = [log | Just log <- logsMaybe]
let falhas = length linhas - length logs
if falhas > 0
then putStrLn $ "[AVISO] Ignoradas " ++ show falhas ++ " linhas mal formatadas no Auditoria.log."
else return ()
return logs
where
handleArquivoNaoExiste :: IOException -> IO String
handleArquivoNaoExiste _ = do
putStrLn "[INFO] Arquivo Auditoria.log nao encontrado."
return ""
-- FUNÇÕES DE PERSISTÊNCIA
salvarInventario :: Inventario -> IO ()
salvarInventario inventario = do
writeFile arquivoInventario (show inventario)
putStrLn "[PERSISTENCIA] Inventario salvo em Inventario.dat"
adicionarLogAuditoria :: LogEntry -> IO ()
adicionarLogAuditoria logEntry = do
appendFile arquivoAuditoria (show logEntry ++ "\n")
putStrLn "[AUDITORIA] Log registrado em Auditoria.log"
-- FUNÇÕES AUXILIARES DE EXIBIÇÃO
imprimirItem :: Item -> IO ()
imprimirItem item = do
putStrLn $ " ID: " ++ itemID item
putStrLn $ " Nome: " ++ nome item
putStrLn $ " Quantidade: " ++ show (quantidade item)
putStrLn $ " Categoria: " ++ categoria item
putStrLn " ---"
mostrarAjuda :: IO ()
mostrarAjuda = do
putStrLn "\n========== COMANDOS DISPONIVEIS =========="
putStrLn "add <ID> <nome> <quantidade> <categoria>"
putStrLn " - Adiciona um novo item ao inventario"
putStrLn " - Exemplo: add 001 Teclado 10 Perifericos"
putStrLn ""
putStrLn "remove <ID> <quantidade>"
putStrLn " - Remove uma quantidade de um item"
putStrLn " - Exemplo: remove 001 5"
putStrLn ""
putStrLn "update <ID> <nova_quantidade>"
putStrLn " - Atualiza a quantidade de um item"
putStrLn " - Exemplo: update 001 20"
putStrLn ""
putStrLn "list"
putStrLn " - Lista todos os itens do inventario"
putStrLn ""
putStrLn "report"
putStrLn " - Gera relatorio de logs"
putStrLn ""
putStrLn "help"
putStrLn " - Mostra esta mensagem de ajuda"
putStrLn ""
putStrLn "exit"
putStrLn " - Encerra o programa"
putStrLn "==========================================\n"
-- PROCESSAMENTO DE COMANDOS
processarComando :: Comando -> Inventario -> IO Inventario
processarComando cmd inventario = do
tempo <- getCurrentTime
case cmd of
CmdAdd idItem nomeItem qtd cat -> do
let novoItem = Item
{ itemID = idItem
, nome = nomeItem
, quantidade = qtd
, categoria = cat
}
case addItem tempo novoItem inventario of
Left erro -> do
putStrLn $ "\n[ERRO] " ++ erro
let logEntry = LogEntry
{ timestamp = tempo
, acao = Add
, detalhes = "Falha ao adicionar item ID: " ++ idItem
, status = Falha erro
}
adicionarLogAuditoria logEntry
return inventario
Right (novoInventario, logEntry) -> do
putStrLn $ "\n[SUCESSO] Item adicionado: " ++ nomeItem ++ " (ID: " ++ idItem ++ ")"
salvarInventario novoInventario
adicionarLogAuditoria logEntry
return novoInventario
CmdRemove idItem qtd -> do
case removeItem tempo idItem qtd inventario of
Left erro -> do
putStrLn $ "\n[ERRO] " ++ erro
let logEntry = LogEntry
{ timestamp = tempo
, acao = Remove
, detalhes = "Falha ao remover do item ID: " ++ idItem
, status = Falha erro
}
adicionarLogAuditoria logEntry
return inventario
Right (novoInventario, logEntry) -> do
putStrLn $ "\n[SUCESSO] Removidas " ++ show qtd ++ " unidade(s) do item ID: " ++ idItem
salvarInventario novoInventario
adicionarLogAuditoria logEntry
return novoInventario
CmdUpdate idItem novaQtd -> do
case updateQty tempo idItem novaQtd inventario of
Left erro -> do
putStrLn $ "\n[ERRO] " ++ erro
let logEntry = LogEntry
{ timestamp = tempo
, acao = Update
, detalhes = "Falha ao atualizar item ID: " ++ idItem
, status = Falha erro
}
adicionarLogAuditoria logEntry
return inventario
Right (novoInventario, logEntry) -> do
putStrLn $ "\n[SUCESSO] Quantidade do item ID: " ++ idItem ++ " atualizada para " ++ show novaQtd
salvarInventario novoInventario
adicionarLogAuditoria logEntry
return novoInventario
CmdList -> do
putStrLn "\n========== INVENTARIO ATUAL =========="
if Map.null inventario
then putStrLn "Inventario vazio."
else mapM_ imprimirItem (Map.elems inventario)
putStrLn "======================================\n"
return inventario
CmdReport -> do
putStrLn "\n[INFO] Carregando logs para gerar relatorio..."
logs <- carregarLogs
let tz = TimeZone { timeZoneMinutes = -180, timeZoneSummerOnly = False, timeZoneName = "BRT" }
-- Gera o relatório usando o módulo Analise.hs
if null logs
then putStrLn "\n[INFO] Nenhum log encontrado para gerar relatorio."
else do
-- Gera o relatório principal
putStrLn $ formatarRelatorioCompleto tz logs
putStrLn "\nDigite um ID de item para ver seu historico (ou aperte o ENTER para pular):"
putStr "> "
hFlush stdout
idInput <- getLine
if null idInput
then return inventario
else do
let historico = historicoPorItem logs idInput
putStrLn $ "\n--- Historico para Item ID: " ++ idInput ++ " ---"
if null historico
then putStrLn "Nenhum registro encontrado para este item."
else
-- Imprime cada log formatado
mapM_ (putStrLn . (" " ++) . formatarLog tz) historico
putStrLn "---------------------------------------------"
return inventario
CmdHelp -> do
mostrarAjuda
return inventario
CmdExit -> do
putStrLn "\n[INFO] Encerrando o sistema..."
return inventario
CmdInvalido msg -> do
putStrLn $ "\n[ERRO] " ++ msg
putStrLn "Digite 'help' para ver os comandos disponiveis.\n"
return inventario
-- LOOP PRINCIPAL
loopPrincipal :: Inventario -> IO ()
loopPrincipal inventario = do
putStr "\n> "
hFlush stdout
input <- getLine
let cmd = parseComando input
if cmd == CmdExit
then do
putStrLn "\n[INFO] Sistema encerrado com sucesso!"
putStrLn "Ate logo!\n"
else do
novoInventario <- processarComando cmd inventario
loopPrincipal novoInventario
-- FUNÇÃO MAIN - PONTO DE ENTRADA
main :: IO ()
main = do
putStrLn "\n=========================================="
putStrLn " SISTEMA DE GERENCIAMENTO DE INVENTARIO"
putStrLn "=========================================="
putStrLn ""
putStrLn "[INICIALIZACAO] Carregando dados do sistema..."
inventario <- carregarInventario
logs <- carregarLogs
putStrLn $ "[INFO] Inventario contem " ++ show (Map.size inventario) ++ " item(ns)"
putStrLn $ "[INFO] Log contem " ++ show (length logs) ++ " entrada(s)"
putStrLn ""
putStrLn "Digite 'help' para ver os comandos disponiveis."
loopPrincipal inventario
-- ============================================================================
-- Módulo: IOPersistencia.hs
-- Descrição: Funções PURAS para parsing de comandos
-- Responsável: Aluno 3 (Jafte Carneiro Fagundes da Silva)
-- ============================================================================
module IOPersistencia
( Comando(..)
, parseComando
, arquivoInventario
, arquivoAuditoria
) where
-- ============================================================================
-- CONSTANTES
-- ============================================================================
-- Nome do arquivo de inventário (sobrescrito a cada operação)
arquivoInventario :: FilePath
arquivoInventario = "Inventario.dat"
-- Nome do arquivo de auditoria (modo append-only)
arquivoAuditoria :: FilePath
arquivoAuditoria = "Auditoria.log"
-- ============================================================================
-- TIPOS DE DADOS
-- ============================================================================
-- ADT representando todos os comandos possÃveis do sistema
data Comando
= CmdAdd String String Int String -- add <ID> <nome> <qtd> <categoria>
| CmdRemove String Int -- remove <ID> <qtd>
| CmdUpdate String Int -- update <ID> <nova_qtd>
| CmdList -- list
| CmdReport -- report
| CmdHelp -- help
| CmdExit -- exit
| CmdInvalido String -- comando inválido
deriving (Show, Eq)
-- ============================================================================
-- PARSER DE COMANDOS
-- ============================================================================
-- Transforma a entrada do usuário em um Comando
-- Função pura: mesma entrada sempre produz mesma saÃda
parseComando :: String -> Comando
parseComando input =
let tokens = words input
in case tokens of
("add":idItem:nomeItem:qtd:cat:_) ->
case reads qtd of
[(quantidade, "")] ->
CmdAdd idItem nomeItem quantidade (unwords (drop 4 tokens))
_ -> CmdInvalido "Quantidade invalida para comando 'add'"
("remove":idItem:qtd:_) ->
case reads qtd of
[(quantidade, "")] -> CmdRemove idItem quantidade
_ -> CmdInvalido "Quantidade invalida para comando 'remove'"
("update":idItem:qtd:_) ->
case reads qtd of
[(quantidade, "")] -> CmdUpdate idItem quantidade
_ -> CmdInvalido "Quantidade invalida para comando 'update'"
["list"] -> CmdList
["report"] -> CmdReport
["help"] -> CmdHelp
["exit"] -> CmdExit
[] -> CmdInvalido "Comando vazio"
_ -> CmdInvalido ("Comando desconhecido: " ++ unwords tokens)
-- Módulo: LogicaNegocio.hs
-- Implementa a lógica de negócio pura do sistema de inventário
-- Responsável: Aluno 2
module LogicaNegocio
( ResultadoOperacao
, addItem
, removeItem
, updateQty
) where
import InventarioTipos
import qualified Data.Map as Map
import Data.Time (UTCTime)
type ResultadoOperacao = (Inventario, LogEntry)
-- | Adiciona um item ao inventário.
-- Retorna Left com mensagem de erro se falhar.
-- Retorna Right com inventário atualizado e log de sucesso.
addItem :: UTCTime -> Item -> Inventario -> Either String ResultadoOperacao
addItem tempo novoItem inventario
| quantidade novoItem < 0 =
Left "Quantidade deve ser maior ou igual a zero"
| Map.member (itemID novoItem) inventario =
Left "Item com ID ja existe no inventario"
| otherwise =
let inventarioNovo = Map.insert (itemID novoItem) novoItem inventario
logEntry = LogEntry
{ timestamp = tempo
, acao = Add
, detalhes = "Adicionado item '" ++ nome novoItem ++ "' (ID: " ++
itemID novoItem ++ ") com quantidade " ++
show (quantidade novoItem) ++ " na categoria " ++ categoria novoItem
, status = Sucesso
}
in Right (inventarioNovo, logEntry)
-- | Remove uma quantidade de um item do inventário.
-- Retorna Left com mensagem de erro se falhar.
-- Retorna Right com inventário atualizado e log de sucesso.
removeItem :: UTCTime -> String -> Int -> Inventario -> Either String ResultadoOperacao
removeItem tempo idItem qtdRemover inventario
| qtdRemover <= 0 =
Left "Quantidade a remover deve ser maior que zero"
| otherwise =
case Map.lookup idItem inventario of
Nothing ->
Left "Item nao encontrado no inventario"
Just itemAtual
| quantidade itemAtual < qtdRemover ->
Left "Estoque insuficiente"
| otherwise ->
let novaQuantidade = quantidade itemAtual - qtdRemover
itemAtualizado = itemAtual { quantidade = novaQuantidade }
inventarioNovo = Map.insert idItem itemAtualizado inventario
logEntry = LogEntry
{ timestamp = tempo
, acao = Remove
, detalhes = "Removidas " ++ show qtdRemover ++
" unidades do item '" ++ nome itemAtual ++
"' (ID: " ++ idItem ++ ")"
, status = Sucesso
}
in Right (inventarioNovo, logEntry)
-- | Atualiza a quantidade de um item no inventário.
-- Retorna Left com mensagem de erro se falhar.
-- Retorna Right com inventário atualizado e log de sucesso.
updateQty :: UTCTime -> String -> Int -> Inventario -> Either String ResultadoOperacao
updateQty tempo idItem novaQtd inventario
| novaQtd < 0 =
Left "Quantidade deve ser maior ou igual a zero"
| otherwise =
case Map.lookup idItem inventario of
Nothing ->
Left "Item nao encontrado no inventario"
Just itemAtual ->
let itemAtualizado = itemAtual { quantidade = novaQtd }
inventarioNovo = Map.insert idItem itemAtualizado inventario
logEntry = LogEntry
{ timestamp = tempo
, acao = Update
, detalhes = "Atualizada quantidade do item '" ++ nome itemAtual ++
"' (ID: " ++ idItem ++ ") para " ++ show novaQtd
, status = Sucesso
}
in Right (inventarioNovo, logEntry)
-- Módulo: InventarioTipos.hs
-- Descrição: Define os tipos de dados do sistema de inventário
module InventarioTipos
( Item(..)
, Inventario
, AcaoLog(..)
, StatusLog(..)
, LogEntry(..)
, emptyInventario
, exemploItem
, exemploLog
) where
-- Importações
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Time (UTCTime)
-- Definição dos Tipos de Dados
-- Tipo: Item
-- Descrição: Representa um item no inventário
data Item = Item
{ itemID :: String
, nome :: String
, quantidade :: Int
, categoria :: String
} deriving (Show, Read, Eq)
-- Tipo: Inventario
-- Descrição: Representa o inventário como um mapa de itens
-- Implementação: Map usando itemID como chave e Item como valor
type Inventario = Map String Item
-- Função auxiliar para criar um inventário vazio
emptyInventario :: Inventario
emptyInventario = Map.empty
-- Tipo: AcaoLog (ADT)
-- Descrição: Tipo algébrico para representar as diferentes ações do sistema
data AcaoLog
= Add
| Remove
| Update
| QueryFail
deriving (Show, Read, Eq)
-- Tipo: StatusLog (ADT)
-- Descrição: Tipo algébrico para representar o resultado de uma operação
data StatusLog
= Sucesso
| Falha String
deriving (Show, Read, Eq)
-- Tipo: LogEntry
-- Descrição: Representa uma entrada de log no sistema
data LogEntry = LogEntry
{ timestamp :: UTCTime
, acao :: AcaoLog
, detalhes :: String
, status :: StatusLog
} deriving (Show, Read, Eq)
-- Exemplos de uso e validação
-- Exemplo de criação de um Item
exemploItem :: Item
exemploItem = Item
{ itemID = "001"
, nome = "Teclado Mecanico"
, quantidade = 15
, categoria = "Perifericos"
}
-- Exemplo de criação de LogEntry
exemploLog :: UTCTime -> LogEntry
exemploLog tempo = LogEntry
{ timestamp = tempo
, acao = Add
, detalhes = "Adicionado item 001 - Teclado Mecanico"
, status = Sucesso
}
-- Módulo: Analise.hs
-- Responsável: Aluno 4 (Ângelo Piovezan Jorgeto)
-- Objetivo: Funções puras para análise e geração de relatórios de logs.
module Analise
( logsDeErro
, historicoPorItem
, itemMaisMovimentado
, formatarRelatorioCompleto
, formatarLog
) where
-- Importações
import InventarioTipos
import Data.List (group, sort, sortBy, filter, find, tails, isPrefixOf, drop, isInfixOf)
import Data.Maybe (mapMaybe)
import Data.Ord (comparing, Down(..))
import Data.Time (TimeZone, utcToZonedTime, formatTime)
import Data.Time.Format (defaultTimeLocale)
-- =============================================
-- Funções de Análise Puras
-- =============================================
-- Filtra a lista de logs, retornando apenas aqueles com status 'Falha'
logsDeErro :: [LogEntry] -> [LogEntry]
logsDeErro = filter (ehFalha . status)
where
ehFalha :: StatusLog -> Bool
ehFalha (Falha _) = True
ehFalha Sucesso = False
-- Filtra logs que mencionam um ID de item especÃfico
historicoPorItem :: [LogEntry] -> String -> [LogEntry]
historicoPorItem logs idAlvo =
let
marcador1 = "ID: " ++ idAlvo
marcador2 = "(ID: " ++ idAlvo
in
filter (\log -> (marcador1 `isInfixOf` detalhes log) || (marcador2 `isInfixOf` detalhes log)) logs
-- Encontra o itemID com mais logs de SUCESSO
itemMaisMovimentado :: [LogEntry] -> String
itemMaisMovimentado logs =
let ids = mapMaybe extractSuccessItemID logs
in case ids of
[] -> "Nenhuma movimentacao de sucesso registrada."
_ ->
let groupedIDs = group (sort ids)
sortedGroups = sortBy (comparing (Down . length)) groupedIDs
in
case sortedGroups of
((topID:_):_) ->
let count = length (filter (== topID) ids)
in topID ++ " (com " ++ show count ++ " movimentacoes)"
_ -> "Erro interno de analise de logs."
-- Helper para extrair ItemID de logs de Sucesso
extractSuccessItemID :: LogEntry -> Maybe String
extractSuccessItemID log
| status log /= Sucesso = Nothing
| otherwise =
let details = detalhes log
marker = "(ID: "
in
case find (isPrefixOf marker) (tails details) of
Just match -> Just $ takeWhile (/= ')') (drop (length marker) match)
Nothing -> Nothing
-- =============================================
-- Funções de Formatação de Relatório
-- =============================================
-- Converte um LogEntry em uma String formatada para exibição
formatarLog :: TimeZone -> LogEntry -> String
formatarLog tz log =
let
zonedTime = utcToZonedTime tz (timestamp log)
formattedTime = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" zonedTime
in
"[" ++ formattedTime ++ "] " ++
"[" ++ show (acao log) ++ "] " ++
"[" ++ formatarStatus (status log) ++ "] " ++
detalhes log
where
formatarStatus Sucesso = "SUCESSO"
formatarStatus (Falha msg) = "FALHA: " ++ msg
-- Cria o texto completo do relatório para ser impresso pelo módulo de IO
formatarRelatorioCompleto :: TimeZone -> [LogEntry] -> String
formatarRelatorioCompleto tz logs =
let total = length logs
erros = logsDeErro logs
totalErros = length erros
itemTop = itemMaisMovimentado logs
in
unlines [ "========== RELATORIO DE ANALISE DE LOGS =========="
, ""
, "** Sumario Geral **"
, "Total de entradas de log: " ++ show total
, "Total de operacoes com falha: " ++ show totalErros
, "Item mais movimentado (com sucesso): " ++ itemTop
, ""
, "---"
, ""
, "** Detalhe de Logs de Erro (" ++ show totalErros ++ ") **"
, if null erros
then "Nenhum erro registrado."
else unlines (map ((" " ++) . formatarLog tz) erros)
, "===================================================="
]