cancel
Showing results for 
Search instead for 
Did you mean: 

[Résolu] Dashlet tâches actives

ptitmat
Champ in-the-making
Champ in-the-making
Bonjour,

Je suis entrain de mettre en place une dashlet qui permettra de voir les taches actives de l'utilisateur et uniquement les siennes.

Je reprends actuellement le travail de Huberd dont un premier post avait été créé à l'adresse suivante http://forums.alfresco.com/fr/viewtopic.php?f=11&t=1947&p=9364&hilit=taskInstance#p9364

Etant nouveau sur le développement d'alfresco. J'ai quelques problèmes de configurations.

Je souhaiterais afficher une nouvelle dashlet que j'ai crée de manière identique à la dashlet tasks-active-dashlet.jsp

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="/WEB-INF/alfresco.tld" prefix="a" %>
<%@ taglib uri="/WEB-INF/repo.tld" prefix="r" %>

<h:outputText value="#{msg.no_tasks}" rendered="#{empty CustomWorkflowBean.allActiveTasks}" />

<a:richList id="personal-tasks-active-list" viewMode="details" value="#{CustomWorkflowBean.allActiveTasks}" var="r"
styleClass="recordSet" headerStyleClass="recordSetHeader" rowStyleClass="recordSetRow"
altRowStyleClass="recordSetRowAlt" width="100%" pageSize="10"
initialSortColumn="created" initialSortDescending="true"
rendered="#{not empty CustomWorkflowBean.allActiveTasks}" refreshOnBind="true">

<%– Primary column for details view mode –%>
<a:column id="col1" primary="true" width="200" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col1-sort" label="#{msg.description}" value="bpm:description" mode="case-insensitive" styleClass="header"/>
</f:facet>
<f:facet name="small-icon">
<a:actionLink id="col1-act1" value="#{r['bpm:description']}" image="/images/icons/workflow_task.gif" showLink="false"
actionListener="#{CustomWorflowBean.setupTaskDialog}" action="dialog:manageTask">
<f:param name="id" value="#{r.id}" />
<f:param name="type" value="#{r.type}" />
</a:actionLink>
</f:facet>
<a:actionLink id="col1-act2" value="#{r['bpm:description']}" actionListener="#{CustomWorflowBean.setupTaskDialog}"
action="dialog:manageTask">
<f:param name="id" value="#{r.id}" />
<f:param name="type" value="#{r.type}" />
</a:actionLink>
</a:column>

<%– Task type –%>
<a:column id="col2" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col2-sort" label="#{msg.type}" value="name" mode="case-insensitive" styleClass="header"/>
</f:facet>
<h:outputText id="col2-txt" value="#{r.name}" />
</a:column>

<%– Task owner column –%>
<a:column id="col2a" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col2a-sort" label="#{msg.owner}" value="cm:owner" styleClass="header"/>
</f:facet>
<h:outputText id="col2a-txt" value="#{r['cm:owner']}" />
</a:column>

<%– Task id column –%>
<a:column id="col3" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col3-sort" label="#{msg.id}" value="bpm:taskId" styleClass="header"/>
</f:facet>
<h:outputText id="col3-txt" value="#{r['bpm:taskId']}" />
</a:column>

<%– Created Date column –%>
<a:column id="col4" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col4-sort" label="#{msg.created}" value="created" styleClass="header"/>
</f:facet>
<h:outputText id="col4-txt" value="#{r.created}">
<a:convertXMLDate type="both" pattern="#{msg.date_time_pattern}" />
</h:outputText>
</a:column>

<%– Due date column –%>
<a:column id="col5" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col5-sort" label="#{msg.due_date}" value="bpm:dueDate" styleClass="header"/>
</f:facet>
<h:outputText id="col5-txt" value="#{r['bpm:dueDate']}">
<a:convertXMLDate type="both" pattern="#{msg.date_pattern}" />
</h:outputText>
</a:column>

<%– Status column –%>
<a:column id="col6" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col6-sort" label="#{msg.status}" value="bpm:status" styleClass="header"/>
</f:facet>
<h:outputText id="col6-txt" value="#{r['bpm:status']}" />
</a:column>

<%– Priority column –%>
<a:column id="col7" style="padding:2px;text-align:left">
<f:facet name="header">
<a:sortLink id="col7-sort" label="#{msg.priority}" value="bpm:priority" styleClass="header"/>
</f:facet>
<h:outputText id="col7-txt" value="#{r['bpm:priority']}" />
</a:column>

<%– Actions column –%>
<a:column id="col8" actions="true" style="padding:2px;text-align:left">
<f:facet name="header">
<h:outputText id="col8-txt" value="#{msg.actions}"/>
</f:facet>
<r:actions id="col8-actions" value="dashlet_todo_actions" context="#{r}" showLink="false"
styleClass="inlineAction" />
</a:column>

<a:dataPager styleClass="pager" />
</a:richList>

J'ai crée une nouvelle classe dans eclipse qui reprend l'exemple du code WorkflowBean (qui permet d'obtenir toutes les taches actives).
package lpr.alfresco.web.bean.workflow;

/*
* Copyright (C) 2005-2007 Alfresco Software Limited.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.

* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.

* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception.  You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.transaction.UserTransaction;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.cmr.workflow.WorkflowTask;
import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition;
import org.alfresco.service.cmr.workflow.WorkflowTaskQuery;
import org.alfresco.service.cmr.workflow.WorkflowTaskState;
import org.alfresco.service.cmr.workflow.WorkflowTransition;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.ParameterCheck;
import org.alfresco.web.app.Application;
import org.alfresco.web.bean.NavigationBean;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.web.bean.repository.TransientMapNode;
import org.alfresco.web.bean.repository.TransientNode;
import org.alfresco.web.bean.repository.User;
import org.alfresco.web.ui.common.Utils;
import org.alfresco.web.ui.common.component.UIActionLink;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
* Managed bean used for providing support for the workflow task dashlets
*
* @author gavinc
*/
public class CustomWorkflowBean
{
   protected NavigationBean navigationBean;
   protected NodeService nodeService;
   protected WorkflowService workflowService;
   protected List<Node> tasks;
   protected List<Node> activeTasks;
   protected List<Node> pooledTasks;
   protected List<Node> completedTasks;
  
   private static final Log logger = LogFactory.getLog(CustomWorkflowBean.class);
  
   public static final String BEAN_NAME = "CustomWorkflowBean";
  
  
   // ——————————————————————————
   // Bean Getters and Setters
  
   /**
    * Returns a list of nodes representing the "all" active tasks.
    *
    * @return List of all active tasks
    */
   public List<Node> getAllActiveTasks()
   {
      if (this.activeTasks == null)
      {
         // get the current username
         FacesContext context = FacesContext.getCurrentInstance();
         User user = Application.getCurrentUser(context);
         String userName = user.getUserName();
        
         UserTransaction tx = null;
         try
         {
            tx = Repository.getUserTransaction(context, true);
            tx.begin();
           
            // query for all active tasks
            WorkflowTaskQuery query = new WorkflowTaskQuery();
            List<WorkflowTask> tasks = this.workflowService.queryTasks(query);
           
            // create a list of transient nodes to represent
            this.activeTasks = new ArrayList<Node>(tasks.size());
            for (WorkflowTask task : tasks)
            {
               
               Node node = createTask(task);
               this.activeTasks.add(node);
              
               if (logger.isDebugEnabled())
                  logger.debug("Added active task: " + node);
            }
           
            // commit the changes
            tx.commit();
         }
         catch (Throwable e)
         {
            // rollback the transaction
            try { if (tx != null) {tx.rollback();} } catch (Exception ex) {}
            Utils.addErrorMessage("Failed to get all active tasks: " + e.toString(), e);
         }
      }
     
      return this.activeTasks;
   }
     
   /**
    * Returns a list of nodes representing the "pooled" to do tasks the
    * current user has.
    *
    * @return List of to do tasks
    */
  
   public List<Node> getPooledTasks()
   {
      if (this.pooledTasks == null)
      {
         // get the current username
         FacesContext context = FacesContext.getCurrentInstance();
         User user = Application.getCurrentUser(context);
         String userName = user.getUserName();
        
         UserTransaction tx = null;
         try
         {
            tx = Repository.getUserTransaction(context, true);
            tx.begin();
           
            // get the current pooled tasks for the current user
            List<WorkflowTask> tasks = this.workflowService.getPooledTasks(userName);
           
            // create a list of transient nodes to represent
            this.pooledTasks = new ArrayList<Node>(tasks.size());
            for (WorkflowTask task : tasks)
            {
               Node node = createTask(task);
               this.pooledTasks.add(node);
              
               if (logger.isDebugEnabled())
                  logger.debug("Added pooled task: " + node);
            }
           
            // commit the changes
            tx.commit();
         }
         catch (Throwable e)
         {
            // rollback the transaction
            try { if (tx != null) {tx.rollback();} } catch (Exception ex) {}
            Utils.addErrorMessage("Failed to get pooled tasks: " + e.toString(), e);
         }
      }
     
      return this.pooledTasks;
   }

   /**
    * Returns a list of nodes representing the to do tasks the
    * current user has.
    *
    * @return List of to do tasks
    */
   public List<Node> getTasksToDo()
   {
      if (this.tasks == null)
      {
         // get the current username
         FacesContext context = FacesContext.getCurrentInstance();
         User user = Application.getCurrentUser(context);
         String userName = user.getUserName();
        
         UserTransaction tx = null;
         try
         {
            tx = Repository.getUserTransaction(context, true);
            tx.begin();
           
            // get the current in progress tasks for the current user
            List<WorkflowTask> tasks = this.workflowService.getAssignedTasks(
                  userName, WorkflowTaskState.IN_PROGRESS);
           
            // create a list of transient nodes to represent
            this.tasks = new ArrayList<Node>(tasks.size());
            for (WorkflowTask task : tasks)
            {
               Node node = createTask(task);
               this.tasks.add(node);
              
               if (logger.isDebugEnabled())
                  logger.debug("Added to do task: " + node);
            }
           
            // commit the changes
            tx.commit();
         }
         catch (Throwable e)
         {
            // rollback the transaction
            try { if (tx != null) {tx.rollback();} } catch (Exception ex) {}
            Utils.addErrorMessage("Failed to get to do tasks: " + e.toString(), e);
         }
      }
     
      return this.tasks;
   }
   /**
    * Returns a list of nodes representing the completed tasks the
    * current user has.
    *
    * @return List of completed tasks
    */
   public List<Node> getTasksCompleted()
   {
      if (this.completedTasks == null)
      {
         // get the current username
         FacesContext context = FacesContext.getCurrentInstance();
         User user = Application.getCurrentUser(context);
         String userName = user.getUserName();
        
         UserTransaction tx = null;
         try
         {
            tx = Repository.getUserTransaction(context, true);
            tx.begin();
           
            // get the current in progress tasks for the current user
            List<WorkflowTask> tasks = this.workflowService.getAssignedTasks(
                  userName, WorkflowTaskState.COMPLETED);
           
            // create a list of transient nodes to represent
            this.completedTasks = new ArrayList<Node>(tasks.size());
            for (WorkflowTask task : tasks)
            {
               Node node = createTask(task);
               this.completedTasks.add(node);
              
               if (logger.isDebugEnabled())
                  logger.debug("Added completed task: " + node);
            }
           
            // commit the changes
            tx.commit();
         }
         catch (Throwable e)
         {
            // rollback the transaction
            try { if (tx != null) {tx.rollback();} } catch (Exception ex) {}
            Utils.addErrorMessage("Failed to get completed tasks: " + e.toString(), e);
         } 
      }
     
      return this.completedTasks;
   }
   /**
    * Sets the navigation bean to use
    *
    * @param navigationBean The NavigationBean to set.
    */
   public void setNavigationBean(NavigationBean navigationBean)
   {
      this.navigationBean = navigationBean;
   }
  
   /**
    * Sets the workflow service to use
    *
    * @param workflowService WorkflowService instance
    */
   public void setWorkflowService(WorkflowService workflowService)
   {
      this.workflowService = workflowService;
   }
  
   /**
    * Sets the node service to use
    *
    * @param nodeService NodeService instance
    */
   public void setNodeService(NodeService nodeService)
   {
      this.nodeService = nodeService;
   }
  
  
   // ——————————————————————————
   // Navigation handlers
  
   public void setupTaskDialog(ActionEvent event)
   {
      UIActionLink link = (UIActionLink)event.getComponent();
      Map<String, String> params = link.getParameterMap();
      String id = params.get("id");
      String type = params.get("type");
     
      // setup the dispatch context with the task we're opening a dialog for
      TransientNode node = new TransientNode(QName.createQName(type), id, null);
      this.navigationBean.setupDispatchContext(node);
     
      // pass on parameters for the dialog
      Application.getDialogManager().setupParameters(event);
   }
  
   public void setupTaskDialog(String id, String type)
   {
      ParameterCheck.mandatoryString("Task ID", id);
      ParameterCheck.mandatoryString("Task Type", type);
     
      // setup the dispatch context with the task we're opening a dialog for
      TransientNode node = new TransientNode(QName.createQName(type), id, null);
      this.navigationBean.setupDispatchContext(node);
     
      // pass on parameters for the dialog
      Map<String, String> params = new HashMap<String, String>(2, 1.0f);
      params.put("id", id);
      params.put("type", type);
      Application.getDialogManager().setupParameters(params);
   }
  
  
   // ——————————————————————————
   // Helper methods
  
   /**
    * Creates and populates a TransientNode to represent the given
    * workflow task from the repository workflow engine
    *
    * @param task The task to create a representation of
    */
   protected TransientMapNode createTask(WorkflowTask task)
   {
      // get the type of the task
      WorkflowTaskDefinition taskDef = task.definition;
     
      // create the basic transient node
      TransientMapNode node = new TransientMapNode(taskDef.metadata.getName(),
            task.title, task.properties);
     
      // add properties for the other useful metadata
      node.getProperties().put(ContentModel.PROP_NAME.toString(), task.title);
      node.getProperties().put("type", node.getType().toString());
      node.getProperties().put("id", task.id);
     
      // add extra properties for completed tasks
      if (task.state.equals(WorkflowTaskState.COMPLETED))
      {
         // add the outcome label for any completed task
         String outcome = null;
         String transition = (String)task.properties.get(WorkflowModel.PROP_OUTCOME);
         if (transition != null)
         {
            WorkflowTransition[] transitions = task.definition.node.transitions;
            for (WorkflowTransition trans : transitions)
            {
               if (trans.id.equals(transition))
               {
                  outcome = trans.title;
                  break;
               }
            }
           
            if (outcome != null)
            {
               node.getProperties().put("outcome", outcome);
            }
         }
        
         // add the workflow instance id and name this taks belongs to
         node.getProperties().put("workflowInstanceId", task.path.instance.id);
        
         // add the task itself as a property
         node.getProperties().put("workflowTask", task);
      }
     
      return node;
   }
}


Je change ensuite le fichiers de configuration faces-config-beans.

<managed-bean>
      <managed-bean-name>CustomWorkflowBean</managed-bean-name>
      <managed-bean-class>lpr.alfresco.web.bean.workflow.CustomWorkflowBean</managed-bean-class>
      <managed-bean-scope>request</managed-bean-scope>
      <managed-property>
         <property-name>navigationBean</property-name>
         <value>#{NavigationBean}</value>
      </managed-property>
      <managed-property>
         <property-name>nodeService</property-name>
         <value>#{NodeService}</value>
      </managed-property>
      <managed-property>
         <property-name>workflowService</property-name>
         <value>#{WorkflowService}</value>
      </managed-property>
   </managed-bean>

Pour compiler ma classe, je l'export dans un jarfile et je met ce jar dans les lib d'Alfresco.

Le résultat donne que dans le dashboard je vois bien apparaitre mes taches actives (donc la création de la dashlet se passe bien). Mais je ne vois pas l'ensemble des taches actives qui devrait apparaitre.

Alors que dans la dashlet toutes les taches actives il y en a 7 qui apparaissent.

Je pense donc qu'alfresco ne fait pas la liaison entre la dashlet et le CustomWorkflowBean. Et je pense donc à un problème de configuration mais je ne trouve pas l'erreur.

Donc si quelqu'un peut m'aider à trouver mon erreur, je lui serait reconnaissant.

Ptitmat
21 REPLIES 21

jayjayecl
Confirmed Champ
Confirmed Champ
c'est tout-à-fait possible.
Il faudrait pour cela résoudre en premier lieu votre problème de non-fonctionnement de votre classe personnalisée CustomworkflowBean.
Mais j'avoue sécher à la lumière des informations fournies.

Vous devriez sans doute mettre votre tomcat en mode debugger (ajouter "-Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n" dans les "JAVA_OPTS" de alfresco.bat), puis configurer le mode debug d'Eclipse et mettre des points d'arret pour vérifier si le passage dans votre classe CustomWorkflowBean est réalisé.

Par ailleurs, par simple conseil, je vous conseille fortement de placer votre config faces-config dans faces-config-custom.xml, et non pas directement dans faces-config-beans.xml

ptitmat
Champ in-the-making
Champ in-the-making
Le problème a été résolu.

En reprenant, j'ai recopié à l'identique mes fichiers que je devais modifier. et maintenant j'ai bien mes deux dashlets (toutes les taches actives et mes taches actives) et toutes les deux affichent l'ensemble des taches actives. Il reste donc à modifier le code source de la classe WorkflowBean pour pouvoir modifier la fonction getAllActiveTasks().

Actuellement je suis entrain de regarder  les différentes API pour pouvoir récupéré le nom de l'acteur d'une tache dont celles ci
Je suis aussi entrain de regarder pour afficher de nouveau log avec ce tuto http://wiki.alfresco.com/wiki/Developing_an_Alfresco_Module#log4j.properties mais mes fichiers de conf diffère du tuto.
Ou bien passé eclipse en mode Debug.

Pour info, j'ai recopié la classe WorkflowBean dans un nouveau package et j'ai remplacé le package org.alfresco.web.bean.Workflow dans l'emplacement  face-config-bean

Et cela fonctionne, je n'ai pas eu besoin de toucher ainsi à mes JSP.

Je ne sais pas si c'est propre mais ca fonctionne.

Ptitmat

jayjayecl
Confirmed Champ
Confirmed Champ
pour récupérer le responsable de la tâche, il s'agit de la propriété "cmSmiley Surprisedwner" de l'objet Node correspondant à la tâche.

ptitmat
Champ in-the-making
Champ in-the-making
SI je te suis bien, pour récupérer la propriété owner de mon nœud qui correspond à la tâche.

J'ai trouvé ceci pour owner
 Ce Service vital fournit une API permettant de manager la notion de propriété sur des entités.

Quelques exemples:


// Get the username of the owner of the given object.
String userName = ownableService.getOwner(NodeRef nodeRef); (1)
// Set the owner of the object.
ownableService.setOwner(NodeRef nodeRef, String userName); (2)


L'instruction (1) permet d'obtenir le Username du propriétaire du contenu de noeud référencé par le NodeRef NodeRef.
L'instruction (2) permet de donner la propriété du contenu de noeud référencé par le NodeRef nodeRef à l'utilisateur de Username userName.

Ce code se trouve à l'adresse suivante http://koossery-tech.developpez.com/tutoriels/java/ecm/alfresco-jbpm/bible-developpeur-alfresco-jbpm...


Donc puisque dans ma classe j'ai ce code

        this.activeTasks = new ArrayList<Node>(tasks.size());
   for (WorkflowTask task : tasks)
   {
           Node node = createTask(task);
          …
        }

A ce moment j'ai bien le nœud correspondant à ma tâche. Donc si j'ajoute cette ligne
String userNameTasks = this.ownableService.getOwner(node.getNodeRef());

J'obtiens logiquement le possesseur du nœud de la tache.

J'ai un petit souci avec mes log4j et avec eclipse en mode debug.

Donc je vais faire un test sans message pour voir déjà si ça ne plante pas et je vous tiens au courant.

Ptitmat

jayjayecl
Confirmed Champ
Confirmed Champ
Je n'utilise jamais le ownableService.

Voici comment je procède : disons plutot que dans le dashlet, tu as accès au WorkflowBean, qui a tendance à faire apparaitre des objets WorkflowTask.
Alors,

task.properties.get(ContentModel.PROP_OWNER) envoie un String qui est le userName du propriétaire

ou plus simplement avec le nodeService.getProperty(NodeRef nodeRef, QName propertyName)

ptitmat
Champ in-the-making
Champ in-the-making
J'aimerais bien testé ta solution Rodel, mais j'ai l'impression qu'alfresco ne charge pas mon nouveau jar. et je ne sais pas pourquoi.

En effet il me met toujours la même erreur. Pourtant, il y a bien eu des changement puisque j'ai un message qui apparait dans la console.

Lorsque je modifie ma classe, je l'exporte dans un jar file que je place dan le dossier WEB_INF/../lib. Je stoppe alfesco, je vide le work, je vide les cookies, le cache …. (tout).
Dans ma config, j'ai mon projet qui est possède un package lpr….WorkflowBean zt un package avec un main qui me lance le constructeur de WorkflowBean.

Je relance Alfresco, mais il ne prend pas en compte les changements.

jayjayecl
Confirmed Champ
Confirmed Champ
houla je déconseille le "main()" pour construire.

Normalement ton CustomWorkflowBean doit être désigné dans le faces-config-custom.xml, dans WEB-INF

note ici tout ce que tu as pour nous aider a debugguer (stack trace, logs, config complete etc …)

ptitmat
Champ in-the-making
Champ in-the-making
J'ai résolu le problème, il y avait une copie du jar qui c'était incorporé dans le dossier. En suppriment cette copie, je vois l'évolution et ta technique pour récupéré le nom de l'utilisateur focntionne.
Je suis entrain de faire des tests pour regarder s'il prends en compte le booléen isAdmin().

Je pense que si j'arrive à faire fonctionner cette tache je referais mon premier post de ce sujet.

Je te teins au courant

ptitmat

ptitmat
Champ in-the-making
Champ in-the-making
Bonjour,

Je remercie Rodel pour son aide. Le développement de ma dashlet "mes tâches actives" fonctionne très bien.
Je peux maintenant en tant qu'admin voir l'ensemble des taches actives de tous les utilisateurs.
Et en fonction de l'user, je ne vois que les tâches actives lancés par l'utilisateur connecté.

J'essaierais de modifier dans la semaine le premier post pour expliquer le fonctionnement  et la réalisation de la dashlet.

Merci pour l'aide.

Ptitmat

jayjayecl
Confirmed Champ
Confirmed Champ
no problem