本教程是 Envato Tuts +上的“ 使用PHP构建启动”系列 的一部分。 在本系列文章中,我 将以 我的 Meeting Planner 应用程序作为真实示例 ,指导您完成从概念到现实的启动 。 在此过程的每一步中,我都会将Meeting Planner代码作为开放源代码示例发布,您可以从中学习。 我还将解决与启动相关的业务问题。
小组会议简介
与多个参与者安排会议始终是我计划的一部分,但不属于最早的最低可行产品(MVP)的一部分。 Meeting Planner的Alpha版本仅以1:1的计划启动。 支持小组日程安排的目标像登上珠穆朗玛峰一样位于任务列表上,目标是针对七个峰会的登山者(我什至都不是户外登山者)。
安排多个参与者会议是最具挑战性的,因此对于会议计划器产品而言非常有价值。 当Beta任务列表达到可以开始从事此工作的程度时,我感到很兴奋。
我从一开始就一直在计划,设计和编码小组会议。 我希望为此功能更新站点将不需要进行大量的UX更改或编码更新。 事实证明,这需要一条中间路线,需要7到10天的重点工作和测试,而无需进行重大重新架构。
实际上,事实证明,测试是构建此功能最困难的方面。 它还有助于揭示早期代码中的缺点。 只是这并不容易...发送到多个电子邮件地址,检查每个电子邮件地址都收到所有正确的通知,但没有收到错误的通知-并查看整个站点中所有正确的菜单选项。
在今天的教程中,我将介绍启用多个参与者,升级组的UX,任命组织者,删除参与者,以及根据参与者的受欢迎程度对日期,时间和地点选项进行排序。
在下一个教程中,我将描述其余的工作:复查受多个参与者会议影响的站点的所有区域,处理并智能显示各种状态的收件人列表,适当地管理组的通知和通知过滤,最后升级最近启动的请求会议更改功能 。
尝试安排小组会议
请今天安排一个小组会议 ! 在下面的评论中分享您的想法和反馈。
我确实参与了讨论,但是您也可以在Twitter上与我联系@reifman 。 我总是乐于接受Meeting Planner的新功能建议以及有关未来系列剧集的建议。
提醒一下,Meeting Planner的所有代码都是开源提供的,并使用PHP的Yii2框架编写。 如果您想了解有关Yii2的更多信息,请查看我的平行系列“ 使用Yii2编程” 。 我听说过有关Laravel的很棒的事情,但是Yii2总是可以轻松快捷地满足我的需求。
回头看
当我第一次设计Meeting Planner计划界面时 ,它在其自己的列中显示了其他参与者的当前可用性。 由于存在禁用的控件,这有点令人困惑。
当时,我担心如何腾出空间来显示群组的可用性。
幸运的是,当我重建UX以获得更好的响应体验时 ,我用一个小的文本摘要替换了参与者的可用性列:
可用性的文本摘要恰好适合小组会议。
通过首先针对移动设备进行重新设计,我解决了多次参加会议的最大用户体验障碍!
小组会议的编码
让我们开始遍历所有代码并测试多个参与者会议所需的内容。
允许多个参与者
小组会议最有趣的方面是激活会议很简单。 我只需要在计划阶段关闭“ 谁”面板上的加号图标按钮的禁用功能即可:
<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
:
class Participant extends \yii\db\ActiveRecord
{
...
const MEETING_LIMIT = 15;
它在提交时用于ParticipantController::actionCreate()
中:
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]);
}
推进用户体验和相关功能
很长一段时间以来,我一直希望允许会议组织者删除参与者,地点和日期时间,而又不会使用户界面混乱。 同样,我意识到可能会有多个命令要对参与者执行。
在“ 高级命令”教程的紧凑型Bootstrap下拉按钮中找到了很多实用程序之后,我决定将其用于显示会议与会者:
组织者以星号表示。 拒绝参加会议的与会者以橙色显示。 组织者删除的与会者以红色显示。
这是我新的部分/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>
现在任何人都可以向任何参与者发送消息(会议记录功能当前已分发给所有会议参与者)。
组织者看到了更深的下拉菜单,这使他们可以任命其他组织者,即Make Organer 。 现在,这是一个非常酷的功能。 组织者将在整个计划阶段收到更完整的通知,并拥有更多权力。 他们还可以删除参与者 。
将AJAX功能构建到参与者按钮中
我一时兴起就决定将所有这些菜单选项都AJAXify。 事实证明,这需要几个小时的编码。
这是定义初始按钮菜单并准备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>
当页面上以交互方式进行更改时,要更新的按钮状态,颜色和星号太多,因此代码变得相当复杂。 我将功能添加到toggleOrganizer()
JavaScript文件中的toggleOrganizer()
,即make / unset toggleParticipant()
和toggleParticipant()
, 即删除/恢复参与者作为参与者。
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;
}
});
}
这些在ParticipantController.php中需要随附的JSON控制器方法来处理切换请求和更新数据库:
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的局部的更改:
<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
上的设置,然后注意后面的panel-body
周围的div。 这些控制每个面板的打开和折叠。
这导致了一些小的外观问题,例如在项目列表周围出现不必要的填充,我将来需要清理这些问题。
小组会议的模型基础架构
尽管我几乎从一开始就计划有多个参与者,但在基础设施方面有一些次要的改进以支持他们。
虽然MeetingTimeChoice
和MeetingPlaceChoice
模型跟踪参与者是否喜欢特定的日期时间和地点,但我想跟踪每个日期和地点所有参与者的总体可用性。 这将使我能够根据地点和时间的受欢迎程度对其进行排序,并在面板顶部显示最受欢迎的设置。
首先,我创建了一个迁移,以将其添加到两个模型中。 我的迁移很少会影响多个模型,这使得这种特殊情况变得如此:
<?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()
开始显示可能的会议日期时间和地点, 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
]
],
]);
您可以在下面的计划屏幕截图中看到这一点:
为了跟踪参与者是否是组织者,并允许将来退出特定会议的通知,我为“参与者”表添加了此迁移:
<?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中添加了一些常量以使用这些属性:
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;
我知道在大型会议模型中具有一些辅助功能会很有帮助。 例如, IsOrganizer()
告诉我当前查看者是否是会议组织者:
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构建您的启动”系列,继续关注所有这些以及以后的教程。