Comment nous avons créé des rapports dynamiques au SSRS 2014


Nous avons déjà expliqué comment nous avons aidé une entreprise manufacturière à transformer les processus de formation en entreprise et de développement du personnel. Les employés du client, qui se noyaient dans des documents papier et des feuilles de calcul Excel, ont reçu une application iPad pratique et un portail Web. L'une des fonctions les plus importantes de ce produit est la création de rapports dynamiques par lesquels les managers jugent le travail des salariés «sur le terrain». Ce sont d'énormes documents avec des dizaines de champs et des tailles moyennes de 3000 * 1600 pixels.

Dans cet article, nous expliquerons comment déployer cette beauté basée sur Microsoft SQL Server Reporting Services, pourquoi un tel backend peut être de mauvais amis avec le portail Web et quelles astuces aideront à établir leur relation. L'ensemble de la partie commerciale de la solution a déjà été décrite dans l'article précédent, nous nous concentrons donc ici sur les problèmes techniques. Commençons!


Formulation du problème


Nous avons un portail avec lequel plusieurs centaines d'utilisateurs travaillent. Ils sont organisés dans une hiérarchie par étapes, où chaque utilisateur a un superviseur d'un rang supérieur. Cette différenciation des droits est nécessaire pour que les utilisateurs puissent créer des événements avec n'importe quel employé subordonné. Vous pouvez sauter des étapes, c.-à-d. l'utilisateur peut commencer une activité avec un employé de tout rang inférieur à lui.

Quels événements signifient ici? Il peut s'agir de formation, de soutien ou de certification d'un employé d'une société commerciale, que le superviseur effectue dans un point de vente. Le résultat d'un tel événement est un questionnaire rempli sur un iPad avec des notes des employés pour les qualités et compétences professionnelles.

Selon les données du questionnaire, vous pouvez préparer des statistiques, par exemple:

  • Combien d'événements avec ses subordonnés de ce genre Vasya Ivanov a-t-il créés en un mois? Combien d'entre eux sont terminés?
  • Quel est le pourcentage de notes satisfaisantes? À quelles questions les marchandiseurs répondent-ils le pire? Quel manager est le pire à passer des tests?

Ces statistiques sont contenues dans des rapports qui peuvent être créés via l'interface Web, aux formats XLS, PDF, DOCX et imprimés. Toutes ces fonctions sont conçues pour les managers de différents niveaux.

Le contenu et la conception des rapports sont définis dans les modèles , vous permettant de définir les paramètres nécessaires. Si à l'avenir les utilisateurs auront besoin de nouveaux types de rapports, le système a la possibilité de créer des modèles, de spécifier des paramètres modifiables et d'ajouter un modèle au portail. Tout cela - sans interférer avec le code source et les processus de travail du produit.

Spécifications et limitations


Le portail fonctionne sur une architecture de microservices, la face avant est écrite en angulaire 5. La ressource utilise l'autorisation JWT, prend en charge les navigateurs Google Chrome, Firefox, Microsoft Edge et IE 11 .

Toutes les données sont stockées sur MS SQL Server 2014. SQL Server Reporting Services (SSRS) est installé sur le serveur, le client l'utilise et ne va pas refuser. D'où la limitation la plus importante: l'accès à SSRS est fermé de l'extérieur, vous ne pouvez donc accéder à l'interface Web et SOAP qu'à partir du réseau local via l'autorisation NTLM.

Quelques mots sur SSRS
SSRS – , , . docs.microsoft.com, SSRS (API) ( Report Server, - HTTP).

Attention, la question: comment réaliser la tâche sans méthodes manuelles, avec un minimum de ressources et un maximum d'avantages pour le client?

Étant donné que le client a SSRS sur un serveur dédié, laissez SSRS faire tout le sale travail de génération et d'exportation de rapports. Ensuite, nous n'avons pas à rédiger notre propre service de création de rapports, à exporter des modules vers XLS, PDF, DOCX, HTML et l'API correspondante.

Ainsi, la tâche était de se lier d'amitié avec le portail SSRS et d'assurer le fonctionnement des fonctions spécifiées dans la tâche. Passons donc en revue la liste de ces scénarios - des subtilités intéressantes ont été trouvées dans presque tous les points.

Structure de la solution


Comme nous avons déjà SSRS, il existe tous les outils pour gérer les modèles de rapport:

  • Report Server - responsable de toute la logique de travail avec les rapports, leur stockage, leur génération, leur gestion et bien plus encore.
  • Report Manager - un service avec une interface Web pour gérer les rapports. Ici, vous pouvez télécharger des modèles créés dans SQL Server Data Tools sur le serveur, configurer les droits d'accès, les sources de données et les paramètres (y compris ceux qui peuvent être modifiés lors du rapport des demandes). Il est capable de générer des rapports sur les modèles téléchargés et de les télécharger dans différents formats, y compris XLS, PDF, DOCX et HTML.

Total: nous créons des modèles dans SQL Server Data Tools, avec l'aide de Report Manager, nous les remplissons sur Report Server, nous configurons - et il est prêt. Nous pouvons générer des rapports, changer leurs paramètres.

La question suivante: comment demander la génération de rapports sur des modèles spécifiques via le portail et obtenir le résultat à l'avant pour la sortie vers l'interface utilisateur ou le télécharger dans le format souhaité?

Rapports de SSRS au portail


Comme nous l'avons dit ci-dessus, SSRS possède sa propre API pour accéder aux rapports. Mais nous ne voulons pas donner ses fonctions pour des raisons de sécurité et d'hygiène numérique - nous avons seulement besoin de demander des données au SSRS sous la bonne forme et de transmettre le résultat à l'utilisateur. La gestion des rapports sera assurée par un personnel client spécialement formé.

Étant donné que l'accès à SSRS se fait uniquement à partir du réseau local, l'échange de données entre le serveur et le portail s'effectue via un service proxy.


Échange de données entre le portail et le serveur

Voyons comment cela fonctionne et pourquoi ReportProxy est ici.

Donc, du côté du portail, nous avons un ReportService, auquel le portail accède pour les rapports. Le service vérifie l'autorisation de l'utilisateur, le niveau de ses droits, convertit les données du SSRS sous la forme souhaitée dans le cadre du contrat.

L'API ReportService ne contient que 2 méthodes, ce qui nous suffit:

  1. GetReports - fournit les identifiants et les noms de tous les modèles que l'utilisateur actuel peut recevoir;
  2. GetReportData (format, params) - fournit des données de rapport exportées prêtes à l'emploi dans le format spécifié, avec un ensemble de paramètres donné.

Vous avez maintenant besoin de ces 2 méthodes pour pouvoir communiquer avec SSRS et en extraire les données nécessaires sous la bonne forme. D'après la documentation, nous savons que nous pouvons accéder au serveur de rapports via HTTP à l'aide de l'API SOAP. Il semble que le puzzle se développe ... Mais en fait, une surprise nous attend ici.

Étant donné que SSRS est fermé au monde extérieur et que vous ne pouvez y accéder que via l'authentification NTLM, il n'est pas disponible directement à partir du portail SOAP. Il y a aussi nos propres souhaits:

  • Donner accès uniquement à l'ensemble de fonctions requis et même interdire le changement;
  • Si vous devez passer à un autre système de génération de rapports, les modifications dans ReportService doivent être minimes et mieux ne pas être requises du tout.

C'est là que ReportProxy nous aide, qui est situé sur la même machine que SSRS et est responsable du proxy des demandes de ReportService à SSRS. Le traitement des demandes est le suivant:

  1. le service reçoit une demande de ReportService, vérifie l'autorisation JWT;
  2. conformément à la méthode API, le proxy passe par le protocole SOAP dans SSRS pour les données nécessaires, se connectant via NTLM en cours de route;
  3. Les données reçues de SSRS sont renvoyées à ReportService en réponse à la demande.

En fait, ReportProxy est un adaptateur entre SSRS et ReportService.
Le contrôleur est le suivant:
[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:"
        );
    }
}


Par conséquent, le processus ressemble à ceci:

  1. Le front envoie une requête http à ReportService;
  2. ReportService envoie une demande http à ReportProxy;
  3. ReportProxy via l'interface SOAP reçoit les données de SSRS et envoie le résultat à ReportService;
  4. ReportService apporte le résultat conformément au contrat et le remet au client.

Nous avons un système de travail qui demande une liste des modèles disponibles, va au SSRS pour les rapports et les donne à l'avant dans tous les formats pris en charge. Vous devez maintenant afficher les rapports générés sur le devant conformément aux paramètres spécifiés, donner la possibilité de les télécharger sur des fichiers XLS, PDF, DOCX et imprimer. Commençons par l'affichage.

Utilisation des rapports SSRS dans le portail


À première vue, c'est une affaire de tous les jours - le rapport est au format HTML, nous pouvons donc en faire ce que nous voulons! Nous allons l'intégrer dans la page, le colorer avec des styles de conception, et le truc est dans le chapeau. En fait, il s'est avéré qu'il y avait suffisamment d'écueils.

Selon le concept de conception, la section des rapports sur le portail devrait comprendre deux pages:

1) une liste de modèles où nous pouvons:

  • Afficher des statistiques sur les activités pour l'ensemble du portail;
  • voir tous les modèles à notre disposition;
  • cliquez sur le modèle souhaité et accédez au générateur de rapport correspondant.



2) un générateur de rapports qui nous permet de:

  • définir des paramètres de modèle et créer un rapport à leur sujet;
  • voir ce qui s'est passé en conséquence;
  • sélectionnez le format de fichier de sortie, téléchargez-le;
  • imprimer le rapport sous une forme pratique et visuelle.



Il n'y a pas eu de problème particulier avec la première page, nous ne l'examinerons donc pas plus avant. Et le générateur de rapports nous a forcés à allumer l'ingénieur, afin qu'il soit pratique pour de vraies personnes d'utiliser toutes les fonctions sur TK.

Problème numéro 1. Tables géantes


Selon le concept de conception, cette page devrait avoir une zone de visualisation afin que l'utilisateur puisse voir son rapport avant d'exporter. Si le rapport ne rentre pas dans la fenêtre, vous pouvez faire défiler horizontalement et verticalement. Dans le même temps, un rapport typique peut atteindre plusieurs tailles d'écran, ce qui signifie que nous avons besoin de coller des blocs avec les noms des lignes et des colonnes. Sans cela, les utilisateurs devront constamment revenir en haut du tableau pour se rappeler ce que signifie une cellule particulière. Ou en général, il sera plus facile d'imprimer un rapport et de garder constamment les feuilles nécessaires devant vos yeux, mais le tableau à l'écran perd tout simplement son sens.

En général, les blocs collants ne peuvent pas être évités. Et SSRS 2014 ne sait pas comment réparer les lignes et les colonnes dans un document MHTML - uniquement dans sa propre interface Web.

Ici, nous rappelons que les navigateurs modernes prennent en charge la propriété collante CSS , qui fournit simplement la fonction dont nous avons besoin. Nous mettons position: collant sur le bloc marqué, spécifions le retrait à gauche ou en haut (propriétés gauche, haut), et le bloc restera en place pendant le défilement horizontal et vertical.

Vous devez trouver un paramètre auquel CSS peut accéder. Les valeurs de cellule personnalisées qui permettent à SSRS 2014 de les capturer dans l'interface Web sont perdues lors de l'exportation au format HTML. OK, nous les marquerons nous-mêmes - nous ne comprendrons que comment.

Après plusieurs heures de lecture de documentation et de discussions avec des collègues, il semblait qu'il n'y avait pas d'options. Et ici, selon toutes les lois de l'intrigue, le champ Info-bulle s'est affiché pour nous, ce qui nous permet de spécifier des info-bulles pour les cellules. Il s'est avéré qu'il est jeté dans le code HTML exporté dans l'attribut info-bulle - exactement sur la balise qui appartient à la cellule personnalisée dans SQL Server Data Tools. Il n'y avait pas d'autre choix - nous n'avons pas trouvé d'autre moyen de marquer les cellules pour la fixation.

Vous devez donc créer des règles de marquage et des marqueurs de transfert en HTML via Info-bulle. Ensuite, en utilisant JS, nous modifions l'attribut info-bulle à la classe CSS au marqueur spécifié.

Il n'y a que deux façons de corriger les cellules: verticalement (colonne fixe) et horizontalement (ligne fixe). Il est logique de placer un autre marqueur sur les cellules des coins, qui restent en place lors du défilement dans les deux directions - fixe-les deux.

L'étape suivante consiste à faire l'interface utilisateur. Lorsque vous recevez un document HTML, vous devez trouver tous les éléments HTML contenant des marqueurs, reconnaître les valeurs, définir la classe CSS appropriée et supprimer l'attribut info-bulle afin qu'il ne sorte pas lorsque vous survolez la souris. Il convient de noter que le balisage résultant est constitué de tables imbriquées (balises de table).

Afficher le code
type 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'); //   SSRS

        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);
    }
}


Et voici un nouveau problème: avec le comportement en cascade, lorsque plusieurs blocs se déplaçant dans une direction sont fixés à la fois dans le tableau, les cellules qui se succèdent seront superposées. En même temps, il n'est pas clair combien chaque bloc suivant devrait reculer - les retraits devront être calculés via JavaScript en fonction de la hauteur du bloc devant lui. Tout cela s'applique aux ancrages verticaux et horizontaux.

Le script de correction a résolu le problème.
//      
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';
        }
    }
}


Le code vérifie les balises des éléments marqués et ajoute les paramètres des cellules fixes à la valeur de retrait. Dans le cas de rangées adhérentes, leur hauteur s'additionne, pour les colonnes, leur largeur.


Exemple de rapport avec une ligne supérieure fixe.

Par conséquent, le processus ressemble à ceci:

  1. Nous obtenons le balisage de SSRS et le collons au bon endroit dans le DOM;
  2. Reconnaître les marqueurs;
  3. Ajustez les paramètres du comportement en cascade.

Étant donné que le comportement de collage est entièrement implémenté via CSS et que JS n'est impliqué que dans la préparation du document entrant, la solution fonctionne assez rapidement et sans décalage.

Malheureusement, pour IE, les blocs collants devaient être désactivés car il ne supporte pas la position: propriété collante. Le reste - Safari, Mozilla Firefox et Chrome - fait un excellent travail.

Passez.

Problème numéro 2. Exportation de rapport


Pour extraire un rapport du système, vous devez (1) accéder au SSRS via ReportService pour un objet Blob, (2) obtenir un lien vers l'objet via l'interface à l'aide de la méthode window.URL.createObjectURL, (3) mettre le lien dans la balise et simuler un clic pour téléchargement de fichiers.

Cela fonctionne dans Firefox, Safari et dans toutes les versions de Chrome sauf Apple. Pour que IE, Edge et Chrome pour iOS prennent également en charge la fonction, j'ai dû jeter mon cerveau.

Dans IE et Edge, l'événement ne déclenchera tout simplement pas une demande de navigateur pour télécharger le fichier. Ces navigateurs ont une telle fonctionnalité que pour simuler un clic, une confirmation de l'utilisateur à télécharger est requise, ainsi qu'une indication claire des actions futures. La solution a été trouvée dans la méthode window.navigator.msSaveOrOpenBlob (), qui est disponible dans IE et Edge. Il sait juste comment demander l'autorisation de l'utilisateur pour l'opération et clarifier quoi faire ensuite. Ainsi, nous déterminons si la méthode window.navigator.msSaveOrOpenBlob existe et agissons sur la situation.

Chrome sur iOS n'avait pas un tel hack, et au lieu d'un rapport, nous avons juste une page vierge. En nous promenant sur le Web, nous avons trouvé une histoire similaire, à en juger par laquelle, dans iOS 13, ce bogue aurait dû être corrigé. Malheureusement, nous avons écrit l'application à l'époque d'iOS 12, donc à la fin nous avons décidé de ne plus perdre de temps et avons simplement désactivé le bouton dans Chrome pour iOS.
Maintenant, à quoi ressemble le processus d'exportation final vers l'interface utilisateur. Il y a un bouton dans le composant de rapport angulaire qui lance une chaîne d'étapes:

  • à travers les paramètres de l'événement, le gestionnaire reçoit l'identifiant du format d'exportation (par exemple, «PDF»);
  • Envoie une demande à ReportService pour recevoir un objet Blob pour le format spécifié;
  • vérifie si le navigateur est IE ou Edge;
  • lorsque la réponse vient de ReportService:
    • si c'est IE ou Edge, il appelle window.navigator.msSaveOrOpenBlob (fileStream, fileName);
    • sinon, il appelle la méthode this.exportDownload (fileStream, fileName), où fileStream est le Blob obtenu à partir de la demande à ReportService et fileName est le nom du fichier à enregistrer. La méthode crée une balise cachée avec un lien vers window.URL.createObjectURL (fileStream), simule un clic et supprime la balise.

Avec cela réglé, la dernière aventure est restée.

Problème numéro 3. Imprimer


Nous pouvons maintenant voir le rapport sur le portail et l'exporter aux formats XLS, PDF, DOCX. Il reste à mettre en œuvre l'impression du document afin d'obtenir un rapport multipage précis. Si le tableau s'avérait être divisé en pages, chacune d'entre elles devrait contenir des titres - les mêmes blocs collants dont nous avons parlé dans la section avant-dernier.

L'option la plus simple consiste à prendre la page actuelle avec le rapport affiché, à masquer tout ce qui est superflu à l'aide de CSS et à l'envoyer à l'impression à l'aide de la méthode window.print (). Cette méthode ne fonctionne pas immédiatement pour plusieurs raisons:

  1. Zone de visualisation non standard - le rapport lui-même est contenu dans une zone de défilement séparée afin que la page ne s'étire pas à des dimensions horizontales incroyables. L'utilisation de window.print () ajuste le contenu qui ne correspond pas à l'écran;
  2. , ;
  3. , .

Tout cela peut être résolu en utilisant JS et CSS, mais nous avons décidé de faire gagner du temps aux développeurs et de chercher une alternative à window.print ().

SSRS peut immédiatement nous donner un PDF prêt à l'emploi avec une pagination présentable. Cela nous évite toutes les difficultés de la version précédente, la seule question est, pouvons-nous imprimer le PDF via un navigateur?

Le PDF étant une norme tierce, les navigateurs le prennent en charge via divers plugins de visualisation. Pas de plug-in - pas de dessins animés, nous avons donc encore besoin d'une option alternative.

Et si vous mettez le PDF sur la page en tant qu'image et envoyez cette page à imprimer? Il existe déjà des bibliothèques et des composants pour Angular qui fournissent un tel rendu. Recherche, expérimentation, mise en œuvre.

Afin de ne pas traiter les données que nous ne voulons pas imprimer, il a été décidé de transférer le contenu rendu sur une nouvelle page, et là déjà exécuter window.print (). En conséquence, l'ensemble du processus est le suivant:

  1. Demandez à ReportService d'exporter le rapport au format PDF;
  2. Nous obtenons l'objet Blob, le convertissons en URL (URL.createObjectURL (fileStream)), donnons l'URL à la visionneuse PDF pour le rendu;
  3. Nous prenons des images de la visionneuse PDF;
  4. Ouvrez une nouvelle page et ajoutez-y un petit balisage (titre, une petite indentation);
  5. Ajoutez l'image de la visionneuse PDF au balisage, appelez window.print ().

Après plusieurs vérifications, un code JS est également apparu sur la page, qui, avant l'impression, vérifie que toutes les images ont été chargées.

Ainsi, l'apparence entière du document est déterminée par les paramètres du modèle SSRS et l'interface utilisateur n'interfère pas avec ce processus. Cela réduit le nombre de bogues possibles. Étant donné que les images sont transférées pour impression, nous sommes assurés contre tout dommage ou déformation de la mise en page.

Il y a aussi des inconvénients:

  • un rapport volumineux pèsera beaucoup, ce qui affectera négativement les plateformes mobiles;
  • la conception ne se met pas à jour automatiquement - les couleurs, polices et autres éléments de conception doivent être installés au niveau du modèle.

Dans notre cas, l'ajout fréquent de nouveaux modèles n'était pas prévu, la solution était donc acceptable. Les performances mobiles ont été prises pour acquises.

Le dernier mot


C'est ainsi qu'un projet régulier nous fait à nouveau rechercher des solutions simples pour des tâches non triviales. Le produit final répond pleinement aux exigences de conception et est magnifique. Et plus important encore, bien que nous n'ayons pas eu à rechercher les méthodes de mise en œuvre les plus évidentes, la tâche s'est terminée plus rapidement que si nous prenions le module de rapport d'origine avec toutes les conséquences. Et à la fin, nous avons pu nous concentrer sur les objectifs commerciaux du projet.

Source: https://habr.com/ru/post/undefined/


All Articles