Introduction
This tutorial is part of the Building Your Startup With PHP series on Envato Tuts+. In this series, I'm guiding you through launching a startup from concept to reality using my Meeting Planner app as a real-life example. Every step along the way, I'll release the Meeting Planner code as open-source examples you can learn from. I'll also address startup-related business issues as they arise.
Why the Gap in This Series?
You may notice that there's been a huge gap in time between the last episode and this one. In April 2015, I was diagnosed with a brain tumor which required surgery and radiation. I'm incredibly fortunate overall to have such good care, and things went pretty well—many people do not have access to the quality of neurosurgery resources available to me with health insurance in the Pacific Northwest. I've been writing again for Envato Tuts+ since last fall, but it's fun to finally return focus to the startup series, and I hope you enjoy it.
What Does This Episode Cover?
In this tutorial, we'll cover the custom features that are needed to provide different views depending on who is looking at meeting invitations. Before we begin emailing the invitation to participants, we need to have a view ready with potentially restricted functionality to share with them. Essentially, we're making sure the meeting view is exactly what's needed for the meeting owner and the meeting participant. Follow along to learn what's needed.
All of the code for Meeting Planner is written in the Yii2 Framework for PHP, which has built-in support for I18n. If you'd like to learn more about Yii2, check out our parallel series Programming With Yii2 at Envato Tuts+.
Interestingly, a potential angel investor recently approached me about contributing resources to speed the development process of our site—he sees the value in the concept. As I sort out the appropriate path to continue forward, I'll keep you posted. If anything, I hope it creates intriguing new tutorial topics around managing the investment process as an entrepreneur.
Just a reminder, I do participate in the comment threads below. I'm especially interested if you have different approaches, additional ideas or want to suggest topics for future tutorials. You can also reach me on Twitter @reifman.
Meeting View Requirements
It's kind of exciting—soon, Meeting Planner will be delivering invitations to invited participants. However, to support that, we need to make sure that the meeting view page is properly configured. If you created the meeting, you have certain powers, such as inviting participants, adding proposed meeting places, dates and times, and choosing the final selections. In some cases, the organizer may want to offer some or all of these powers to participants as well.
Essentially, we have to make the application aware of who's viewing the meeting page and then customize the appearance and commands available. Yii makes most of this pretty easy, but there's a lot of detail involved.
A Brief Caveat About the User Experience
And let me say up front, there is a lot of user experience rework and polish that will need to be done iteratively over time on the way to the minimum viable product (MVP). Most of what I'm building right now is core functionality to get the alpha running for actual usage. I know it looks rough in places and won't always seem as intuitive as you want. There are also coding inefficiencies that will need to be optimized in the future. Please feel free to post your thoughts and comments below and I will take them into account for ongoing work.
The Current Meeting View
Here's a look at the existing meeting view that the creator (or owner) sees:
The Send button emails the meeting invitation with the current open options to the participant for them to offer feedback. The checkboxes below You and Them allow the viewer to express whether location(s) and time(s) work for them. The Choose checkboxes allow the viewer to determine the final place and final time. The Finalize button puts the meeting on the schedule with the chosen place and time options.
Certainly, as the product matures, we'll want to improve the user experience in a number of ways and polish it a lot, but here are a handful of functional elements we'd like to modify for participants:
- The Send button won't be needed after the owner delivers the invitation.
- Participants may or may not be allowed to Finalize meeting options.
- Participants won't be able to Edit (pencil icon) the meeting detail text.
- Participants won't be able to add People at this time (for our MVP).
- Participants may or may not be allowed to add Places (plus icon).
- Participants may or may not be allowed to add Dates & Times (plus icon).
- In both Places and Dates & Times panels, we'll want to show the current viewer's choices under the You column and the other person's data in Them.
- In both Places and Dates & Times panels, participants may or may not be able to Choose the final location and time.
All of these options need to be addressed in our work today. Let's walk through what's required to build these features.
Requirements Implementation
If you're following along with the code, the updates described here are included in this release on GitHub.
Who's the Current Viewer
The Yii Framework provides the current user_id
for the viewer here:
$user_id = Yii::$app->user->getId()
The Meeting model has the $owner_id
property and isOwner
function to help determine if the current viewer is actually the meeting's creator. If not, the viewer will have conditionally less control over the meeting.
I've created a couple of helper functions in the Meeting model to make this faster:
public function setViewer() { $this->viewer_id = Yii::$app->user->getId(); if ($this->owner_id == $this->viewer_id) { $this->viewer = Meeting::VIEWER_ORGANIZER; } else { $this->viewer = Meeting::VIEWER_PARTICIPANT; } }
These configure the $owner_id
and $viewer
properties in the Meeting model.
Building for Meeting Settings
Every meeting you create will likely have different characteristics. Sometimes, you'll want to limit the participant from suggesting different times and places or finalizing the details. Other times, you won't care. When we eventually create Meeting Templates for reusing common types of meetings, e.g. morning coffee business meetings, the Templates will likely need to retain these kinds of custom settings as well. How should we implement this?
First, I'd like to create a set of default preferences for users with respect to the meetings they create.
Then, I'll create a set of MeetingSettings for every meeting. When a meeting is created from scratch, they'll inherit the default preferences from the user that creates it. Editing the settings for individual meetings can be postponed until later.
In the future, when we implement the Meeting Templates, we'll add Meeting settings for the Templates too. However, this can also be postponed.
Here are the preferences that we'd like to create to start:
- Allow participants to add Places.
- Allow participants to add Dates & Times.
- Allow participants to choose Places.
- Allow participants to choose Dates & Times.
- Allow participants to Finalize the meeting.
Since we're all coming back to the series after some time because of my health absence, I'll go through a bit more detail on some of the work.
First, we'll create the Meeting Settings migration:
$ ./yii migrate/create meeting_setting_table Yii Migration Tool (based on Yii v2.0.7) Create new migration '/Users/Jeff/Sites/mp/console/migrations/m160401_203412_meeting_setting_table.php'? (yes|no) [no]:yes New migration created successfully.
That creates the migration file we need to write the code which builds the database table according to our schema:
<?php use yii\db\Schema; use yii\db\Migration; class m160401_203412_meeting_setting_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->createTable('{{%meeting_setting}}', [ 'id' => Schema::TYPE_PK, 'meeting_id' => Schema::TYPE_INTEGER.' NOT NULL', 'participant_add_place' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', 'participant_add_date_time' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', 'participant_choose_place' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', 'participant_choose_date_time' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', 'participant_finalize' => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0', 'created_at' => Schema::TYPE_INTEGER . ' NOT NULL', 'updated_at' => Schema::TYPE_INTEGER . ' NOT NULL', ], $tableOptions); $this->addForeignKey('fk_meeting_setting', '{{%meeting_setting}}', 'meeting_id', '{{%meeting}}', 'id', 'CASCADE', 'CASCADE'); } public function down() { $this->dropForeignKey('fk_meeting_setting', '{{%meeting_setting}}'); $this->dropTable('{{%meeting_setting}}'); } }
Each meeting essentially has a row of MeetingSettings with boolean properties for the various participant options I've shown above.
Then, we instruct Yii to migrate up and create the table:
$ ./yii migrate/up Yii Migration Tool (based on Yii v2.0.7) Total 1 new migration to be applied: m160401_203412_meeting_setting_table Apply the above migration? (yes|no) [no]:yes *** applying m160401_203412_meeting_setting_table > create table {{%meeting_setting}} ... done (time: 0.010s) > add foreign key fk_meeting_setting: {{%meeting_setting}} (meeting_id) references {{%meeting}} (id) ... done (time: 0.011s) *** applied m160401_203412_meeting_setting_table (time: 0.040s) 1 migration was applied. Migrated up successfully.
Our foreign key creates a relation between the Meeting table and the MeetingSetting table.
Next, we'll use Yii's Gii to auto-generate code for viewing and updating the settings. To begin, I return to http://localhost:8888/mp/index.php/gii/. We'll start with generating the model:
Then, we'll generate the Create, Read, Update, Delete code (CRUD):
Since we don't need all that code right now, Gii lets us select just the functions that we do need: the controller, view, _form and update:
Gii shows you a list of the files it creates with each step:
But what about the user's default meeting settings? Essentially, their typical meeting preferences?
Extending the User Preferences
For that, we'll add parallel meeting setting properties to the user_setting
table. Again, we'll create a migration:
$ ./yii migrate/create extend_user_setting_table Yii Migration Tool (based on Yii v2.0.7) Create new migration '/Users/Jeff/Sites/mp/console/migrations/m160401_210852_extend_user_setting_table.php'? (yes|no) [no]:yes New migration created successfully.
Here are the columns we need to add:
class m160401_210852_extend_user_setting_table extends Migration { public function up() { $tableOptions = null; if ($this->db->driverName === 'mysql') { $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; } $this->addColumn('{{%user_setting}}','participant_add_place',Schema::TYPE_SMALLINT.' NOT NULL'); $this->addColumn('{{%user_setting}}','participant_add_date_time',Schema::TYPE_SMALLINT.' NOT NULL'); $this->addColumn('{{%user_setting}}','participant_choose_place',Schema::TYPE_SMALLINT.' NOT NULL'); $this->addColumn('{{%user_setting}}','participant_choose_date_time',Schema::TYPE_SMALLINT.' NOT NULL'); $this->addColumn('{{%user_setting}}','participant_finalize',Schema::TYPE_SMALLINT.' NOT NULL'); } public function down() { $this->dropColumn('{{%user_setting}}','participant_finalize'); $this->dropColumn('{{%user_setting}}','participant_choose_date_time'); $this->dropColumn('{{%user_setting}}','participant_choose_place'); $this->dropColumn('{{%user_setting}}','participant_add_date_time'); $this->dropColumn('{{%user_setting}}','participant_add_place'); } }
Then, we'll run the migration:
$ ./yii migrate/up Yii Migration Tool (based on Yii v2.0.7) Total 1 new migration to be applied: m160401_210852_extend_user_setting_table Apply the above migration? (yes|no) [no]:yes *** applying m160401_210852_extend_user_setting_table > add column participant_add_place smallint NOT NULL to table {{%user_setting}} ... done (time: 0.012s) > add column participant_add_date_time smallint NOT NULL to table {{%user_setting}} ... done (time: 0.007s) > add column participant_choose_place smallint NOT NULL to table {{%user_setting}} ... done (time: 0.010s) > add column participant_choose_date_time smallint NOT NULL to table {{%user_setting}} ... done (time: 0.009s) > add column participant_finalize smallint NOT NULL to table {{%user_setting}} ... done (time: 0.009s) *** applied m160401_210852_extend_user_setting_table (time: 0.061s) 1 migration was applied. Migrated up successfully.
Rather than force an overwrite of our UserSetting.php model with Gii, we'll use Gii's diff option:
And, from there, we'll hand pick the new additions to the file and paste them in:
Functionally, we'll add a meeting settings tab to the Update Your Settings property page:
We'll add the following code to /frontend/views/user-setting/_form.php
to support our new properties:
<div class="col-md-8"> <!-- Nav tabs --> <ul class="nav nav-tabs" role="tablist"> <li class="active"><a href="#general" role="tab" data-toggle="tab"><?= Yii::t('frontend','General Settings') ?></a></li> <li><a href="#preferences" role="tab" data-toggle="tab"><?= Yii::t('frontend','Meeting Preferences') ?></a></li> <li><a href="#photo" role="tab" data-toggle="tab"><?= Yii::t('frontend','Upload Photo') ?></a></li> </ul> <!-- Tab panes --> <div class="tab-content"> ... </div> <div class="tab-pane vertical-pad" id="preferences"> <?= $form->field($model, 'participant_add_place')->checkbox(['uncheck' => $model::SETTING_NO, 'checked' => $model::SETTING_YES]); ?> <?= $form->field($model, 'participant_add_date_time')->checkbox(['uncheck' => $model::SETTING_NO, 'checked' => $model::SETTING_YES]); ?> <?= $form->field($model, 'participant_choose_place')->checkbox(['uncheck' => $model::SETTING_NO, 'checked' => $model::SETTING_YES]); ?> <?= $form->field($model, 'participant_choose_date_time')->checkbox(['uncheck' => $model::SETTING_NO, 'checked' => $model::SETTING_YES]); ?> <?= $form->field($model, 'participant_finalize')->checkbox(['uncheck' => $model::SETTING_NO, 'checked' => $model::SETTING_YES]); ?> </div> <!-- end of upload meeting-settings tab --> <div class="tab-pane vertical-pad" id="photo"> ...
Here's the updated form:
Initializing New Meeting Sessions
Whenever the user creates a new meeting, we have to load their default settings and copy them to the individual meeting's settings. initializeMeetingSetting is called when a new meeting is created to do this:
public function initializeMeetingSetting($meeting_id,$owner_id) { // load meeting creator (owner) user settings to initialize meeting_settings $user_setting = UserSetting::find()->where(['user_id' => $owner_id])->one(); $meeting_setting = new MeetingSetting(); $meeting_setting->meeting_id = $meeting_id; $meeting_setting->participant_add_place=$user_setting->participant_add_place; $meeting_setting->participant_add_date_time=$user_setting->participant_add_date_time; $meeting_setting->participant_choose_place=$user_setting->participant_choose_place; $meeting_setting->participant_choose_date_time=$user_setting->participant_choose_date_time; $meeting_setting->participant_finalize=$user_setting->participant_finalize; $meeting_setting->save(); }
With meeting settings in place, we're ready to move on to what's actually the bulk of today's work, customizing the meeting views for the owner and participant.
Reviewing the Meeting Owner View
Now, let's consider the state of our meeting view based on the meeting creator or owner. Here's a meeting invitation I've recently created to invite my friend Rob to drinks:
The Command Bar
Before Send and Finalize should be enabled, there must be a person invited and at least one place and time. If there are more than one place and time, one must be chosen for the meeting to be finalized.
The Cancel (X icon) and Edit (pencil icon) meeting buttons are also enabled for creators.
People
For the MVP, we're limiting meeting invitations to one participant at first. So, once a person has been invited, the Add (plus icon) button is disabled.
Places and Date & Times
The creator can add Places and Date & Times up to our site's maximum (e.g. seven per meeting) and they can indicate their availability and acceptance. And, finally, when there is more than one, they can choose which location and time will be used.
Notes
The creator can always add notes to the meeting. Notes allow the creator and participants to communicate with each other.
Ultimately, we'll put the bulk of our work into improving the AJAX functionality so that as the owner chooses places and times, the Send and Finalize buttons are properly enabled (or disabled in some cases).
Here's an example of a meeting with two possible times. The Finalize button can't be enabled until one time is chosen:
Once the choice is made, we'd like to enable the Finalize button via AJAX, sparing the user a page refresh.
Reviewing the Participant View
When we view the invitation from the participant's point of view, there's a lot less initial capability:
The participant can cancel (X icon) their attendance to the meeting and they can specify whether the places and times are acceptable to them, but they can't choose the final place or Finalize the meeting. Also, the data in the You and Them columns are now switched. And, the participant panel is hidden as it's not needed.
Additionally, let's say the meeting was created with settings that allowed the participant to choose the location, date and time but not finalize the meeting. That would need to look like this:
Since there's only one Place, Herkimer Coffee, there's no need for a choice selector. But, where there are two possible times, you can now see the Choose selectors. Still, there is no Finalize button.
It turned out that supporting all of this required a lot of new code to update the system, but this is beginning to dive into the heart of the product—the scheduling meetings user experience. I'll walk you through a handful of the changes that were needed.
Coding the Meeting Requirements
Implementing the Meeting Settings
In the meeting-time and meeting-place panels, we need to use the meeting settings to determine if we need to show the choice selector. In the _panel.php view, it looks like this:
<table class="table"> <thead> <tr class="small-header"> <td></td> <td ><?=Yii::t('frontend','You') ?></td> <td ><?=Yii::t('frontend','Them') ?></td> <td > <?php if ($timeProvider->count>1 && ($isOwner || $model->meetingSettings->participant_choose_date_time)) echo Yii::t('frontend','Choose'); ?> </td> </tr> </thead> <?= ListView::widget([ 'dataProvider' => $timeProvider, 'itemOptions' => ['class' => 'item'], 'layout' => '{items}', 'itemView' => '_list', 'viewParams' => ['timeCount'=>$timeProvider->count,'isOwner'=>$isOwner,'participant_choose_date_time'=>$model->meetingSettings['participant_choose_date_time']], ]) ?> </table>
We're checking the participant settings and passing them as a parameter to the subsequent _list.php view, which looks like this:
<td style> <?php if ($timeCount>1) { if ($model->status == $model::STATUS_SELECTED) { $value = $model->id; } else { $value = 0; } if ($isOwner || $participant_choose_date_time) { // value has to match for switch to be on echo SwitchInput::widget([ 'type' => SwitchInput::RADIO, 'name' => 'time-chooser', 'items' => [ [ 'value' => $model->id], ], 'value' => $value, 'pluginOptions' => [ 'size' => 'mini','handleWidth'=>60,'onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>'], 'labelOptions' => ['style' => 'font-size: 12px'], ]); } } ?> </td>
If the view is the creator or the participant is allowed to choose the final time, they'll see something like this, the ability to Choose in the right column:
Can the Viewer Send and Finalize the Meeting
I created canSend()
and canFinalize()
functions, which support the code generally and the AJAX requests to determine the active state of Send and Finalize buttons.
Here's canSend()
:
public function canSend($sender_id) { // check if an invite can be sent // req: a participant, at least one place, at least one time if ($this->owner_id == $sender_id && count($this->participants)>0 && count($this->meetingPlaces)>0 && count($this->meetingTimes)>0 ) { $this->isReadyToSend = true; } else { $this->isReadyToSend = false; } return $this->isReadyToSend; }
The organizer can't send the meeting invitation until there are participant(s), places and times.
Here's canFinalize()
:
public function canFinalize($user_id) { $this->isReadyToFinalize = false; // check if meeting can be finalized by viewer // check if overall meeting state can be sent by owner if (!$this->canSend($this->owner_id)) return false; $chosenPlace = false; if (count($this->meetingPlaces)==1) { $chosenPlace = true; } else { foreach ($this->meetingPlaces as $mp) { if ($mp->status == MeetingPlace::STATUS_SELECTED) { $chosenPlace = true; break; } } } $chosenTime = false; if (count($this->meetingTimes)==1) { $chosenTime = true; } else { foreach ($this->meetingTimes as $mt) { if ($mt->status == MeetingTime::STATUS_SELECTED) { $chosenTime = true; break; } } } if ($this->owner_id == $user_id || $this->meetingSettings->participant_finalize) { if ($chosenPlace && $chosenTime) { $this->isReadyToFinalize = true; } } return $this->isReadyToFinalize; }
This first checks if the meeting could be sent, because if not, it can't be finalized. Then, it checks to make sure that both a place and time have been chosen. And then, it checks if the viewer is the organizer or the meeting settings allow a participant to finalize the meeting.
Basically, as changes are made, you'll see the state of the Send and Finalize buttons change:
In the meeting view.php, I've embedded JavaScript to support AJAX updates to the state of the Send and Finalize buttons as users change settings for their meeting. When selections of places and times are made, refreshSend()
and refreshFinalize()
are called and the buttons are appropriately modified:
<?php if (isset(Yii::$app->params['urlPrefix'])) { $urlPrefix = Yii::$app->params['urlPrefix']; } else { $urlPrefix =''; } $script = <<< JS function refreshSend() { $.ajax({ url: '$urlPrefix/meeting/cansend', data: {id: $model->id, 'viewer_id': $viewer}, success: function(data) { if (data) $('#actionSend').removeClass("disabled"); else $('#actionSend').addClass("disabled"); return true; } }); } function refreshFinalize() { $.ajax({ url: '$urlPrefix/meeting/canfinalize', data: {id: $model->id, 'viewer_id': $viewer}, success: function(data) { if (data) $('#actionFinalize').removeClass("disabled"); else $('#actionFinalize').addClass("disabled"); return true; } }); } JS; $position = \yii\web\View::POS_READY; $this->registerJs($script, $position); ?>
Reversing the Place and Time Status Selectors
In the current user interface, we show the viewer's place and time selections in the leftmost or first column. The code has to be customized to reverse this when participants are viewing:
To support showing different data in the You and Them columns of the meeting view for Times and Places, the meeting-time and meeting-place _list.php files needed to be updated to dynamically determine what data to display:
<td style> <?php if ($isOwner) { showTimeOwnerStatus($model,$isOwner); } else { showTimeParticipantStatus($model,$isOwner); } ?> </td> <td style> <?php if (!$isOwner) { showTimeOwnerStatus($model,$isOwner); } else { showTimeParticipantStatus($model,$isOwner); } ?> </td>
For now, I placed these functions within the _panel.php view, which calls _list.php, as they rely on having the SwitchInput widget included in context:
<?php use \kartik\switchinput\SwitchInput; function showTimeOwnerStatus($model,$isOwner) { foreach ($model->meetingTimeChoices as $mtc) { if ($mtc->user_id == $model->meeting->owner_id) { if ($mtc->status == $mtc::STATUS_YES) $value = 1; else $value =0; echo SwitchInput::widget([ 'type' => SwitchInput::CHECKBOX, 'name' => 'meeting-time-choice', 'id'=>'mtc-'.$mtc->id, 'value' => $value, 'disabled' => !$isOwner, 'pluginOptions' => ['size' => 'mini','onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>','onColor' => 'success','offColor' => 'danger',], ]); } } } function showTimeParticipantStatus($model,$isOwner) { foreach ($model->meetingTimeChoices as $mtc) { if (count($model->meeting->participants)==0) break; if ($mtc->user_id == $model->meeting->participants[0]->participant_id) { if ($mtc->status == $mtc::STATUS_YES) $value = 1; else if ($mtc->status == $mtc::STATUS_NO) $value =0; else if ($mtc->status == $mtc::STATUS_UNKNOWN) $value =-1; echo SwitchInput::widget([ 'type' => SwitchInput::CHECKBOX, 'name' => 'meeting-time-choice', 'id'=>'mtc-'.$mtc->id, 'tristate'=>true, 'indeterminateValue'=>-1, 'indeterminateToggle'=>false, 'disabled'=>$isOwner, 'value' => $value, 'pluginOptions' => ['size' => 'mini','onText' => '<i class="glyphicon glyphicon-ok"></i>','offText'=>'<i class="glyphicon glyphicon-remove"></i>','onColor' => 'success','offColor' => 'danger',], ]); } } } ?>
Upcoming Adjustments
Ultimately, there are a lot of improvements to make to this code going forward. In places, I'm making AJAX calls to the server two or three times when I could code these more efficiently into a single request. In other places, I can do more locally with JavaScript. And the user interface will need to keep improving, and the code will need to change to adapt to that. But, from a functional perspective, today's work represents a lot of overall progress towards the MVP.
What's Next?
With the meeting settings and view requirements in place for organizers and participants, I'm ready to move on to sending the first invitation. The next episode will explore emailing the invitation to the participant and implementing the appearance of content, functional command links within the email, and managing the permissions for users that haven't registered yet. Watch for upcoming tutorials in our Building Your Startup With PHP series—this is getting exciting!
Please feel free to add your questions and comments below; I generally participate in the discussions. You can also reach me on Twitter @reifman.
Comments