
Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.
Введение в групповые встречи
Планирование встреч с несколькими участниками всегда было частью моего плана, но не частью самого раннего продукта с минимальной жизнеспособностью (MVP). Выпущена альфа-версия Meeting Planner с расписанием 1: 1. Цель поддержки группового планирования находилась в списке задач, таких как гора Эверест, для альпиниста, стремящегося к семи встречам на высшем уровне (а я даже не открытый альпинист).
Встречи с несколькими участниками являются наиболее сложными для планирования и, следовательно, ценными для предлагаемого продукта. Я был взволнован, когда список задач бета-тестирования достиг того момента, когда я смог начать работу над этим.
Я планировал, проектировал и программировал с учетом групповых встреч с самого начала. Я надеялся, что обновление сайта для этой функции не потребует значительных изменений UX или обновлений кодирования. Оказалось, что требуется средний путь, 7-10 дней очень сфокусированной работы и тестирования, но без серьезной реорганизации.
На самом деле, тестирование оказалось самым сложным аспектом построения этой функции. Это также помогло выявить недостатки в более раннем коде. Просто это не так просто … отправка на несколько адресов электронной почты, проверка того, что каждый из них получает все правильные уведомления, но не неправильные уведомления, и видит все правильные пункты меню по всему сайту.
В сегодняшнем уроке я расскажу о включении нескольких участников, обновлении UX для групп, назначении организаторов, удалении участников и сортировке опций даты, времени и места по их популярности среди участников.
В следующем уроке я опишу остальную часть работы: просмотр всех областей сайта, затронутых несколькими собраниями участников, обработка и умное отображение списков получателей с различным статусом, правильное управление уведомлениями и фильтрацией уведомлений для групп, и, наконец, обновление недавно запущенной функции изменения запроса на встречу .
Попробуйте запланировать встречу группы
Пожалуйста, назначьте встречу группы сегодня ! Поделитесь своими мыслями и отзывами в комментариях ниже.
Я участвую в обсуждениях, но вы также можете связаться со мной @reifman в Twitter. Я всегда открыт для новых идей для планировщика собраний, а также предложений для будущих серий.
Напоминаем, что весь код для Meeting Planner предоставляется с открытым исходным кодом и написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с моей параллельной серией Программирование с Yii2 . Я много слышал о Laravel, но Yii2 всегда отвечает моим потребностям быстро и легко.
Оглядываясь назад
Когда я впервые разработал интерфейс планирования Планировщика собраний , он показал текущую доступность другого участника в своем собственном столбце. И это немного сбивало с толку, так как были отключены элементы управления.

В то время я беспокоился о том, как бы освободить место для отображения доступности групп.
К счастью, когда я перестроил UX для лучшего восприятия , я заменил столбец доступности участников на небольшую текстовую сводку:

Текстовое резюме о наличии по совпадению будет хорошо работать для групповых встреч.
Первым делом изменив дизайн для мобильных устройств, я преодолел самый значительный барьер UX для встреч нескольких участников!
Кодирование для групповых встреч
Давайте начнем изучать весь код и тестирование, необходимое для нескольких встреч участников.
Включение нескольких участников

Самым забавным аспектом групповых встреч является то, что их активизация была простой. Мне просто нужно было отключить отключение кнопки « плюс» на панели « Кто» для собраний на этапе планирования:
|
1
2
3
4
5
|
<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>
|
Затем я начал с создания MEETING_LIMIT в модели участника:
|
1
2
3
4
5
|
class Participant extends \yii\db\ActiveRecord
{
…
const MEETING_LIMIT = 15;
|
Он используется в ParticipantController::actionCreate() при ParticipantController::actionCreate() :
|
1
2
3
4
5
6
|
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]);
}
|
Продвижение UX и связанных с ним функций
В течение долгого времени я хотел разрешить организаторам собраний удалять участников, места и время, не перегружая пользовательский интерфейс. Точно так же я понял, что может быть несколько команд для участников.
После того, как я нашел такую полезную кнопку в выпадающем меню компактной Bootstrap в учебном пособии по расширенным командам , я решил использовать ее для отображения участников собрания:

Организаторы отмечены звездочкой. Участники, которые отклонили собрание, отображаются оранжевым цветом. Участники, удаленные организаторами, отображаются красным цветом.
Вот код в моем новом частичном /frontend/views/participant/_buttons.php:
|
01
02
03
04
05
06
07
08
09
10
|
<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»>
<?= MiscHelpers::getDisplayName($model->owner_id) ?>
<span class=»caret»>
</button>
<ul class=»dropdown-menu»>
<li><?= Html::a(Yii::t(‘frontend’,’Send a message’),Url::to(‘mailto:’.$model->owner->email))?></li>
</ul>
</div>
|
Любой может теперь отправить сообщение любому участнику (функции заметок собрания в настоящее время распространяются на всех участников собрания).
Организаторы видят более глубокий выпадающий список, который позволяет им назначить дополнительных организаторов, например, Сделать организатора . Теперь это очень крутая функция. Организаторы будут получать более полные уведомления и иметь больше возможностей на всех этапах планирования. Они также могут удалить участников .
Встраивание функций AJAX в кнопки участников
Я решил по прихоти AJAXify все эти пункты меню. Оказалось, что это требует нескольких сложных часов кодирования.
Вот код, который определяет начальное меню кнопок и подготавливает JavaScript:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<?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»>
<?= MiscHelpers::getDisplayName($p->participant->id) ?>
<span class=»caret»>
</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 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 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 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;»]);
<?php
}
?>
</ul>
</div>
|
Существует так много состояний кнопок, цветов и звездочек, которые нужно обновлять, так как изменения вносятся в интерактивном режиме на странице, поэтому код становится довольно сложным. Я добавил функции в файл JavaScript toggleOrganizer() для функции toggleOrganizer() , т. toggleParticipant() Организатор make / unset и toggleParticipant() , т.е. удалить / восстановить участника в качестве участника.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
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;
}
});
}
|
Для обработки запросов переключения и обновления баз данных необходимы сопутствующие методы контроллера JSON в ParticipantController.php:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
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;
}
}
|
Активация функции аккордеона на панелях

В это время я также понял, что по мере усложнения планов встреч с увеличением количества получателей и вариантов будет больше прокрутки. Я решил реализовать функцию аккордеона Bootstrap для всех панелей в нашем виде собрания.
Другими словами, теперь вы можете нажать на заголовок, чтобы свернуть или открыть каждую и / или все панели.
Вот изменения в части для места встречи _panel.php:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<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>
|
Обратите внимание на настройки выше в panel-heading а затем на окружающий div для более позднего panel-body . Они управляют открытием и разрушением каждой панели.
Это привело к небольшим косметическим проблемам, таким как нежелательное заполнение списка элементов, которые мне нужно будет убрать в будущем.
Модельная инфраструктура для групповых встреч
Хотя я планировал создать несколько участников с самого начала, было несколько незначительных или скромных улучшений инфраструктуры для их поддержки.
Хотя модели MeetingTimeChoice и MeetingPlaceChoice отслеживают, предпочитают ли участники определенное время и места, я хотел отслеживать общую доступность для всех участников в каждое время и место. Это позволило бы мне сортировать места и время по их популярности и показывать самые популярные настройки в верхней части панелей.
Сначала я создал миграцию, чтобы добавить это к обеим моделям. Нередко моя миграция затрагивает несколько моделей, что делает этот тип особенным:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?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’);
}
}
|
Благодаря этой способности я смог начать отображать возможные даты и времени встреч, отсортированные по популярности среди участников, из MeetingController::actionView() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
$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
]
],
]);
|
Вы можете увидеть это в действии на скриншоте ниже:

Чтобы отследить, являются ли участники организаторами и разрешить ли в будущем отказ от уведомлений о конкретных собраниях, я добавил эту миграцию для таблицы «Участники»:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?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’);
}
}
|
Я также добавил ряд констант в Participant.php для работы с этими свойствами:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
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;
|
И я знал, что было бы полезно иметь некоторые вспомогательные функции в рамках массивной модели Meeting. Например, IsOrganizer() сообщает мне, является ли текущий зритель организатором собрания:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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;
}
|
Подождите, есть еще?
Как вы можете видеть, есть много возможностей для создания этой функции. В следующем эпизоде я расскажу о второй половине разработки и тестирования, необходимых для запуска нескольких встреч участников: строки получателей, уведомления, запросы и ответы на запросы.
Если вы еще этого не сделали, запланируйте свою первую встречу с Meeting Planner и попробуйте все это. Пожалуйста, поделитесь своим мнением в комментариях ниже.
Учебное пособие по краудфандингу также находится в разработке, поэтому, пожалуйста, следуйте нашей странице WeFunder Meeting Planner .
Вы также можете обратиться ко мне @reifman . Я всегда открыт для новых идей и тематических предложений для будущих уроков.
Следите за всеми этими и другими учебниками, ознакомившись с серией « Построение стартапа на PHP» .