Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

Este tutorial es parte de la serie Construyendo Tu Startup Con PHP en Envato Tuts+. En esta serie, te estoy guiando a través de lanzar una startup desde el concepto a la realidad usando mi aplicación Planificador de Reuniones como un ejemplo de la vida real. Cada paso a lo largo del camino, liberará el código de Planificador de Reuniones como ejemplos de código abierto que de los cuales puedes aprender. También abordaré asuntos de negocios relacionados con startup según vayan surgiendo.
Desarrollo En Marcha de Reuniones Grupales
¡Bienvenido! Este es el episodio de continuación para Construyendo Tu Startup: Reuniones con Múltiples Participantes. Hoy, estaré completando el trabajo que comenzamos en ese episodio: programar reuniones de múltiples participantes.
Un Breve Recordatorio
Programar reuniones con múltiples participantes siempre fue una meta mayor para Planificador de Reuniones, que se lanzó solo con programación 1:1. Las reuniones de múltiples participantes son las más retadoras para gente para intentar programar con cada uno y de ahí una de las características más valiosas para servicio que proporcionar de Planificador de Reuniones.
En el tutorial de hoy, voy a cubrir revisando todas las áreas del sitio afectado por reuniones de múltiples participantes, manejando y mostrando inteligentemente listas de recipientes de varios estados, administrar notificaciones apropiadamente y filtrando para grupos y finalmente mejorando la recientemente lanzada característica requerir cambios de reunión.
Programa Tu Primera Reunión de Grupo
¡Por favor programa tu propia reunión de grupo hoy! Invita a unos cuantos amigos para encontrar para tu kombucha o kava. Comparte tus ideas y retroalimentación de la experiencia de todos en los comentarios de abajo. Participo en las discusiones, pero también puedes contactarme en Twitter @reifman. Siempre estoy abierto a nuevas ideas de características para Planificador de Reuniones así como a sugerencias para futuros episodios en la serie.
Como recordatorio, todo el código para Planificador de Reuniones es proporcionado de código abierto y escrito en el Framework Yii2 para PHP. Si quisieras aprender más sobre Yii2, revisa mi serie paralela Programando Con Yii2.
Revisando el Código
Como puedes imaginar, transformar Planificador de Reuniones de reuniones 1:1 a reuniones grupales tocó casi todo el código. Tuve que pensar a través de todas las áreas, revisar el código, y hacer cambios de pequeños a moderados en muchos lugares. En otras áreas, hubiera diseñado para múltiples participantes, y los cambios no fueron necesarios o fueron menores.
Por ejemplo, programación grupal tocada:
- Enviando invitaciones y Finalizando (confirmando) horarios de reunión
- Haciendo Cambios, Reenviando, Repitiendo, Reprogramando una reunión
- Enviando una alerta de retraso
- Recordatorios de Reuniones
- Pidiendo cambios de reunión
- Notificaciones
En su mayoría, en donde previamente solo enviaríamos a participante[0], el primer y único participante, ahora necesitaba procesar el arreglo de participantes. Y, haciéndolo, necesitaba revisar:
- ¿Este participante es un organizador?
- ¿Esta persona ha sido removida o ha declinado por su cuenta?
- En el futuro, ¿esta persona ha optado por no recibir notificaciones para esta reunión?
Los Retos de Probar
Con más recursos, podría haber administrado esto más exhaustivamente con pruebas automáticas. Sin embargo, trabajando en solitario con la entrega como meta, probé manualmente todo de manera exhaustiva.
Usé un dominio de correo catchall para que pudiera invitar n1, n2, n3, n4 y n5 @midominiodeprueba.com a mi muestra de reuniones grupales. Afortunadamente, las invitaciones de Planificador de Reuniones hacen fácil iniciar sesión rápidamente con cualquier cuenta dando clic dentro de cada invitación de reunión; esto ayudó a mis pruebas.
Era importante revisar casi todo el código de planificación de reunión.
Pero regresemos a los retos de código más específicos de la segunda mitad de la característica de programación grupal.
Mostrando Inteligentemente Listas de Participantes
Hace un tiempo, construí el método MiscHelpers
para mostrar listas gramaticalmente correctas en Inglés con "and" antes del último nombre, como se muestra en el índice de reunión abajo:

Sin embargo, quise simplificar la visibilidad de la disponibilidad de fecha y lugar. Por ejemplo, en vez de listar cinco nombres de gente que aceptó reunirse en Herkimer Coffee, actualicé MiscHelpers::listNames
para decir, "todos los demás".
public static function listNames($items,$everyoneElse=false,$total_count=0,$anyoneElse=false) { $temp =''; $x=1; $cnt = count($items); if ($everyoneElse && $cnt >= ($total_count-1)) { if (!$anyoneElse) { $temp = Yii::t('frontend','everyone else'); } else { $temp = Yii::t('frontend','anyone else'); } } else { foreach ($items as $i) { $temp.= MiscHelpers::getDisplayName($i); if ($x == ($cnt-1)) { $temp.=' and '; } else if ($x < ($cnt-1)) { $temp.=', '; } $x+=1; } } return $temp; }
Puedes ver esto en acción abajo:

Pero en vez de decir "No hay respuesta todos los demás," es más propio decir "No hay respuesta de nadie más.", lo que hace el código.
Abajo, puedes ver MeetingPlace::getWhereStatus()
preparando estas cadenas para cada lugar en el panel de lugares de reunión:
public static function getWhereStatus($meeting,$viewer_id) { // get an array of textual status of meeting places for $viewer_id // Acceptable / Rejected / No response: $whereStatus['style'] = []; $whereStatus['text'] = []; foreach ($meeting->meetingPlaces as $mp) { // build status for each place $acceptableChoice=[]; $rejectedChoice=[]; $unknownChoice=[]; // to do - add meeting_id to MeetingPlaceChoice for sortable queries foreach ($mp->meetingPlaceChoices as $mpc) { if ($mpc->user_id == $viewer_id) continue; switch ($mpc->status) { case MeetingPlaceChoice::STATUS_UNKNOWN: $unknownChoice[]=$mpc->user_id; break; case MeetingPlaceChoice::STATUS_YES: $acceptableChoice[]=$mpc->user_id; break; case MeetingPlaceChoice::STATUS_NO: $rejectedChoice[]=$mpc->user_id; break; } } // to do - integrate current setting for this user in style setting $temp =''; // count those still in attendance $cntP = Participant::find() ->where(['meeting_id'=>$meeting->id]) ->andWhere(['status'=>Participant::STATUS_DEFAULT]) ->count()+1; if (count($acceptableChoice)>0) { $temp.='Acceptable to '.MiscHelpers::listNames($acceptableChoice,true,$cntP).'. '; $whereStatus['style'][$mp->place_id]='success'; } if (count($rejectedChoice)>0) { $temp.='Rejected by '.MiscHelpers::listNames($rejectedChoice,true,$cntP).'. '; $whereStatus['style'][$mp->place_id]='danger'; } if (count($unknownChoice)>0) { $temp.='No response from '.MiscHelpers::listNames($unknownChoice,true,$cntP,true).'.'; $whereStatus['style'][$mp->place_id]='warning'; } $whereStatus['text'][$mp->place_id]=$temp; } return $whereStatus; }
Cada usuario tiene una fila MeetingPlaceChoice
relacionada a MeetingPlace
que graba si un lugar es aceptable, no aceptable o no respondido aún. MeetingTimeChoice
también existe de manera similar. Esta información es pasada a listNames()
.
Declinando y Retirándose de una Reunión
Los grupos también requerían más intrincación cuando de rechazaba una invitación de reunión. Anteriormente, si un participante declinaba, la reunión estaba efectivamente cancelada. Ahora, uno podría declinar, dejando a los otros tres continuar.
Así que si un participante recibe una invitación a una reunión, ellos pueden Declinar. Pero si la reunión ya ha sido confirmada y finalizada, entonces ellos están esencialmente Retirándose como puedes ver abajo:

Nota: En la imagen de abajo, es Sarah Smithers viendo el botón Retirarse; Robert McSmith ha sido removido por otro organizador, sea Jeff o Alex.
Sin embargo, si es un organizador (propietario de reunión o participante organizador agregado), ellos pueden solo Cancelar la reunión. Abajo está de _command_bar_confirmed.php. Este determina qué botones presentar:
if (!$isPast) { if ($model->isOrganizer()) { echo Html::a('<i class="glyphicon glyphicon-remove-circle"></i> '.Yii::t('frontend', 'Cancel'), ['cancel', 'id' => $model->id], ['class' => 'btn btn-primary btn-danger', 'title'=>Yii::t('frontend','Cancel'), 'data-confirm' => Yii::t('frontend', 'Are you sure you want to cancel this meeting?') ]) ; } else { if ($model->getParticipantStatus(Yii::$app->user->getId())==Participant::STATUS_DEFAULT) { echo Html::a('<i class="glyphicon glyphicon-remove-circle"></i> '.Yii::t('frontend', 'Withdraw'), ['decline', 'id' => $model->id], ['class' => 'btn btn-primary btn-danger', 'title'=>Yii::t('frontend','Withdraw from the meeting'), 'data-confirm' => Yii::t('frontend', 'Are you sure you want to decline attendance to this meeting?') ]) ; } else { // to do - offer rejoin meeting option }
La priorización es un elemento clave de construir una startup. Así que mientras quería ofrecer una manera para que un usuario se retirara o volviera a unir a una reunión, decidí agregar esto a la lista de tarea Asana para después. Un usuario que se vuelve a unir necesita notificaciones actualizadas y posiblemente algunas actualizaciones a sus estructuras de datos de programación.
Pensando a Través de Notificaciones
Mientras que con reuniones 1:1, cada cambio necesitaba ser enviado a la otra parte, esto no necesariamente tiene sentido para reuniones de 12 personas---¿o sí? Depende.
Inicialmente, creé lineamientos generales. Si un participante estaba actualizando sus preferencias para una fecha específica o lugar, solo el propietario y otros organizadores necesitaban ser actualizados sobre esto.
Creé un arreglo $groupSkip
en MeetingLog
que determinó eventos que no deberían ser enviados a otros participantes.
public static $groupSkip=[ MeetingLog::ACTION_ACCEPT_ALL_PLACES, MeetingLog::ACTION_ACCEPT_PLACE, MeetingLog::ACTION_REJECT_PLACE, MeetingLog::ACTION_ACCEPT_ALL_TIMES, MeetingLog::ACTION_ACCEPT_TIME, MeetingLog::ACTION_REJECT_TIME ];
En MeetingLog::getHistory
, saltamos notificar al participante para estos eventos pero siempre notificamos a los organizadores:
if ( ... // skip over availability response events in multi participant meetings ($isGroup && !$isOrganizer && in_array($e->action,MeetingLog::$groupSkip)) ) { $num_events-=1; // skip event, reduce number of events continue; }
En un ejemplo inusual, el código fue de hecho más simple con múltiples participantes: Meeting::findFresh()
, que busca actualizaciones de cambios de reunión para compartir vía email.
Más temprano, tuvimos que identificar cuál de los dos usuarios realizaron las dos acciones y si notificarlos o no también. Ahora, solo notificamos al propietario y después notificamos a los participantes:
if ((time()-$m->logged_at) > MeetingLog::TIMELAPSE && $m->status>=Meeting::STATUS_SENT) { // // get logged items which occured after last cleared_at $m->notify($m->id,$m->owner_id); // notify the participants foreach ($m->participants as $p) { // don't update removed and declined participants if ($p->status!=Participant::STATUS_DEFAULT) { continue; } //echo 'Notify P-id: '.$p->participant_id.'<br />'; $m->notify($m->id,$p->participant_id); }
Cualquier filtrado es hecho a más profundidad dentro de la textualización de registro del evento.
Notificaciones Mejoradas: "¡Todos Están Disponibles!"
También creé una nueva notificación para alertar a organizadores cuando todos están de acuerdo en al menos un lugar y fecha específicos, MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE
:
// check if meeting has place and time for everyone now if (count($m->participants)>1 && !MeetingLog::hasEventOccurred($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE) && Meeting::isEveryoneAvailable($m->id)) { Meeting::notifyOrganizers($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE); MeetingLog::add($m->id,MeetingLog::ACTION_SEND_EVERYONE_AVAILABLE,0); }
Esto notifica a los organizadores cuando la reunión está lista para finalizar/confirmar.
Aquí está el código que busca todos los lugares y tiempos de reunión para ver si todos están de acuerdo en al menos un lugar:
public static function isEveryoneAvailable($meeting_id) { // check that one place works for everyone attending $m = Meeting::findOne($meeting_id); $cntAll = $m->countAttendingParticipants(true); // count organizer + attending participants $mpExists=false; $mtExists=true; $mps = \frontend\models\MeetingPlace::find()->where(['meeting_id'=>$meeting_id])->all(); foreach ($mps as $mp) { $cnt=0; foreach ($mp->meetingPlaceChoices as $mpc) { if ($m->getParticipantStatus($mpc->user_id)!=Participant::STATUS_DEFAULT) { // skip withdrawn, declined, removed participants continue; } if ($mpc->status == \frontend\models\MeetingPlaceChoice::STATUS_YES) { $cnt+=1; } } if ($cnt >=$cntAll) { $mpExists = true; } } $mts = \frontend\models\MeetingTime::find()->where(['meeting_id'=>$meeting_id])->all(); foreach ($mts as $mt) { $cnt=0; foreach ($mt->meetingTimeChoices as $mtc) { if ($m->getParticipantStatus($mtc->user_id)!=Participant::STATUS_DEFAULT) { // skip withdrawn, declined, removed participants continue; } if ($mtc->status == \frontend\models\MeetingTimeChoice::STATUS_YES) { $cnt+=1; } } if ($cnt >=$cntAll) { $mtExists = true; } } // at least one time and one place works for everyone attending if ($mpExists && $mtExists) { return true; } else { return false; } }
De manera similar, construí una función para mostrar un aviso al organizador de que ninguna de las fechas y lugares son aceptables para cualquiera, Meeting::isSomeoneAvailable()
:
if ($model->status <= Meeting::STATUS_SENT) { if ($model->isOrganizer() && ($model->status == Meeting::STATUS_SENT) && !$model->isSomeoneAvailable()) { Yii::$app->getSession()->setFlash('danger', Yii::t('frontend', 'None of the participants are available for the meeting\'s current options.')); }
Esto indica que ellos deberían sugerir fechas y/o lugares adicionales.
Actualizando Recordatorios de Reunión
Todo acerca de recordatorios de reunión funcionó bien para múltiples participantes, pero necesité desactivar los recordatorios si un participante había declinado o retirado de una reunión o había sido removido:
$cnt =1; foreach ($mtg->participants as $p) { if ($p->status ==Participant::STATUS_DEFAULT) { $attendees[$cnt]=$p->participant_id; $cnt+=1; }
STATUS_DEFAULT
indica un asistente que debería ser agregado al arreglo de usuarios para recordatorios de email también.
Revisando Archivos de Calendario
También revisé el trabajo de generar archivos de calendario para invitaciones para asegurar que todos los asistentes están incluidos. En Meeting::prepareDownloadIcs()
, junté un arreglo de asistentes con el propietario y participantes asistiendo de manera activa:
$attendees = array(); foreach ($m->participants as $p) { if ($p->status ==Participant::STATUS_DEFAULT) { $auth_key=\common\models\User::find()->where(['id'=>$p->participant_id])->one()->auth_key; $attendees[$cnt]=['user_id'=>$p->participant_id,'auth_key'=>$auth_key, 'email'=>$p->participant->email, 'username'=>$p->participant->username]; $cnt+=1; // reciprocate friendship to organizer \frontend\models\Friend::add($p->participant_id,$p->invited_by); // to do - reciprocate friendship in multi participant meetings } } $auth_key=\common\models\User::find()->where(['id'=>$m->owner_id])->one()->auth_key; $attendees[$cnt]=['user_id'=>$m->owner_id, 'auth_key'=>$auth_key, 'email'=>$m->owner->email, 'username'=>$m->owner->username]; foreach ($attendees as $cnt=>$a) { if ($a['user_id']==$actor_id) { $icsPath = Meeting::buildCalendar($m->id,$chosenPlace,$chosenTime,$a,$attendees);
Durante este tiempo, también aprendí cómo indicar que un archivo de calendario de una reunión cancelada debería disparar la remosión del evento del calendario de alguien. El estándar ics es poderoso aunque no fácil de aprender.
Actualizando Cambios de Petición para Grupos
Como escribí recientemente, la característica de Solicitando Cambios de Reunión requirió mucho trabajo y una nueva UX.

Para reuniones de múltiples participantes, la ingeniería social necesitó ser cambiada de nuevo. Por ejemplo, los organizadores pueden aceptar o rechazar peticiones y cambiar el horario de la reunión. Sin embargo, los participantes solo pueden expresar Gusto, Disgusto o Indiferencia sobre peticiones de cambios. Y los organizadores deberían ver todas las respuestas de los participantes para ayudarlos en su toma de decisiones.
Aquí está lo que ve el participante después de que envían su petición de cambio:

Las nuevas peticiones de cambio necesitaban ser enviadas a todos los participantes. Esto es manejado transparentemente por las notificaciones de registro de actividad. Cunado una petición es hecha, el evento es creado en envío RequestController::actionCreate()
:
MeetingLog::add($model->meeting_id,MeetingLog::ACTION_REQUEST_CREATE,Yii::$app->user->getId(),$model->id);
Aquí está como luce la notificación de cambio solicitado para los otros participantes:

A todos se les pide responder. Dar clic a Responder a Petición salta justo a la petición. O puedes encontrarlo a través de la lista de peticiones desde el enlace de alerta en la reunión mostrada arriba.

Nueva UX para Participantes Respondiendo a Peticiones
Aquí está el formulario que los participantes ven cuando responden a una petición:

Si ya hay otras respuestas de participantes, ellos las verán:

Aquí está la parte superior de ese formulario en /frontend/views/request-response/_form.php:
<p><em> <?= $subject ?> </em> </p> <?= GridView::widget([ 'dataProvider' => $responseProvider, 'columns' => [ [ 'label'=>'Responses from Other Participants', 'attribute' => 'responder_id', 'format' => 'raw', 'value' => function ($model) { $note=''; if (!empty($model->note)) { $note = ' said, "'.$model->note.'"'; } return '<div>'.MiscHelpers::getDisplayName($model->responder_id).' '.$model->lookupOpinion().$note.'</div>'; }, ], ], ]); ?> <div class="request-response-form"> <?php $form = ActiveForm::begin(); ?> <?= BaseHtml::activeHiddenInput($model, 'responder_id'); ?> <?= BaseHtml::activeHiddenInput($model, 'request_id'); ?> <?= $form->field($model, 'note')->label(Yii::t('frontend','Include a note'))->textarea(['rows' => 6])->hint(Yii::t('frontend','optional')) ?>
El Gridview lista las respuestas existentes, ej. Gusto, Disgusto, Neutral y cualquier nota personal adjunta.
Entonces, aquí está el código de la mitad inferior del formulario, que mostrará Gusto, Disgusto, Indiferencia a los participantes pero Aceptar y Hacer Cambios o Declinar Petición a los propietarios.
<?php if (!$isOwner && $isOrganizer) { ?> <p><em><?= Yii::t('frontend','Since you are an organizer, you can accept the request and make the changes or reject it.');?></em></p> <?php } ?> <?php if ($isOrganizer) { ?> <div class="form-group"> <?= Html::submitButton(Yii::t('frontend', 'Accept and Make Changes'), ['class' => 'btn btn-success','name'=>'accept',]) ?> <?= Html::submitButton(Yii::t('frontend', 'Decline Request'),['class' => 'btn btn-danger','name'=>'reject', 'data' => [ 'confirm' => Yii::t('frontend', 'Are you sure you want to decline this request?'), 'method' => 'post', ],]) ?> </div> <?php } ?> <?php if (!$isOwner && $isOrganizer) { ?> <p><em><?= Yii::t('frontend','Or, you can just express your opinion and defer to other organizers.');?></em></p> <?php } ?> <?php if (!$isOwner) { ?> <?php if (!$isOrganizer) { ?> <p><em><?= Yii::t('frontend','Please share your opinion of this request for the organizers to consider.');?></em></p> <?php } ?> <div class="form-group"> <?= Html::submitButton(Yii::t('frontend', 'Like'), ['class' => 'btn btn-success','name'=>'like',]) ?> <?= Html::submitButton(Yii::t('frontend', 'Don\'t Care'), ['class' => 'btn btn-info','name'=>'neutral',]) ?> <?= Html::submitButton(Yii::t('frontend', 'Dislike'),['class' => 'btn btn-danger','name'=>'dislike',]) ?> </div> <?php } ?> <?php ActiveForm::end(); ?>

A los participantes que son organizadores se les muestran ambos conjuntos de botones y pueden expresar su opinión o declinar el cambio.
Aquí está el email de notificación de que un cambio ha sido aceptado:

Por supuesto, una invitación actualizada y archivos de calendario serán enviados a todos si el cambio es realizado.
Siempre Hay Más Mejoras que Hacer
Espero que hayas disfrutado estos dos episodios (el de hoy y Construyendo Tu Startup: Reuniones Con Múltiples Participantes). En modo startup con una enorme nueva característica, siempre hay un esfuerzo enfocado y coordinado, que deja muchos cabos sueltos sin atender y defectos sin pulir.
Unos cuantos ejemplos de esto incluyen:
- Volviéndose a unir a reuniones que has declinado o de las que te has retirado.
- Presentación mejorada de lista de participantes en invitaciones.
- Opciones para mantener la lista de participantes y/o sus estados individuales privados de otros participantes.
- Manejo mejorado de presentación de información de contacto de grupo y detalles de conferencia virtual, ej. una línea de conferencia y código de participación.
- URL segura para compartir invitaciones de reunión. Esto permitiría a los organizadores compartir una URL en Facebook o vía email para invitar a nuevos participantes.
A pesar de estas deficiencias, he trabajado muy arduamente para alcanzar este nivel de función y usabilidad para Planificador de Reuniones. Estoy super emocionado sobre este progreso y escuchando buena retroalimentación sobre este desde amigos y colegas.
Estoy entregando estos dos tutoriales hoy y tomando unos cuantos días fuera de línea en el bosque para algún tiempo de relajación. El descanso es importante. Estar en contacto con la naturaleza te ayuda a recordarte lo que es importante en la vida---las startups no lo son siempre, de hecho. Podría haber presecuciones creativas, importantes para nuestro ingreso y carreras, podrían en algunos casos ayudar a la gente a vivir más eficiente y productivamente--pero son frecuentemente distantes de la tierra, de amistades y de ayudar a los menos afortunados. Estas son todas las cosas sobre las que pienso día con día y lo haré de nuevo mientras estoy fuera.
Me pregunto a mi mismo repetidamente si estoy haciendo todo lo que quiero hacer con mi tiempo---especialmente a la luz de mi cirugía de cerebro.
También tomo el corazón del hecho de que estoy orgulloso de Planificador de Reuniones, especialmente el trabajo hasta la fecha y su creciente utilidad. De manera general, la beta del servicio se está acercando a estar completo.
Las reuniones de múltiples participantes ha sido el elemento restante de trabajo más desalentador y complejo. Mirando hacia adelante, las características son más moderadas, pequeñas y más fácilmente manejables. ¡Estoy emocionado sobre sus futuros prospectos!
Si no lo has hecho aún, ¡programa ahora tu primera reunión con Planificador de Reuniones!
Un tutorial sobre corwdfunding también está en marcha, así que por favor sigue nuestra página WeFunder Meeting Planer.
También puedes contactarme en @reifman. Siempre estoy abierto a nuevas ideas de características y sugerencias de tópicos para futuros tutoriales.
Mantente en sintonía para este y más tutoriales próximos revisando la serie Construyendo Tu 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