Esempio n. 1
0
        /// <summary>
        /// Crée un scénario cible à partir d'un autre scénario, initial ou cible.
        /// </summary>
        /// <param name="context">Le contexte.</param>
        /// <param name="projectId">L'identifiant du projet.</param>
        /// <param name="sourceScenarioId">L'identifiant du scénario source.</param>
        /// <param name="natureCode">Le code de la nature.</param>
        /// <param name="save"><c>true</c> pour sauvegarder le scénario créé.</param>
        /// <returns>
        /// Le scénario créé
        /// </returns>
        public static async Task <Scenario> CreateDerivatedScenario(KsmedEntities context, int projectId, int sourceScenarioId, string natureCode, bool save)
        {
            Scenario fromScenario;

            using (var tempContext = ContextFactory.GetNewContext())
            {
                // Charger les référentiels
                var referentialsUsed = await SharedScenarioActionsOperations.GetReferentialsUse(context, projectId);

                await Queries.LoadAllReferentialsOfProject(context, projectId, referentialsUsed);

                var videos = await context.Projects
                             .Include(nameof(Project.Process))
                             .Include($"{nameof(Project.Process)}.{nameof(Procedure.Videos)}")
                             .Where(p => p.ProjectId == projectId)
                             .SelectMany(p => p.Process.Videos)
                             .ToArrayAsync();

                fromScenario = await context.Scenarios.FirstAsync(s => s.ScenarioId == sourceScenarioId);

                await Queries.LoadScenariosDetails(context, EnumerableExt.Concat(fromScenario), referentialsUsed);
            }

            return(await CreateDerivatedScenario(context, fromScenario, natureCode, save));
        }
Esempio n. 2
0
        /// <summary>
        /// Crée un scénario cible à partir d'un autre scénario, initial ou cible.
        /// </summary>
        /// <param name="context">Le contexte.</param>
        /// <param name="sourceScenario">Le scenario source.</param>
        /// <param name="natureCode">Le code de la nature.</param>
        /// <param name="save"><c>true</c> pour sauvegarder le scénario créé.</param>
        /// <param name="targetNumber">Le numéro cible.</param>
        /// <returns>
        /// Le scénario créé
        /// </returns>
        public static async Task <Scenario> CreateDerivatedScenario(KsmedEntities context, Scenario sourceScenario, string natureCode, bool save, int targetNumber)
        {
            // Charger les données du scénario source
            var newScenario = new Scenario();

            ActionCloneBehavior cloneBehavior;

            if (sourceScenario.NatureCode == KnownScenarioNatures.Initial && natureCode == KnownScenarioNatures.Target)
            {
                cloneBehavior = ActionCloneBehavior.InitialToTarget;
            }
            else if (sourceScenario.NatureCode == KnownScenarioNatures.Target && natureCode == KnownScenarioNatures.Target)
            {
                cloneBehavior = ActionCloneBehavior.TargetToTarget;
            }
            else if (sourceScenario.NatureCode == KnownScenarioNatures.Target && natureCode == KnownScenarioNatures.Realized)
            {
                cloneBehavior = ActionCloneBehavior.TargetToRealized;
            }
            else if (sourceScenario.NatureCode == KnownScenarioNatures.Realized && natureCode == KnownScenarioNatures.Initial)
            {
                cloneBehavior = ActionCloneBehavior.RealizedToNewInitial;
            }
            else if (sourceScenario.NatureCode == KnownScenarioNatures.Target && natureCode == KnownScenarioNatures.Initial)
            {
                cloneBehavior = ActionCloneBehavior.TargetToNewInitial;
            }
            else if (sourceScenario.NatureCode == KnownScenarioNatures.Initial && natureCode == KnownScenarioNatures.Initial)
            {
                cloneBehavior = ActionCloneBehavior.InitialToNewInitial;
            }
            else
            {
                throw new InvalidOperationException("Conversion impossible pour ces scénarios");
            }
            switch (natureCode)
            {
            case KnownScenarioNatures.Target:
                newScenario.Label = LocalizationManager.GetString("Business_AnalyzeService_TargetScenarioLabel") + " " + targetNumber;
                break;

            case KnownScenarioNatures.Realized:
                newScenario.Label = LocalizationManager.GetString("Business_AnalyzeService_ValidationScenarioLabel");
                break;

            case KnownScenarioNatures.Initial:
                newScenario.Label = LocalizationManager.GetString("Business_AnalyzeService_InitialScenarioLabel");
                break;

            default:
                throw new ArgumentOutOfRangeException(nameof(natureCode));
            }

            newScenario.StateCode  = KnownScenarioStates.Draft;
            newScenario.NatureCode = natureCode;
            if (cloneBehavior != ActionCloneBehavior.RealizedToNewInitial &&
                cloneBehavior != ActionCloneBehavior.TargetToNewInitial &&
                cloneBehavior != ActionCloneBehavior.InitialToNewInitial)
            {
                newScenario.ProjectId          = sourceScenario.ProjectId;
                newScenario.Original           = sourceScenario;
                newScenario.OriginalScenarioId = sourceScenario.ScenarioId;
            }
            newScenario.IsShownInSummary      = true;
            newScenario.CriticalPathIDuration = sourceScenario.CriticalPathIDuration;

            string[] scenarioLAbels = await EnsureCanShowScenarioInSummary(newScenario, true);

            // Copier toutes les actions
            foreach (var action in sourceScenario.Actions.ToArray())
            {
                var newAction = CloneAction(action, cloneBehavior);

                newAction.OriginalActionId = action.ActionId;
                newAction.Original         = action;
                if (newAction.Reduced != null)
                {
                    newAction.Reduced.OriginalBuildDuration = action.BuildDuration;
                }

                // S'il s'agit d'un scénario validé, utiliser les temps process en tant que temps vidéo
                if (cloneBehavior == ActionCloneBehavior.TargetToRealized)
                {
                    newAction.Start  = newAction.BuildStart;
                    newAction.Finish = newAction.BuildFinish;
                }

                newScenario.Actions.Add(newAction);
            }

            SharedScenarioActionsOperations.EnsureEmptySolutionExists(newScenario);
            SharedScenarioActionsOperations.UdpateSolutionsApprovedState(newScenario);

            // Copier les liens prédécesseurs successeurs
            foreach (var action in sourceScenario.Actions.ToArray())
            {
                var newAction = newScenario.Actions.FirstOrDefault(a => a.OriginalActionId == action.ActionId);
                if (newAction != null)
                {
                    foreach (var predecessor in action.Predecessors)
                    {
                        var newPredecessor = newScenario.Actions.FirstOrDefault(a => a.OriginalActionId == predecessor.ActionId);
                        if (newPredecessor != null)
                        {
                            newAction.Predecessors.Add(newPredecessor);
                        }
                    }
                }
            }

            //Suppression des actions avec durée = 0
            if (cloneBehavior == ActionCloneBehavior.TargetToRealized || cloneBehavior == ActionCloneBehavior.TargetToTarget)
            {
                ActionsRecursiveUpdate.RemoveEmptyDurationActionsAndGroupsFromNewScenario(newScenario);
            }

            if (cloneBehavior != ActionCloneBehavior.TargetToRealized &&        // ToDelete ne s'applique pas aux scenarios validés
                cloneBehavior != ActionCloneBehavior.InitialToNewInitial &&     // ToDelete ne s'applique pas aux scenarios initiaux
                cloneBehavior != ActionCloneBehavior.TargetToNewInitial &&      // ToDelete ne s'applique pas aux scenarios initiaux
                cloneBehavior != ActionCloneBehavior.RealizedToNewInitial)      // ToDelete ne s'applique pas aux scenarios initiaux
            {
                foreach (var newAction in newScenario.Actions)
                {
                    // Si la category associée est dite "à supprimer", modifier la tache optimisée à "à supprimer"
                    if (newAction.Category != null && newAction.Category.ActionTypeCode == KnownActionCategoryTypes.S)
                    {
                        SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.S);
                    }
                }
            }

            if (save)
            {
                context.Scenarios.ApplyChanges(newScenario);
                await context.SaveChangesAsync();
            }

            ActionsTimingsMoveManagement.FixPredecessorsSuccessorsTimings(newScenario.Actions.ToArray(), false);
            ActionsTimingsMoveManagement.UpdateVideoGroupsTiming(newScenario.Actions.ToArray());
            ActionsTimingsMoveManagement.UpdateBuildGroupsTiming(newScenario.Actions.ToArray());
            newScenario.CriticalPathIDuration = ActionsTimingsMoveManagement.GetInternalCriticalPathDuration(newScenario);

            // Supprimer les liens vers les originaux car dans un autre projet
            if (cloneBehavior == ActionCloneBehavior.RealizedToNewInitial ||
                cloneBehavior == ActionCloneBehavior.TargetToNewInitial ||
                cloneBehavior == ActionCloneBehavior.InitialToNewInitial)
            {
                foreach (var action in newScenario.Actions)
                {
                    action.Original         = null;
                    action.OriginalActionId = null;
                }

                newScenario.Original           = null;
                newScenario.OriginalScenarioId = null;
            }

            return(newScenario);
        }
Esempio n. 3
0
        /// <summary>
        /// Clone une action pour qu'elle soit utilisée dans un nouveau scénario.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="cloneBehavior">The clone behavior.</param>
        /// <returns></returns>
        public static KAction CloneAction(KAction action, ActionCloneBehavior cloneBehavior)
        {
            var newAction = new KAction();

            var originalValues = action.GetCurrentValues();

            // Ignorer certaines propriétés
            var excludedPropertyNames = new List <string> {
                "ActionId", "ScenarioId", "OriginalActionId", "Predecessors", "Successors", "Reduced"
            };

            // Quand on passe vers un scénario de validation, il ne faut pas copier la vignette car on supprime les leins avec les vidéos
            if (cloneBehavior == ActionCloneBehavior.TargetToRealized)
            {
                excludedPropertyNames.Add("Thumbnail");
                excludedPropertyNames.Add("IsThumbnailSpecific");
                excludedPropertyNames.Add("ThumbnailPosition");
            }

            // Vérifier que ces noms de propriétés soient corrects
            if (excludedPropertyNames.Except(originalValues.Keys).Any())
            {
                throw new InvalidOperationException("Les noms de propriétés présents dans excludedPropertyNames ne sont pas valides.");
            }

            foreach (var kvp in originalValues)
            {
                if (!excludedPropertyNames.Contains(kvp.Key))
                {
                    newAction.SetPropertyValue(kvp.Key, kvp.Value);
                }
            }

            // Copier les liens actions / référentiel
            CloneReferentialActionsLinks(action, newAction);

            // Copier la partie Reduced
            switch (cloneBehavior)
            {
            case ActionCloneBehavior.InitialToTarget:
            {
                // S'il existe un prétypage par la catégorie, il est prioritaire. Sinon, utiliser celui sur l'action s'il y en a.
                string actionTypeCode;

                if (action.Category != null && action.Category.ActionTypeCode != null)
                {
                    actionTypeCode = action.Category.ActionTypeCode;
                }
                else
                {
                    actionTypeCode = action.IsReduced ? action.Reduced.ActionTypeCode : KnownActionCategoryTypes.I;
                }

                SharedScenarioActionsOperations.ApplyNewReduced(newAction, actionTypeCode);
            }
            break;

            case ActionCloneBehavior.TargetToTarget:

                if (action.Reduced.ActionTypeCode == null)
                {
                    throw new InvalidOperationException("Une action cible doit toujours être réduite avec un type");
                }

                if (!ActionsTimingsMoveManagement.GetIsSolutionApproved(action).GetValueOrDefault(true))
                {
                    // Tache I provenant du grand parent, on n'applique pas le prétype mais on applique la réduc
                    SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.I);
                }
                else
                {
                    switch (action.Reduced.ActionTypeCode)
                    {
                    case KnownActionCategoryTypes.I:
                        // Déterminer si la tâche provient d'un scénario ancêtre
                        if (action.OriginalActionId != null)
                        {
                            // Tache I provenant du grand parent, on n'applique pas le prétype mais on applique la réduc
                            SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.I);
                        }
                        else
                        {
                            // Tache I provenant du parent, on applique le prétypage et la réduc s'il y en a

                            string actionTypeCode;

                            if (action.Category != null && action.Category.ActionTypeCode != null)
                            {
                                actionTypeCode = action.Category.ActionTypeCode;
                            }
                            else
                            {
                                actionTypeCode = KnownActionCategoryTypes.I;
                            }

                            SharedScenarioActionsOperations.ApplyNewReduced(newAction, actionTypeCode);
                        }

                        break;

                    case KnownActionCategoryTypes.E:
                        // Tâche E, on conserve E et on applique la réduc
                        SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.E);
                        break;

                    case KnownActionCategoryTypes.S:
                        SharedScenarioActionsOperations.ApplyReduced(action, newAction);
                        break;

                    default:
                        throw new ArgumentOutOfRangeException(nameof(action), new ArgumentOutOfRangeException(nameof(KAction.Reduced), new ArgumentOutOfRangeException(nameof(KActionReduced.ActionTypeCode))));
                    }
                }

                break;

            case ActionCloneBehavior.TargetToRealized:

                // Pour les tâches externes, il faut les faire réapparaitre comme externes
                // Pour les tâches internes, il faut qu'elle aient une partie réduite pour pouvoir les repasser à exerne.
                switch (action.Reduced.ActionTypeCode)
                {
                case KnownActionCategoryTypes.I:
                    SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.I);
                    break;

                case KnownActionCategoryTypes.E:
                    SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.E);
                    break;
                }

                break;


            case ActionCloneBehavior.Cascade:
                SharedScenarioActionsOperations.ApplyReduced(action, newAction);
                break;

            case ActionCloneBehavior.RealizedToNewInitial:
            case ActionCloneBehavior.TargetToNewInitial:
            case ActionCloneBehavior.InitialToNewInitial:

                // Pour les tâches externes, il faut les faire réapparaitre comme externes
                // Pour les tâches internes, il faut qu'elle aient une partie réduite pour pouvoir les repasser à exerne.
                if (action.Reduced != null)
                {
                    switch (action.Reduced.ActionTypeCode)
                    {
                    case KnownActionCategoryTypes.I:
                        SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.I);
                        break;

                    case KnownActionCategoryTypes.E:
                        SharedScenarioActionsOperations.ApplyNewReduced(newAction, KnownActionCategoryTypes.E);
                        break;
                    }
                }

                break;


            default:
                throw new ArgumentOutOfRangeException(nameof(cloneBehavior));
            }

            return(newAction);
        }
Esempio n. 4
0
        /// <summary>
        /// Met à jour les actions récursivement sur les scénarios dérivés de celui spécifié.
        /// </summary>
        /// <param name="context">Le contexte EF.</param>
        /// <param name="sourceScenario">Le scénario source.</param>
        /// <param name="allScenarios">Tous les scénarios qui peuvent être impactés.</param>
        /// <param name="actionsToRemove">Les actions à supprimer manuellement.</param>
        internal static void UpdateActions(KsmedEntities context, Scenario sourceScenario, Scenario[] allScenarios,
                                           out KAction[] actionsToRemove, out IList <KAction> actionsWithOriginal)
        {
            var derivedScenarios = ScenarioActionHierarchyHelper.GetDerivedScenarios(sourceScenario, allScenarios);

            var actions = GetActionsSortedWBS(sourceScenario);

            actionsWithOriginal = new List <KAction>();

            foreach (var scenario in derivedScenarios)
            {
                // Mettre à jour IsGroup
                foreach (var action in scenario.Actions)
                {
                    action.IsGroup = WBSHelper.HasChildren(action, scenario.Actions);
                }
            }

            foreach (var originalAction in actions)
            {
                // J'enlève le IsMArkedAsModified car les 2 références sont la sauvegarde des actions depuis la construction et depuis l'optimisation
                // Or depuis la construction, en modification, le bout de code cidessous est déjà appelé
                // Et depuis l'optimisation, il n'y a pas de changement de temps video
                if (originalAction.IsMarkedAsAdded /*|| originalAction.IsMarkedAsModified*/)
                {
                    var originalValues = originalAction.ChangeTracker.OriginalValues;
                    var modifiedValues = originalAction.ChangeTracker.ModifiedValues;

                    if (originalAction.IsMarkedAsAdded || modifiedValues.ContainsKey(ActionsTimingsMoveManagement.KActionStartPropertyName) || modifiedValues.ContainsKey(ActionsTimingsMoveManagement.KActionFinishPropertyName))
                    {
                        // Vérifier si le temps vidéo a changé
                        ActionsTimingsMoveManagement.GetOrignalModifiedVideoDurations(originalAction, out long originalDuration, out long modifiedDuration);

                        bool hasVideoDurationChanged = originalDuration != modifiedDuration;

                        // Si c'est une tâche créée et non dupliquée, le buildDuration est à 0, donc on doit le mettre à jour
                        //Sinon, si c'est une tâche dupliquée, on le laisse tel quel.
                        if (originalAction.BuildDuration == 0)
                        {
                            var paceRating = originalAction.Resource != null ? originalAction.Resource.PaceRating : 1d;
                            originalAction.BuildDuration = Convert.ToInt64(modifiedDuration * paceRating);
                        }
                    }
                }

                if (originalAction.IsMarkedAsAdded)
                {
                    // Si l'action est une action nouvelle dans un scénario cible, définir automatiquement la partie réduite
                    if (sourceScenario.NatureCode == KnownScenarioNatures.Target && originalAction.Reduced == null)
                    {
                        SharedScenarioActionsOperations.ApplyNewReduced(originalAction);
                    }

                    var originalActionKey    = context.CreateEntityKey(KsmedEntities.KActionsEntitySetName, originalAction);
                    var parentOriginalAction = WBSHelper.GetParent(originalAction, actions);

                    foreach (var derivedScenario in derivedScenarios)
                    {
                        var derivedActions = GetActionsSortedWBS(derivedScenario);

                        // Rechercher le parent dans le scénario dérivé
                        var parentDerivedAction = ScenarioActionHierarchyHelper.GetDerivedAction(parentOriginalAction, derivedScenario);

                        // Cloner l'action originale
                        var newAction = ScenarioCloneManager.CloneAction(originalAction, ActionCloneBehavior.Cascade);

                        // Assigner l'original
                        var originalActionForCurrentDerivedScenario = derivedScenario.Original == sourceScenario ? originalAction :
                                                                      ScenarioActionHierarchyHelper.GetDerivedAction(originalAction, derivedScenario.Original);
                        newAction.Original = originalActionForCurrentDerivedScenario;
                        actionsWithOriginal.Add(newAction);

                        // Insérer l'action clonée dans le scénario dérivé
                        ActionsTimingsMoveManagement.InsertUpdateWBS(
                            derivedActions, newAction, parentDerivedAction, WBSHelper.GetParts(originalAction.WBS).Last(),
                            (a, wbs) => EnsureTracking(a));

                        // Rafraichir les actions
                        derivedScenario.Actions.Add(newAction);
                        derivedActions = GetActionsSortedWBS(derivedScenario);

                        // Ajouter les mêmes prédécesseurs et successeurs
                        foreach (var originalPredecessor in originalAction.Predecessors)
                        {
                            var derivedPredecessor = ScenarioActionHierarchyHelper.GetDerivedAction(originalPredecessor, derivedScenario);
                            if (derivedPredecessor != null)
                            {
                                EnsureTracking(derivedPredecessor);
                                ActionsTimingsMoveManagement.AddPredecessor(derivedActions, newAction, derivedPredecessor);
                            }
                        }

                        foreach (var originalSuccessor in originalAction.Successors)
                        {
                            var derivedSuccessor = ScenarioActionHierarchyHelper.GetDerivedAction(originalSuccessor, derivedScenario);
                            if (derivedSuccessor != null)
                            {
                                EnsureTracking(derivedSuccessor);
                                ActionsTimingsMoveManagement.AddPredecessor(derivedActions, derivedSuccessor, newAction);
                            }
                        }

                        EnsureTracking(derivedScenario);
                        SharedScenarioActionsOperations.EnsureEmptySolutionExists(derivedScenario);
                        SharedScenarioActionsOperations.UdpateSolutionsApprovedState(derivedScenario);

                        ActionsTimingsMoveManagement.DebugCheckAllWBS(derivedActions);
                    }
                }
                else if (originalAction.IsMarkedAsModified)
                {
                    var originalValues         = originalAction.ChangeTracker.OriginalValues;
                    var modifiedValues         = originalAction.ChangeTracker.ModifiedValues;
                    var propertiesToCopyValues = new Dictionary <string, object>();

                    foreach (var propertyName in _kActionPropertyNamesToCopy)
                    {
                        if (modifiedValues.ContainsKey(propertyName))
                        {
                            propertiesToCopyValues[propertyName] = modifiedValues[propertyName];
                        }
                    }

                    // Vérifier si les reduced doit être impactés également
                    ActionsTimingsMoveManagement.GetOrignalModifiedBuildDurations(originalAction, out long originalDuration, out long modifiedDuration);

                    bool hasBuildDurationChanged = originalDuration != modifiedDuration;


                    foreach (var derivedScenario in derivedScenarios)
                    {
                        var derivedAction = ScenarioActionHierarchyHelper.GetDerivedAction(originalAction, derivedScenario);
                        if (derivedAction != null)
                        {
                            EnsureTracking(derivedAction);
                            foreach (var kvp in propertiesToCopyValues)
                            {
                                derivedAction.SetPropertyValue(kvp.Key, kvp.Value);
                            }

                            if (hasBuildDurationChanged)
                            {
                                if (derivedAction.IsReduced)
                                {
                                    // Modifier l'original duration et recalculer le temps final en fonction du gain
                                    EnsureTracking(derivedAction.Reduced);
                                    derivedAction.Reduced.OriginalBuildDuration = modifiedDuration;

                                    ActionsTimingsMoveManagement.UpdateTimingsFromReducedReduction(derivedAction);
                                }
                                else
                                {
                                    // Simplement recopier la durée
                                    derivedAction.BuildDuration = modifiedDuration;
                                }
                            }
                        }
                    }
                }
            }

            var toRemove = new List <KAction>();

            // Gérer les actions supprimées
            // EF gérant mal l'ordre des suppressions, ça créer une ConstraintException sur la FK OriginalActionId
            // Malheureusement un CascadeDelete est impossible puisque la FK est sur un même table
            if (sourceScenario.ChangeTracker.ObjectsRemovedFromCollectionProperties.ContainsKey("Actions"))
            {
                var removedActions = sourceScenario.ChangeTracker.ObjectsRemovedFromCollectionProperties["Actions"].ToArray();
                foreach (KAction originalAction in removedActions)
                {
                    EnsureTracking(originalAction);
                    toRemove.Add(originalAction);
                    originalAction.MarkAsUnchanged();

                    foreach (var derivedScenario in derivedScenarios)
                    {
                        var derivedAction = ScenarioActionHierarchyHelper.GetDerivedAction(originalAction, derivedScenario);
                        if (derivedAction != null)
                        {
                            var derivedActions = GetActionsSortedWBS(derivedScenario);

                            // Mettre à jour les WBS des autres actions
                            ActionsTimingsMoveManagement.DeleteUpdateWBS(derivedActions, derivedAction,
                                                                         (a, wbs) => EnsureTracking(a));
                            EnsureTracking(derivedAction);
                            toRemove.Add(derivedAction);
                        }
                    }
                }

                // Il faut maintenant trier les actions à supprimer pour que la suppression se fasse dans le bon ordre
                toRemove.Reverse();
                actionsToRemove = toRemove.ToArray();
            }
            else
            {
                actionsToRemove = new KAction[] { }
            };

            sourceScenario.CriticalPathIDuration = ActionsTimingsMoveManagement.GetInternalCriticalPathDuration(sourceScenario);
            foreach (var scenario in derivedScenarios)
            {
                EnsureTracking(scenario);
                ActionsTimingsMoveManagement.FixPredecessorsSuccessorsTimings(scenario.Actions.ToArray(), false);
                ActionsTimingsMoveManagement.UpdateVideoGroupsTiming(scenario.Actions.ToArray());
                ActionsTimingsMoveManagement.UpdateBuildGroupsTiming(scenario.Actions.ToArray());
                scenario.CriticalPathIDuration = ActionsTimingsMoveManagement.GetInternalCriticalPathDuration(scenario);
            }
        }