Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Este tutorial forma parte de la serie Creando su Startup con PHP en Envato Tuts +. En esta serie, te estoy guiando a través del lanzamiento de un inicio de concepto a realidad utilizando mi aplicación Meeting Planner como un ejemplo de la vida real. Cada paso a lo largo del camino, voy a lanzar el código de Meeting Planner como ejemplos de código abierto que puede aprender. También me ocuparé de asuntos relacionados con el inicio de negocios a medida que surjan.
Introducción a las reuniones de grupo
La programación de reuniones con múltiples participantes siempre formaba parte de mi plan—pero no forma parte del primer producto mínimo viable (MVP, por sus siglas en inglés). La versión alfa de Meeting Planner se lanzó con sólo 1a1 de programación. El objetivo de apoyar la programación de grupo se sentó en la lista de tareas como el Monte Everest a un escalador con el objetivo de las siete cumbres (y ni siquiera soy un escalador al aire libre).
Las reuniones de múltiples participantes son las más difíciles de programar y por lo tanto valiosas para ofrecer para el producto Meeting Planner. Estaba emocionado cuando la lista de tareas beta llegó al punto de que podría empezar a trabajar en esto.
He estado planeando, arquitectando y codificando con reuniones de grupo en mente desde casi el comienzo. Esperaba que la actualización del sitio para esta función no requeriría cambios UX significativos o actualizaciones de codificación. Resultó que se requiere un camino medio, 7-10 días de trabajo muy centrado y las pruebas, pero no re-diseño importante.
De hecho, la prueba demostró ser el aspecto más difícil de construir esta característica. También ayudó a revelar las deficiencias en el código anterior. Es sólo que no es fácil ... enviar a varias direcciones de correo electrónico, comprobando que cada una de ellas recibe todas las notificaciones adecuadas, pero no las notificaciones incorrectas, y ve todas las opciones de menú correctas en todo el sitio.
En el tutorial de hoy, voy a cubrir la habilitación de múltiples participantes, la actualización de la UX para los grupos, el nombramiento de los organizadores, la eliminación de los participantes, y la clasificación de la fecha, hora y opciones de lugar por su popularidad con los participantes.
En el próximo tutorial, describiré el resto del trabajo: revisar todas las áreas del sitio afectadas por múltiples reuniones de participantes, manejar y mostrar inteligentemente las listas de destinatarios de varios estados, administrar correctamente las notificaciones y el filtrado de notificaciones para los grupos y finalmente actualizar la recientemente lanzada capacidad de cambios de reunión
Trate de programar una reunión de grupo
Por favor, programe una reunión de grupo hoy! Comparta sus pensamientos y comentarios en los comentarios a continuación.
Yo participo en las discusiones, pero también puedes contactarme @reifman en Twitter. Siempre estoy abierto a nuevas ideas de funciones para Meeting Planner, así como sugerencias para futuros episodios de la serie.
Como recordatorio, todo el código para Meeting Planner se proporciona de código abierto y escrito en el Framework Yii2 para PHP. Si desea obtener más información acerca de Yii2, consulte mi serie paralela Programación con Yii2. He oído grandes cosas sobre Laravel, pero Yii2 siempre satisface mis necesidades de forma rápida y sencilla.
Mirando hacia atrás
Cuando diseñé por primera vez la interfaz de programación Meeting Planner, mostró la disponibilidad actual del otro participante en su propia columna. Y fue un poco confuso, ya que había controles deshabilitados.

En ese momento, me preocupaba cómo podría hacer espacio para mostrar la disponibilidad de grupos.
Afortunadamente, cuando reconstruí el UX para una mejor experiencia de adaptabilidad, sustituí la columna de disponibilidad del participante por un pequeño resumen de texto:

El resumen del texto de la disponibilidad coincidentemente funcionaría bien para las reuniones de grupo.
Al rediseñar para móviles primero, resolví la barrera UX más significativa para las reuniones de múltiples participantes!
Codificación para reuniones de grupo
Vamos a empezar a ir a través de todo el código y pruebas que las reuniones de múltiples participantes necesarios.
Habilitación de múltiples participantes

El aspecto más gracioso de las reuniones de grupo es que activarlas era sencillo. Solo necesitaba apagar la inhabilitación del botón de icono más en el panel Who para reuniones en la etapa de planificación:
<div class="col-lg-2 col-md-2 col-xs-2"> <div style="float:right;"> <?= Html::a(Yii::t('frontend', ''), ['/participant/create', 'meeting_id' => $model->id], ['class' => 'btn btn-primary '.($model->status>=$model::STATUS_CONFIRMED?'disabled':'').' glyphicon glyphicon-plus']) ?> </div> </div>
Entonces, comencé creando un MEETING_LIMIT
en el modelo Participant:
class Participant extends \yii\db\ActiveRecord { ... const MEETING_LIMIT = 15;
Se utiliza en ParticipantController::actionCreate()
en enviar:
public function actionCreate($meeting_id) { if (!Participant::withinLimit($meeting_id)) { Yii::$app->getSession()->setFlash('error', Yii::t('frontend','Sorry, you have reached the maximum number of participants per meeting. Contact support if you need additional help or want to offer feedback.')); return $this->redirect(['/meeting/view', 'id' => $meeting_id]); }
Avanzando el UX y las características relacionadas
Durante mucho tiempo, he querido permitir que los organizadores de reuniones eliminen los participantes, los lugares y los tiempos de la fecha sin que se abarquen la interfaz de usuario. Del mismo modo, me di cuenta de que podría haber varios comandos para realizar en los participantes.
Después de encontrar tanta utilidad en el botón desplegable Bootstrap compacto en el tutorial de comandos avanzados, decidí usarlo para mostrar a los asistentes a la reunión:

Los organizadores se indican con una estrella. Los asistentes que han rechazado la reunión se muestran en naranja. Los asistentes eliminados por los organizadores se muestran en rojo.
Aquí está el código en mi nuevo parcial /frontend/views/participant/_buttons.php:
<div class="btn-group btn-participant"> <button type="button" class="btn btn-default btn-sm dropdown-toggle " data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span class="glyphicon glyphicon-star red-star aria-hidden="true"></span> <?= MiscHelpers::getDisplayName($model->owner_id) ?> <span class="caret"></span> </button> <ul class="dropdown-menu"> <li><?= Html::a(Yii::t('frontend','Send a message'),Url::to('mailto:'.$model->owner->email))?></li> </ul> </div>
Cualquier persona puede ahora enviar un mensaje a cualquier participante (las características de las notas de la reunión se distribuyen actualmente a todos los participantes de la reunión).
Los organizadores ven un menú desplegable más profundo que les permite ungir organizadores adicionales, es decir, hacer organizador. Esto ahora es una característica muy fresca. Los organizadores recibirán notificaciones más completas y tendrán más poder durante las fases de planificación. También pueden eliminar participantes.
Construyendo características de AJAX en los botones del participante
Decidí por un capricho para AJAXify todas estas opciones de menú. Eso resultó requerir varias horas complejas de codificación.
Aquí está el código que define el menú del botón inicial y prepara el código JavaScript:
<?php if (count($model->participants)>0) { foreach ($model->participants as $p) { if ($p->participant->id==Yii::$app->user->getId()) { continue; } $btn_color = 'btn-default'; if ($p->status == Participant::STATUS_DECLINED) { $btn_color = 'btn-warning'; } else if ($p->status == Participant::STATUS_REMOVED || $p->status == Participant::STATUS_DECLINED_REMOVED) { $btn_color = 'btn-danger'; } ?> <div class="btn-group btn-participant"> <button id="btn_<?= $p->id ?>" type="button" class="btn <?= $btn_color ?> btn-sm dropdown-toggle " data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <span id="star_<?= $p->id ?>" class="glyphicon glyphicon-star red-star <?= (!$p->isOrganizer())?'hidden':''?>" aria-hidden="true"></span> <?= MiscHelpers::getDisplayName($p->participant->id) ?> <span class="caret"></span> </button> <ul class="dropdown-menu"> <li><?= Html::a(Yii::t('frontend','Send a message'),Url::to('mailto:'.$p->participant->email))?></li> <?php if ($model->isOrganizer()) { ?> <li role="separator" class="divider"></li> <li id="mo_<?= $p->id ?>" class="<?= ($p->isOrganizer())?'hidden':''?>"><?= Html::a(Yii::t('frontend','Make organizer'),'javascript:void(0);',['onclick' => "toggleOrganizer($p->id,true);return false;"]); ?></li> <li id="ro_<?= $p->id ?>" class="<?= (!$p->isOrganizer())?'hidden':''?>"><?= Html::a(Yii::t('frontend','Revoke organizer role'),'javascript:void(0);',['onclick' => "toggleOrganizer($p->id,false);return false;"]); ?></li> <li id="rp_<?= $p->id ?>" class="<?= ($p->status == Participant::STATUS_REMOVED || $p->status == Participant::STATUS_DECLINED_REMOVED)?'hidden':''?>"><?= Html::a(Yii::t('frontend','Remove participant'),'javascript:void(0);',['onclick' => "toggleParticipant($p->id,false,$p->status);return false;"]); ?></li> <li id="rstp_<?= $p->id ?>" class="<?= ($p->status != Participant::STATUS_REMOVED && $p->status != Participant::STATUS_DECLINED_REMOVED)?'hidden':''?>"><?= Html::a(Yii::t('frontend','Restore participant'),'javascript:void(0);',['onclick' => "toggleParticipant($p->id,true,$p->status);return false;"]); ?></li> <?php } ?> </ul> </div>
Hay
tantos estados de los botones, colores y estrellas para actualizarse a
medida que los cambios se hacen interactivamente en una página que el
código se vuelve bastante complicado. He añadido
funciones al archivo de JavaScript de la reunión.js para toggleOrganizer()
, es decir, hacer/deshacer organizador y toggleParticipant()
, es decir,
eliminar / restaurar el participante como asistente.
function toggleOrganizer(id, val) { if (val === true) { arg2 = 1; } else { arg2 =0; } $.ajax({ url: $('#url_prefix').val()+'/participant/toggleorganizer', data: {id: id, val: arg2}, success: function(data) { if (data) { if (val===false) { $('#star_'+id).addClass("hidden"); $('#ro_'+id).addClass("hidden"); $('#mo_'+id).removeClass("hidden"); } else { $('#star_'+id).removeClass("hidden"); $('#ro_'+id).removeClass("hidden"); $('#mo_'+id).addClass("hidden"); } } return true; } }); } function toggleParticipant(id, val, original_status) { if (val === true) { arg2 = 1; } else { arg2 =0; } $.ajax({ url: $('#url_prefix').val()+'/participant/toggleparticipant', data: {id: id, val: arg2, original_status: original_status}, success: function(data) { if (data) { if (val===false) { $('#rp_'+id).addClass("hidden"); $('#rstp_'+id).removeClass("hidden"); $('#btn_'+id).addClass("btn-danger"); $('#btn_'+id).removeClass("btn-default"); } else { $('#rp_'+id).removeClass("hidden"); $('#rstp_'+id).addClass("hidden"); if (original_status==100) { $('#btn_'+id).addClass("btn-warning"); $('#btn_'+id).removeClass("btn-danger"); } else { $('#btn_'+id).addClass("btn-default"); $('#btn_'+id).removeClass("btn-danger"); } } } return true; } }); }
Estos requerían los métodos de controlador JSON que lo acompañaban en ParticipantController.php para procesar las solicitudes de cambio y actualizar las bases de datos:
public function actionToggleorganizer($id,$val) { Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; // change setting $p=Participant::findOne($id); if ($p->meeting->isOrganizer()) { $p->email = $p->participant->email; if ($val==1) { $p->participant_type=Participant::TYPE_ORGANIZER; } else { $p->participant_type=Participant::TYPE_DEFAULT; } $p->update(); return true; } else { return false; } } public function actionToggleparticipant($id,$val) { Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; // change setting $p=Participant::findOne($id); if ($p->meeting->isOrganizer()) { $p->email = $p->participant->email; if ($val==0) { if ($p->status == Participant::STATUS_DECLINED) { $p->status=Participant::STATUS_DECLINED_REMOVED; } else { $p->status=Participant::STATUS_REMOVED; } } else { if ($p->status == Participant::STATUS_DECLINED_REMOVED) { $p->status=Participant::STATUS_DECLINED; } else { $p->status=Participant::STATUS_DEFAULT; } } $p->update(); return true; } else { return false; } }
Activación de la función de acordeón en los paneles

En este momento, también me di cuenta de que a medida que los planes de reunión aumentaran en complejidad con más destinatarios y opciones, habría más desplazamiento. Decidí implementar la característica de acordeón Bootstrap para todos los paneles en nuestra vista de reunión.
En otras palabras, ahora puede hacer clic en un encabezado para colapsar o abrir cada uno y / o todos los paneles.
Estos son los cambios en los parciales para el lugar de reunión _panel.php:
<div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading" role="tab" id="headingWhere"> <div class="row"> <div class="col-lg-10 col-md-10 col-xs-10" ><h4 class="meeting-place"> <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseWhere" aria-expanded="true" aria-controls="collapseWhere"><?= Yii::t('frontend','Where') ?></a> </h4><p> <div class="hint-text heading-pad"> <?php if ($placeProvider->count<=1) { ?> <?= Yii::t('frontend','add places for participants or switch to \'virtual\'') ?> <?php } elseif ($placeProvider->count>1) { ?> <?= Yii::t('frontend','are listed places okay? ') ?> <?php } ?> ... <div id="collapseWhere" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingWhere"> <div class="panel-body"> <?php $style = ($model->switchVirtual==$model::SWITCH_VIRTUAL?'none':'block'); ?> <div id ="meeting-place-list" style="display:<?php echo $style; ?>"> <?php if ($placeProvider->count>0): ?> <table class="table"> <?= ListView::widget([ 'dataProvider' => $placeProvider, 'itemOptions' => ['class' => 'item'], 'layout' => '{items}', 'itemView' => '_list', 'viewParams' => ['placeCount'=>$placeProvider->count,'isOwner'=>$isOwner,'participant_choose_place'=>$model->meetingSettings['participant_choose_place'],'whereStatus'=>$whereStatus], ]) ?> </table>
Observe los ajustes anteriores en el panel-heading
y luego la div circundante para el cuerpo del panel-body
. Estos controlan la apertura y el colapso de cada panel.
Esto condujo a algunos pequeños problemas cosméticos tales como acolchado indeseado alrededor de la lista de artículos, que necesitaré limpiar en el futuro.
Infraestructura modelo para reuniones de grupo
Aunque había estado planeando para varios participantes desde casi el comienzo, hubo algunas mejoras de infraestructura menores a modestas para apoyarlas.
Mientras
los modelos MeetingTimeChoice
y MeetingPlaceChoice
mantienen un
seguimiento de si los participantes prefieren fechas y lugares
específicos, quería realizar un seguimiento de la disponibilidad general
de todos los participantes en cada fecha, hora y lugar. Esto
me permitiría clasificar lugares y tiempos por lo popular que son y
mostrar los ajustes más populares en la parte superior de los paneles.
En primer lugar, creé una migración para agregar esto a ambos modelos. Es poco frecuente que una migración de la mina afecta a múltiples modelos, lo que hace que este tipo de especial:
<?php use yii\db\Schema; use yii\db\Migration; class m160824_235517_extend_meeting_place_and_time extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->addColumn('{{%meeting_time}}','availability',Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0'); $this->addColumn('{{%meeting_place}}','availability',Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0'); } public function down() { $this->dropColumn('{{%meeting_time}}','availability'); $this->dropColumn('{{%meeting_place}}','availability'); } }
Con
esta capacidad, pude comenzar a mostrar posibles fechas de reunión y
lugares clasificados por su popularidad con los participantes, desde MeetingController::actionView()
:
$timeProvider = new ActiveDataProvider([ 'query' => MeetingTime::find()->where(['meeting_id'=>$id]), 'sort' => [ 'defaultOrder' => [ 'availability'=>SORT_DESC ] ], ]); $placeProvider = new ActiveDataProvider([ 'query' => MeetingPlace::find()->where(['meeting_id'=>$id]), 'sort' => [ 'defaultOrder' => [ 'availability'=>SORT_DESC ] ], ]);
Puede ver esto en acción en la siguiente captura de pantalla de planificación:

Para supervisar si los participantes son organizadores y para permitir futuras exclusiones de las notificaciones de una reunión específica, agregué esta migración a la tabla Participant :
<?php use yii\db\Schema; use yii\db\Migration; class m160825_074740_extend_participant_add_type extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->addColumn('{{%participant}}','participant_type',Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0'); $this->addColumn('{{%participant}}','notify',Schema::TYPE_SMALLINT.' NOT NULL DEFAULT 0'); } public function down() { $this->dropColumn('{{%participant}}','participant_type'); $this->dropColumn('{{%participant}}','notify'); } }
También agregué una serie de constantes en Participant.php para trabajar con estas propiedades:
class Participant extends \yii\db\ActiveRecord { const TYPE_DEFAULT = 0; const TYPE_ORGANIZER = 10; const NOTIFY_ON = 0; const NOTIFY_OFF = 1; const STATUS_DEFAULT = 0; const STATUS_REMOVED = 90; const STATUS_DECLINED = 100; const STATUS_DECLINED_REMOVED = 110;
Y yo sabía que sería útil tener algunas funciones auxiliares dentro del masivo modelo de Meeting. Por ejemplo, IsOrganizer()
me dice si el visor actual es un organizador de reuniones:
public function isOrganizer() { $user_id = Yii::$app->user->getId(); if ($user_id == $this->owner_id) { return true; } else { foreach ($this->participants as $p) { if ($user_id == $p->participant_id) { if ($p->participant_type == Participant::TYPE_ORGANIZER) { return true; } else { return false; } } } } return false; }
Espera, ¿hay más?
Como puede ver, hay mucho terreno para cubrir para construir esta característica. En el próximo episodio, cubriré la segunda mitad del desarrollo y las pruebas necesarias para iniciar varias reuniones de participantes: cadenas de destinatarios, notificaciones, solicitudes y respuestas a las solicitudes.
Si todavía no lo ha hecho, vaya a programar su primera reunión con Meeting Planner y pruebe todo esto. Por favor, comparta sus comentarios en los comentarios a continuación.
Un tutorial sobre crowdfunding también está en los trabajos, así que por favor siga nuestra página WeFunder Meeting Planner.
También puede contactar conmigo @reifman. Siempre estoy abierto a nuevas ideas de características y sugerencias de temas para futuros tutoriales.
Manténgase en sintonía para todo esto y más tutoriales próximos, echa un vistazo a la serie creando su startup con PHP.
Enlaces relacionados
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post