Nós já conversamos sobre como ajudamos uma empresa de fabricação de transformar os processos de treinamento corporativo e desenvolvimento de pessoal. Os funcionários do cliente, que estavam se afogando em documentos em papel e planilhas do Excel, receberam um aplicativo conveniente para iPad e um portal da web. Uma das funções mais importantes deste produto é a criação de relatórios dinâmicos pelos quais os gerentes julgam o trabalho dos funcionários “no campo”. São documentos enormes, com dezenas de campos e tamanhos médios de 3000 * 1600 pixels.Neste artigo, falaremos sobre como implantar essa beleza com base no Microsoft SQL Server Reporting Services, por que esse back-end pode ser um mau amigo do portal da Web e quais truques ajudarão a estabelecer seu relacionamento. Toda a parte comercial da solução já foi descrita no artigo anterior; portanto, aqui nos concentramos em questões técnicas. Vamos começar!Formulação do problema
Temos um portal com o qual várias centenas de usuários trabalham. Eles são organizados em uma hierarquia gradual, onde cada usuário tem um supervisor de uma classificação superior. Essa diferenciação de direitos é necessária para que os usuários possam criar eventos com qualquer funcionário subordinado. Você pode pular etapas, ou seja, o usuário pode iniciar a atividade com um funcionário de qualquer nível inferior a ele.Que eventos são significados aqui? Isso pode ser treinamento, suporte ou certificação de um funcionário de uma empresa comercial, que o supervisor conduz no ponto de venda. O resultado desse evento é um questionário preenchido em um iPad com classificações de funcionários para qualidades e habilidades profissionais.De acordo com os dados do questionário, você pode preparar estatísticas, por exemplo:- Quantos eventos com seus subordinados desse tipo Vasya Ivanov criou em um mês? Quantos deles foram concluídos?
- Qual é a porcentagem de classificações satisfatórias? Quais perguntas os comerciantes respondem pior? Qual gerente é pior em fazer testes?
Essas estatísticas estão contidas nos relatórios que podem ser criados via interface da web, nos formatos XLS, PDF, DOCX e impressos. Todas essas funções são projetadas para gerentes em diferentes níveis.O conteúdo e o design dos relatórios são definidos nos modelos , permitindo definir os parâmetros necessários. Se, no futuro, os usuários precisarem de novos tipos de relatórios, o sistema poderá criar modelos, especificar parâmetros modificáveis e adicionar um modelo ao portal. Tudo isso - sem interferir no código fonte e nos processos de trabalho do produto.Especificações e limitações
O portal é executado na arquitetura de microsserviço, a frente está escrita em Angular 5. O recurso usa autorização JWT, suporta navegadores Google Chrome, Firefox, Microsoft Edge e IE 11 .Todos os dados são armazenados no MS SQL Server 2014. O SQL Server Reporting Services (SSRS) está instalado no servidor, o cliente usa e não vai recusar. Daí a limitação mais importante: o acesso ao SSRS é fechado a partir do exterior, para que você possa acessar a interface da web e o SOAP somente da rede local através da autorização NTLM.Algumas palavras sobre o SSRSSSRS – , , . docs.microsoft.com, SSRS (API) ( Report Server, - HTTP).
Atenção, a pergunta: como concluir a tarefa sem métodos manuais, com recursos mínimos e benefícios máximos para o cliente?Como o cliente possui o SSRS em um servidor dedicado, deixe o SSRS fazer todo o trabalho sujo de gerar e exportar relatórios. Então não precisamos escrever nosso próprio serviço de relatórios, exportar módulos para XLS, PDF, DOCX, HTML e a API correspondente.Portanto, a tarefa era fazer o SSRS fazer amizade com o portal e garantir a operação das funções especificadas na tarefa. Então, vamos examinar a lista desses cenários - sutilezas interessantes foram encontradas em quase todos os pontos.Estrutura da solução
Como já temos o SSRS, existem todas as ferramentas para gerenciar modelos de relatório:- Servidor de Relatório - responsável por toda a lógica de trabalhar com relatórios, seu armazenamento, geração, gerenciamento e muito mais.
- Gerenciador de relatórios - um serviço com uma interface da web para gerenciar relatórios. Aqui você pode carregar modelos criados no SQL Server Data Tools para o servidor, configurar direitos de acesso, fontes de dados e parâmetros (incluindo aqueles que podem ser alterados ao relatar solicitações). Ele é capaz de gerar relatórios sobre modelos baixados e enviá-los para vários formatos, incluindo XLS, PDF, DOCX e HTML.
Total: criamos modelos no SQL Server Data Tools, com a ajuda do Report Manager, os preenchemos no Report Server, configuramos - e está pronto. Podemos gerar relatórios, alterar seus parâmetros.A próxima pergunta: como solicitar a geração de relatórios sobre modelos específicos por meio do portal e obter o resultado inicial para saída na interface do usuário ou fazer o download no formato desejado?Relatórios do SSRS para o portal
Como dissemos acima, o SSRS possui sua própria API para acessar relatórios. Mas não queremos distribuir suas funções por razões de segurança e higiene digital - precisamos apenas solicitar dados do SSRS na forma correta e transmitir o resultado ao usuário. O gerenciamento de relatórios será tratado por uma equipe de clientes especialmente treinada.Como o acesso ao SSRS é apenas da rede local, a troca de dados entre o servidor e o portal é feita por meio de um serviço proxy.
Troca de dados entre o portal e o servidorVamos ver como funciona e por que o ReportProxy está aqui.Portanto, no lado do portal, temos um ReportService, que o portal acessa para relatórios. O serviço verifica a autorização do usuário, o nível de seus direitos, converte dados do SSRS na forma desejada no contrato.A API ReportService contém apenas 2 métodos, que são suficientes para nós:- GetReports - fornece os identificadores e nomes de todos os modelos que o usuário atual pode receber;
- GetReportData (formato, parâmetros) - fornece dados de relatório exportados e prontos no formato especificado, com um determinado conjunto de parâmetros.
Agora você precisa desses 2 métodos para poder se comunicar com o SSRS e obter os dados necessários da forma correta. A partir da documentação, sabe-se que podemos acessar o servidor de relatório via HTTP usando a API SOAP. Parece que o quebra-cabeça está se desenvolvendo ... Mas, de fato, uma surpresa nos espera aqui.Como o SSRS está fechado para o mundo externo e você pode alcançá-lo somente através da autenticação NTLM, ele não está disponível diretamente no portal SOAP. Há também nossos próprios desejos:- Conceda acesso apenas ao conjunto de funções necessário e até proíba a alteração;
- Se você precisar mudar para outro sistema de relatórios, as edições no ReportService deverão ser mínimas e, melhor, não serão necessárias.
É aqui que o ReportProxy nos ajuda, localizado na mesma máquina que o SSRS e responsável por solicitações de proxy do ReportService para o SSRS. O processamento de solicitações é o seguinte:- o serviço recebe uma solicitação do ReportService, verifica a autorização da JWT;
- de acordo com o método API, o proxy passa pelo protocolo SOAP no SSRS para obter os dados necessários, efetuando login no NTLM ao longo do caminho;
- Os dados recebidos do SSRS são enviados de volta ao ReportService em resposta à solicitação.
De fato, o ReportProxy é um adaptador entre o SSRS e o ReportService.O controlador é o seguinte:[BasicAuthentication]
public class ReportProxyController : ApiController
{
[HttpGet()]
public List<ReportItem> Get(string rootPath)
{
}
public HttpResponseMessage Post([FromBody]ReportRequest request)
{
}
}
BasicAuthentication :
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
var authenticationToken = actionContext.Request.Headers.Authorization.Parameter;
var tokenFromBase64 = Convert.FromBase64String(authenticationToken);
var decodedAuthenticationToken = Encoding.UTF8.GetString(tokenFromBase64);
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
var userName = usernamePasswordArray[0];
var password = usernamePasswordArray[1];
var isValid = userName == BasiAuthConf.Login && password == BasiAuthConf.Password;
if (isValid)
{
var principal = new GenericPrincipal(new GenericIdentity(userName), null);
Thread.CurrentPrincipal = principal;
return;
}
}
HandleUnathorized(actionContext);
}
private static void HandleUnathorized(HttpActionContext actionContext)
{
actionContext.Response = actionContext.Request.CreateResponse(
HttpStatusCode.Unauthorized
);
actionContext.Response.Headers.Add(
"WWW-Authenticate", "Basic Scheme='Data' location = 'http://localhost:"
);
}
}
Como resultado, o processo fica assim:- A frente envia uma solicitação http para ReportService;
- ReportService envia uma solicitação http para ReportProxy;
- O ReportProxy através da interface SOAP recebe dados do SSRS e envia o resultado ao ReportService;
- O ReportService traz o resultado de acordo com o contrato e o entrega ao cliente.
Temos um sistema operacional que solicita uma lista de modelos disponíveis, acessa o SSRS para relatórios e os apresenta à frente em todos os formatos suportados. Agora você precisa exibir os relatórios gerados na frente, de acordo com os parâmetros especificados, dar a oportunidade de enviá-los para arquivos XLS, PDF, DOCX e imprimir. Vamos começar com a exibição.Trabalhando com relatórios do SSRS no portal
À primeira vista, é uma questão cotidiana - o relatório é fornecido em formato HTML, para que possamos fazer o que quisermos com ele! Vamos incorporá-lo à página, pintá-lo com estilos de design, e a coisa está no chapéu. De fato, houve armadilhas suficientes.De acordo com o conceito de design, a seção de relatórios no portal deve consistir em duas páginas:1) uma lista de modelos onde podemos:- Visualizar estatísticas de atividades para todo o portal;
- veja todos os modelos disponíveis para nós;
- clique no modelo desejado e vá para o gerador de relatórios correspondente.
2) um gerador de relatórios que nos permite:- defina os parâmetros do modelo e crie um relatório sobre eles;
- veja o que aconteceu como resultado;
- selecione o formato do arquivo de saída, faça o download;
- imprima o relatório de forma conveniente e visual.
Não houve problemas especiais com a primeira página, por isso não vamos considerar mais. E o gerador de relatórios nos forçou a ligar o engenheiro, para que fosse conveniente que pessoas reais usassem todas as funções no TK.Problema número 1. Mesas gigantes
De acordo com o conceito de design, esta página deve ter uma área de visualização para que o usuário possa ver seu relatório antes de exportar. Se o relatório não couber na janela, você pode rolar horizontal e verticalmente. Ao mesmo tempo, um relatório típico pode atingir tamanhos de várias telas, o que significa que precisamos colar blocos com os nomes de linhas e colunas. Sem isso, os usuários terão que retornar constantemente ao topo da tabela para lembrar o que significa uma célula específica. Ou, em geral, será mais fácil imprimir um relatório e manter constantemente as folhas necessárias diante dos seus olhos, mas a tabela na tela simplesmente perde seu significado.Em geral, os blocos colantes não podem ser evitados. E o SSRS 2014 não sabe como corrigir linhas e colunas em um documento MHTML - apenas em sua própria interface da web.Aqui, lembramos que os navegadores modernos suportam a propriedade adesiva CSS , que apenas fornece a função que precisamos. Colocamos a posição: pegajosa no bloco marcado, especifica o recuo à esquerda ou na parte superior (propriedades esquerda, superior) e o bloco permanecerá no lugar durante a rolagem horizontal e vertical.Você precisa encontrar um parâmetro que o CSS possa capturar. Valores de célula personalizados que permitem que o SSRS 2014 os capture na interface da Web são perdidos ao exportar para HTML. OK, vamos marcá-los nós mesmos - só entenderíamos como.Depois de várias horas lendo a documentação e as discussões com os colegas, parecia que não havia opções. E aqui, de acordo com todas as leis da plotagem, o campo Dica de ferramenta apareceu para nós, o que nos permite especificar dicas de ferramentas para células. Descobriu-se que ele é lançado no código HTML exportado no atributo dica de ferramenta - exatamente na marca que pertence à célula personalizada no SQL Server Data Tools. Não houve escolha - não encontramos outra maneira de marcar as células para fixação.Portanto, você precisa criar regras de marcação e encaminhar marcadores em HTML via ToolTip. Em seguida, usando JS, alteramos o atributo dica de ferramenta para a classe CSS no marcador especificado.Existem apenas duas maneiras de corrigir células: verticalmente (coluna fixa) e horizontalmente (linha fixa). Faz sentido colocar outro marcador nas células dos cantos, que permanecem no lugar ao rolar nas duas direções - fixa-ambas.O próximo passo é fazer a interface do usuário. Ao receber um documento HTML, você precisa encontrar todos os elementos HTML com marcadores, reconhecer os valores, definir a classe CSS apropriada e remover o atributo dica de ferramenta para que ele não saia quando você passa o mouse sobre o mouse. Note-se que a marcação resultante consiste em tabelas aninhadas (tags de tabela).Ver códigotype FixationType = 'row' | 'column' | 'both';
init(reportHTML: HTMLElement) {
const rowsFixed: NodeList = reportHTML.querySelectorAll('[title^="RowFixed"]');
const columnFixed: NodeList = reportHTML.querySelectorAll('[title^="ColumnFixed"]');
const bothFixed: NodeList = reportHTML.querySelectorAll('[title^="BothFixed"]');
this.prepare(rowsFixed, 'row');
this.prepare(columnFixed, 'column');
this.prepare(bothFixed, 'both');
}
prepare(nodeList: NodeList, fixingType: FixationType) {
for (let i = 0; i < nodeList.length; i++) {
const element: HTMLElement = nodeList[i];
element.classList.add(fixingType + '-fixed');
element.removeAttribute('title');
element.removeAttribute('alt');
element.parentElement.classList.add(fixingType + '-fixed-parent');
element.style.width = element.getBoundingClientRect().width + 'px';
element.style.height = element.getBoundingClientRect().height + 'px';
this.calculateCellCascadeParams(element, fixingType);
}
}
E aqui está um novo problema: com o comportamento em cascata, quando vários blocos que se movem em uma direção são fixados ao mesmo tempo na tabela, as células que vão uma após a outra são colocadas em camadas. Ao mesmo tempo, não está claro quanto cada próximo bloco deve recuar - os recuos terão que ser calculados via JavaScript com base na altura do bloco à sua frente. Tudo isso se aplica às âncoras verticais e horizontais.O script de correção resolveu o problema.
calculateCellCascadeParams(cell: HTMLElement, fixationType: FixationType) {
const currentTD: HTMLTableCellElement = cell.parentElement;
const currentCellIndex = currentTD.cellIndex;
currentTD.style.left = '';
currentTD.style.top = '';
const currentTDStyles = getComputedStyle(currentTD);
if (fixationType === 'row' || fixationType === 'both') {
const parentRow: HTMLTableRowElement = currentTD.parentElement;
let previousRow: HTMLTableRowElement = parentRow;
let topOffset = 0;
while (previousRow = previousRow.previousElementSibling) {
let previousCellIndex = 0;
let cellIndexBulk = 0;
for (let i = 0; i < previousRow.cells.length; i++) {
if (previousRow.cells[i].colSpan > 1) {
cellIndexBulk += previousRow.cells[i].colSpan;
} else {
cellIndexBulk += 1;
}
if ((cellIndexBulk - 1) >= currentCellIndex) {
previousCellIndex = i;
break;
}
}
const previousCell = previousRow.cells[previousCellIndex];
if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
topOffset += previousCell.getBoundingClientRect().height;
}
}
if (topOffset > 0) {
if (currentTDStyles.top) {
topOffset += <any>currentTDStyles.top.replace('px', '') - 0;
}
currentTD.style.top = topOffset + 'px';
}
}
//
if (fixationType === 'column' || fixationType === 'both') {
//
// .
// , .
let previousCell: HTMLTableCellElement = currentTD;
let leftOffset = 0;
while (previousCell = previousCell.previousElementSibling) {
if (previousCell.classList.contains(fixationType + '_fixed_parent')) {
leftOffset += previousCell.getBoundingClientRect().width;
}
}
if (leftOffset > 0) {
if (currentTDStyles.left) {
leftOffset += <any>currentTDStyles.left.replace('px', '') - 0;
}
currentTD.style.left = leftOffset + 'px';
}
}
}
O código verifica as tags dos elementos marcados e adiciona os parâmetros das células fixas ao valor do recuo. No caso de linhas aderentes, sua altura é somada, para colunas, sua largura.
Um exemplo de relatório com uma linha superior fixa,como resultado, o processo é semelhante a este:- Nós obtemos a marcação do SSRS e colamos no lugar certo no DOM;
- Reconhecer marcadores;
- Ajuste os parâmetros para o comportamento em cascata.
Como o comportamento de aderência é totalmente implementado por meio do CSS, e o JS está envolvido apenas na preparação do documento recebido, a solução funciona com rapidez suficiente e sem atrasos.Infelizmente, para o IE, os blocos aderentes tiveram que ser desativados porque não suporta a posição: propriedade pegajosa. O restante - Safari, Mozilla Firefox e Chrome - faz um excelente trabalho.Ir em frente.Problema número 2. Exportação de relatórios
Para retirar um relatório do sistema, você deve (1) acessar o SSRS via ReportService para um objeto Blob, (2) obter um link para o objeto através da interface usando o método window.URL.createObjectURL, (3) colocar o link na tag e simular um clique para upload de arquivo.Isso funciona no Firefox, Safari e em todas as versões do Chrome, exceto a Apple. Para que o IE, Edge e Chrome para iOS também suportassem a função, eu tive que jogar meu cérebro de volta.No IE e Edge, o evento simplesmente não acionará uma solicitação do navegador para baixar o arquivo. Esses navegadores possuem um recurso que, para simular um clique, é necessária a confirmação do usuário para fazer o download, além de uma indicação clara de outras ações. A solução foi encontrada no método window.navigator.msSaveOrOpenBlob (), disponível no IE e no Edge. Ele apenas sabe como pedir permissão ao usuário para a operação e esclarecer o que fazer em seguida. Portanto, determinamos se o método window.navigator.msSaveOrOpenBlob existe e atuamos na situação.O Chrome no iOS não teve esse tipo de hack e, em vez de um relatório, temos apenas uma página em branco. Vagando pela Web, encontramos uma história semelhante, a julgar pela qual no iOS 13 esse bug deveria ter sido corrigido. Infelizmente, escrevemos o aplicativo nos dias do iOS 12, por isso decidimos não perder mais tempo e simplesmente desligamos o botão no Chrome para iOS.Agora, sobre como é o processo final de exportação para a interface do usuário. Há um botão no componente de relatório Angular que inicia uma cadeia de etapas:- através dos parâmetros do evento, o manipulador recebe o identificador do formato de exportação (por exemplo, “PDF”);
- Envia uma solicitação ao ReportService para receber um objeto Blob para o formato especificado;
- verifica se o navegador é IE ou Edge;
- quando a resposta vier do ReportService:
- se for IE ou Edge, chama window.navigator.msSaveOrOpenBlob (fileStream, fileName);
- caso contrário, ele chamará o método this.exportDownload (fileStream, fileName), em que fileStream é o Blob obtido da consulta ao ReportService e fileName é o nome do arquivo a ser salvo. O método cria uma marca oculta com um link para window.URL.createObjectURL (fileStream), simula um clique e remove a marca.
Com isso resolvido, a última aventura permaneceu.Problema número 3. Imprimir
Agora podemos ver o relatório no portal e exportá-lo para os formatos XLS, PDF e DOCX. Resta implementar a impressão do documento para obter um relatório preciso de várias páginas. Se a tabela acabou sendo dividida em páginas, cada uma delas deve conter cabeçalhos - os mesmos blocos que mencionamos na seção anterior.A opção mais fácil é pegar a página atual com o relatório exibido, ocultar tudo supérfluo usando CSS e enviá-la para impressão usando o método window.print (). Este método não funciona imediatamente por vários motivos:- Área de visualização não padrão - o próprio relatório está contido em uma área de rolagem separada, para que a página não se estenda a dimensões horizontais incríveis. Usar window.print () corta o conteúdo que não se ajusta à tela;
- , ;
- , .
Tudo isso pode ser corrigido usando JS e CSS, mas decidimos economizar tempo para os desenvolvedores e procurar uma alternativa para window.print ().O SSRS pode nos fornecer imediatamente um PDF pronto com uma paginação apresentável. Isso nos salva de todas as dificuldades da versão anterior, a única pergunta é: podemos imprimir o PDF através de um navegador?Como o PDF é um padrão de terceiros, os navegadores o suportam por meio de vários plugins do visualizador. Sem plug-in - sem desenhos, então, novamente, precisamos de uma opção alternativa.E se você colocar o PDF na página como uma imagem e enviar esta página para impressão? Já existem bibliotecas e componentes para o Angular que fornecem essa renderização. Pesquisado, experimentado, implementado.Para não lidar com os dados que não queremos imprimir, foi decidido transferir o conteúdo renderizado para uma nova página e já executar window.print (). Como resultado, todo o processo é o seguinte:- Solicite o ReportService para exportar o relatório em formato PDF;
- Obtemos o objeto Blob, convertemos para um URL (URL.createObjectURL (fileStream)), fornecemos o URL para o visualizador de PDF para renderização;
- Tiramos imagens do visualizador de PDF;
- Abra uma nova página e adicione uma pequena marcação lá (título, um pequeno recuo);
- Adicione a imagem do visualizador de PDF à marcação, chame window.print ().
Após várias verificações, também apareceu na página um código JS que, antes da impressão, verifica se todas as imagens foram carregadas.Assim, toda a aparência do documento é determinada pelos parâmetros do modelo SSRS, e a interface do usuário não interfere nesse processo. Isso reduz o número de possíveis erros. Como as imagens estão sendo transferidas para impressão, estamos seguros contra qualquer dano ou deformação do layout.Há também desvantagens:- um relatório grande pesará muito, o que afetará adversamente as plataformas móveis;
- o design não é atualizado automaticamente - cores, fontes e outros elementos do design precisam ser instalados no nível do modelo.
No nosso caso, a adição frequente de novos modelos não era esperada, portanto a solução era aceitável. O desempenho móvel foi tomado como garantido.A última palavra
Assim, um projeto regular mais uma vez nos faz procurar soluções simples para tarefas não triviais. O produto final atende totalmente aos requisitos de design e fica bonito. E o mais importante, embora não precisássemos procurar os métodos de implementação mais óbvios, a tarefa foi concluída mais rapidamente do que se adotássemos o módulo de relatório original com todas as consequências. E, no final, conseguimos nos concentrar nos objetivos de negócios do projeto.