Статьи

Создание вашего стартапа: уведомление людей об обновлениях собраний

Конечный продукт
Что вы будете создавать

Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения

В этой серии из двух частей я опишу, как мы создали инфраструктуру уведомлений и их доставку. Сегодня я собираюсь сосредоточиться на MeetingLog, чтобы отслеживать изменения, которые помогают нам определить, когда отправлять обновления.

Если вы еще не опробовали Планировщик собраний, запланируйте свою первую встречу . Я принимаю участие в комментариях ниже, так что скажите мне, что вы думаете! Мне особенно интересно, если вы хотите предложить новые функции или темы для будущих уроков.

Напоминаем, что весь код для Meeting Planner написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с нашей параллельной серией Программирование с Yii2 .

Вот что обычно видит участник встречи, когда получает приглашение. Они начнут делиться своей доступностью по местам и времени и иногда выбирают окончательный выбор:

Уведомления организатора собрания - Форма планирования собрания

Когда участник встречи отвечает на приглашение, подобное приведенному выше, он может внести несколько изменений. Например:

  • Эта дата и время работают, а эти два — нет.
  • Это место работает, это не так, давайте также рассмотрим это дополнительное.
  • Я выбираю это место и на этот раз.
  • Добавьте примечание: «Я рад, что мы наконец собрались вместе».

После внесения изменений нам необходимо сообщить об этом организатору.

Наша цель состоит в том, чтобы упростить планирование собраний и сократить количество используемых электронных писем. Если мы подождем несколько минут после того, как человек внесет изменения, и объединим их в одном обновлении, то, вероятно, самое время доставить уведомление. Но как нам создать код, который это делает? Как мы отслеживаем, какие изменения мы сделали, и разъясняем это для организатора?

У меня была еще одна идея — избавиться от необходимости нажимать кнопку «Отправить» после внесения изменений. Во-первых, это потребует некоторого обучения пользователей, чтобы гарантировать, что мы отправим их изменения, когда они будут готовы. Но нам также нужен был способ узнать, когда можно передать изменения организатору.

Повторяя и обобщая требования, вот как будут работать уведомления:

Все действия, связанные с собраниями, хранятся в нашей модели MeetingLog. Но нам нужен способ создать текстовое резюме журнала, чтобы поделиться с участниками. Мы можем просто показать изменения, произошедшие с момента последнего обновления. Так что мы должны это отследить.

Когда пользователь вносит изменения в собрание, мы наглядно сообщим, что нет необходимости нажимать на несуществующую кнопку отправки или сохранения изменений.

Мы хотим отслеживать, когда изменения устаревают несколько минут, и объединять их в одно обновление для другого участника или организатора.

И, наконец, нам нужно будет доставить обновление по электронной почте только другому участнику, а не тому, кто внес изменения. Или, если оба участника внесли изменения в это время, мы должны будем уведомить каждого только о действиях другого.

С самого начала я хотел получить полный журнал действий, выполняемых при планировании встреч по ряду причин. Возможно, люди захотят увидеть историю планирования или вернуться к определенному моменту, но это также полезно для отладки. Также выяснилось, что необходимо знать, когда доставлять обновления об изменениях собрания.

Давайте рассмотрим структуру позади модели MeetingLog. Вот миграция, создающая таблицу:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class m141025_220133_create_meeting_log_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_log}}’, [
          ‘id’ => Schema::TYPE_PK,
          ‘meeting_id’ => Schema::TYPE_INTEGER.’
          ‘action’ => Schema::TYPE_INTEGER.’
          ‘actor_id’ => Schema::TYPE_BIGINT.’
          ‘item_id’ => Schema::TYPE_INTEGER.’
          ‘extra_id’ => Schema::TYPE_INTEGER.’
          ‘created_at’ => Schema::TYPE_INTEGER .
          ‘updated_at’ => Schema::TYPE_INTEGER .
      ], $tableOptions);
      $this->addForeignKey(‘fk_meeting_log_meeting’, ‘{{%meeting_log}}’, ‘meeting_id’, ‘{{%meeting}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);
      $this->addForeignKey(‘fk_meeting_log_actor’, ‘{{%meeting_log}}’, ‘actor_id’, ‘{{%user}}’, ‘id’, ‘CASCADE’, ‘CASCADE’);
 
  }

Основные элементы, которые мы записываем:

  • meeting_id сообщает нам, какую встречу мы отслеживаем.
  • action говорит нам, что было сделано.
  • actor_id сообщает нам, кто выполнил действие (например, user_id ).
  • Перегруженный item_id может представлять время, место заметки.
  • extra_id предназначен для записи другой информации в зависимости от действия.
  • created_at сообщает нам, когда действие было выполнено.

Затем я определил константы для всех действий, таких как ACTION_SUGGEST_PLACE или ACTION_ADD_NOTE и т. Д.

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
class MeetingLog extends \yii\db\ActiveRecord
{
    const ACTION_CREATE_MEETING = 0;
    const ACTION_EDIT_MEETING = 3;
    const ACTION_CANCEL_MEETING = 7;
    const ACTION_DELETE_MEETING = 8;
    const ACTION_DECLINE_MEETING = 9;
    const ACTION_SUGGEST_PLACE = 10;
    const ACTION_ACCEPT_ALL_PLACES = 11;
    const ACTION_ACCEPT_PLACE = 12;
    const ACTION_REJECT_PLACE = 15;
    const ACTION_SUGGEST_TIME = 20;
    const ACTION_ACCEPT_ALL_TIMES = 21;
    const ACTION_ACCEPT_TIME = 22;
    const ACTION_REJECT_TIME = 25;
    const ACTION_INVITE_PARTICIPANT = 30;
    const ACTION_ADD_NOTE = 40;
    const ACTION_SEND_INVITE = 50;
    const ACTION_FINALIZE_INVITE = 60;
    const ACTION_COMPLETE_MEETING = 100;
    const ACTION_CHOOSE_PLACE = 110;
    const ACTION_CHOOSE_TIME = 120;
    const ACTION_SENT_CONTACT_REQUEST = 150;
 
    const TIMELAPSE = 300;

Метод add позволяет функциональным возможностям во всем приложении регистрировать активность в MeetingLog:

01
02
03
04
05
06
07
08
09
10
11
12
// add to log
   public static function add($meeting_id,$action,$actor_id=0,$item_id=0,$extra_id=0) {
        $log = new MeetingLog;
        $log->meeting_id=$meeting_id;
        $log->action =$action;
        $log->actor_id =$actor_id;
        $log->item_id =$item_id;
        $log->extra_id =$extra_id;
        $log->save();
        // sets the touched_at field for the Meeting
        Meeting::touchLog($meeting_id);
   }

Я также добавил два новых поля в таблицу Meeting: logged_at и cleared_at . Каждый раз, когда добавляются записи журнала, собрание обновляет logged_at времени logged_at , показывая момент последнего изменения собрания:

1
2
3
4
5
public static function touchLog($id) {
        $mtg = Meeting::findOne($id);
        $mtg->logged_at = time();
         $mtg->update();
      }

Например, всякий раз, когда кто-то добавляет новый параметр MeetingPlace, в журнал добавляется событие afterSave , а затем, конечно, также обновляется Meeting->logged_at :

01
02
03
04
05
06
07
08
09
10
11
public function afterSave($insert,$changedAttributes)
   {
       parent::afterSave($insert,$changedAttributes);
       if ($insert) {
         // if MeetingPlace is added
         // add MeetingPlaceChoice for owner and participants
         $mpc = new MeetingPlaceChoice;
         $mpc->addForNewMeetingPlace($this->meeting_id,$this->suggested_by,$this->id);
         MeetingLog::add($this->meeting_id,MeetingLog::ACTION_SUGGEST_PLACE,$this->suggested_by,$this->place_id);
       }
   }

Время logged_at сообщает нам, когда произошло последнее изменение. Время Meeting->cleared_at сообщит нам время, когда мы последний раз делились обновлениями с участниками. Итак, если logged_at > cleared_at , мы знаем, что участники не полностью обновлены.

Уведомления Планировщика Встреч - История Планирования MeetingLog

Затем я создал методы, помогающие перевести журнал в историю изменений на английском языке для собрания.

Сначала я создал getMeetingLogCommand (), чтобы получить текстовое описание действия:

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
59
60
61
62
63
64
65
66
67
68
69
70
public function getMeetingLogCommand() {
  switch ($this->action) {
    case MeetingLog::ACTION_CREATE_MEETING:
      $label = Yii::t(‘frontend’,’create meeting’);
    break;
    case MeetingLog::ACTION_EDIT_MEETING:
      $label = Yii::t(‘frontend’,’edit meeting’);
    break;
    case MeetingLog::ACTION_CANCEL_MEETING:
      $label = Yii::t(‘frontend’,’cancel meeting’);
    break;
    case MeetingLog::ACTION_DELETE_MEETING:
      $label = Yii::t(‘frontend’,’cancel meeting’);
    break;
    case MeetingLog::ACTION_DELETE_MEETING:
      $label = Yii::t(‘frontend’,’deleted meeting’);
    break;
    case MeetingLog::ACTION_SUGGEST_PLACE:
    $label = Yii::t(‘frontend’,’add place’);
    break;
    case MeetingLog::ACTION_SUGGEST_TIME:
    $label = Yii::t(‘frontend’,’add time’);
    break;
    case MeetingLog::ACTION_ADD_NOTE:
    $label = Yii::t(‘frontend’,’add note’);
    break;
    case MeetingLog::ACTION_INVITE_PARTICIPANT:
    $label = Yii::t(‘frontend’,’Invite participant’);
    break;
    case MeetingLog::ACTION_ACCEPT_ALL_PLACES:
      $label = Yii::t(‘frontend’,’accept all places’);
    break;
    case MeetingLog::ACTION_ACCEPT_PLACE:
      $label = Yii::t(‘frontend’,’accept place’);
    break;
    case MeetingLog::ACTION_REJECT_PLACE:
      $label = Yii::t(‘frontend’,’reject place’);
    break;
    case MeetingLog::ACTION_ACCEPT_ALL_TIMES:
      $label = Yii::t(‘frontend’,’accept all times’);
    break;
    case MeetingLog::ACTION_ACCEPT_TIME:
      $label = Yii::t(‘frontend’,’accept time’);
    break;
    case MeetingLog::ACTION_REJECT_TIME:
      $label = Yii::t(‘frontend’,’reject time’);
    break;
    case MeetingLog::ACTION_CHOOSE_PLACE:
      $label = Yii::t(‘frontend’,’choose place’);
    break;
    case MeetingLog::ACTION_CHOOSE_TIME:
      $label = Yii::t(‘frontend’,’choose time’);
    break;
    case MeetingLog::ACTION_SEND_INVITE:
    $label = Yii::t(‘frontend’,’Send’);
    break;
    case MeetingLog::ACTION_FINALIZE_INVITE:
    $label = Yii::t(‘frontend’,’Finalize’);
    break;
    case MeetingLog::ACTION_COMPLETE_MEETING:
    $label = Yii::t(‘frontend’,’Complete meeting’);
    break;
    case MeetingLog::ACTION_SENT_CONTACT_REQUEST:
    $label = Yii::t(‘frontend’,’Send request for contact information’);
    default:
      $label = Yii::t(‘frontend’,’Unknown’);
    break;
  }
  return $label;
}

Затем я создал getMeetingLogItem () , который контекстуально находит соответствующую метку объекта на основе того, какое действие было выполнено. Здесь все еще есть некоторые заметки отладки для меня:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public function getMeetingLogItem() {
  $label=»;
  switch ($this->action) {
    case MeetingLog::ACTION_CREATE_MEETING:
    case MeetingLog::ACTION_EDIT_MEETING:
    case MeetingLog::ACTION_CANCEL_MEETING:
    case MeetingLog::ACTION_DECLINE_MEETING:
      $label = Yii::t(‘frontend’,’-‘);
    break;
    case MeetingLog::ACTION_INVITE_PARTICIPANT:
      $label = MiscHelpers::getDisplayName($this->item_id);
      if (is_null($label)) {
        $label = ‘Error — unknown user’;
      }
    break;
    case MeetingLog::ACTION_SUGGEST_PLACE:
    $label = Place::find()->where([‘id’=>$this->item_id])->one();
    if (is_null($label)) {
      $label = ‘Error — suggested unknown place’;
    } else {
      $label = $label->name;
      if (is_null($label)) {
        $label = ‘Error — suggested place has unknown name’;
      }
    }
    break;
    case MeetingLog::ACTION_ACCEPT_PLACE:
    case MeetingLog::ACTION_REJECT_PLACE:
    $label = MeetingPlace::find()->where([‘id’=>$this->item_id])->one();
    if (is_null($label)) {
      $label = ‘Error — Accept or reject unknown place x1’;
    } else {
      if (is_null($label->place))
        $label = ‘Error Accept or reject unknown place x2’;
      else {
        $label = $label->place->name;
        if (is_null($label)) {
          $label = ‘Error accept or reject unknown place name x3’;
        }
      }
    }
    break;
    case MeetingLog::ACTION_CHOOSE_PLACE:
    $label = MeetingPlace::find()->where([‘id’=>$this->item_id])->one();
    if (is_null($label)) {
      $label = ‘Error — chose unknown place x1’;
    } else {
      if (is_null($label->place))
        $label = ‘Error chose unknown place x2’;
      else {
        $label = $label->place->name;
        if (is_null($label)) {
          $label = ‘Error — choose unknown place name x3’;
        }
      }
    }
    break;
    case MeetingLog::ACTION_CHOOSE_TIME:
    case MeetingLog::ACTION_SUGGEST_TIME:
    case MeetingLog::ACTION_ACCEPT_TIME:
    case MeetingLog::ACTION_REJECT_TIME:
      // get the start time
      $mt = MeetingTime::find()->where([‘id’=>$this->item_id])->one();
      if (is_null($mt)) {
        $label = ‘Error meeting time unknown’;
      } else {
        $label = Meeting::friendlyDateFromTimestamp($mt->start);
      }
    break;
    case MeetingLog::ACTION_ADD_NOTE:
      if ($this->item_id ==0) {
        $label = ‘note not logged’;
      } else {
        $label = MeetingNote::find()->where([‘id’=>$this->item_id])->one()->note;
      }
    break;
    case MeetingLog::ACTION_ACCEPT_ALL_PLACES:
    case MeetingLog::ACTION_ACCEPT_ALL_TIMES:
    case MeetingLog::ACTION_SEND_INVITE:
    case MeetingLog::ACTION_FINALIZE_INVITE:
    case MeetingLog::ACTION_COMPLETE_MEETING:
    case MeetingLog::ACTION_SENT_CONTACT_REQUEST:
      $label = Yii::t(‘frontend’,’-‘);
    break;
    default:
      $label = Yii::t(‘frontend’,’n/a’);
    break;
  }
  return $label;
}

Например, для создания, редактирования, отмены, удаления собрания не требуется информация об элементе, в то время как для времени принятия требуется определенный item_id соответствующий дате и времени MeetingTimeChoice .

Когда участник меняет ползунки на места и время, чтобы указать свои предпочтения, или, если это разрешено, делает окончательный выбор, я хотел указать для них, что их организатор собрания будет уведомлен, когда они закончат. Я также хотел, чтобы это было ненавязчивым, например всплывающее предупреждение.

Поскольку многие из этих изменений связаны с AJAX, сообщение должно появиться рядом с действиями мыши. В случае отправки изменения (например, новой заметки) уведомление может появиться в верхней части страницы.

Замечания по Yii Flash отличные, но они появляются только вверху страниц. Например, в настоящее время добавление заметки требует обновления страницы. Люди увидят подсказку в верхней части страницы после публикации своей заметки:

Уведомления планировщика собраний - обновленное Flash-уведомление в верхней части страницы

Итак, пока наш дизайн остается в движении, я только что создал скрытые флэш-элементы уведомлений над областями, показывающими места, и над областями, показывающими время. Всякий раз, когда люди вносят изменения, они видят уведомление рядом с действиями мыши

Уведомления о планировщике собраний - начало раздела AJAX Flash-уведомления

В будущем мы не будем показывать эти уведомления опытным пользователям, поскольку они уже знают об этом.

Итак, если человек вносит изменения во время или место, мы вызываем displayNotifier () :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// users can say if a time is an option for them
$(‘input[name=»meeting-time-choice»]’).on(‘switchChange.bootstrapSwitch’, function(e, s) {
  // set intval to pass via AJAX from boolean state
  if (s)
    state = 1;
  else
    state =0;
  $.ajax({
     url: ‘$urlPrefix/meeting-time-choice/set’,
     data: {id: e.target.id, ‘state’: state},
     success: function(data) {
       displayNotifier(‘time’);
       refreshSend();
       refreshFinalize();
       return true;
     }
  });
});
JS;

Этот код в /frontend/views/meeting/view.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
$session = Yii::$app->session;
if ($session[‘displayHint’]==’on’ || $model->status == $model::STATUS_PLANNING ) {
  $notifierOkay=’off’;
  $session->remove(‘displayHint’);
} else {
  $notifierOkay=’on’;
}
 
?>
<input id=»notifierOkay» value=»<?= $notifierOkay ?>» type=»hidden»>
<?php
$script = <<< JS
var notifierOkay;
 
if ($(‘#notifierOkay’).val() == ‘on’) {
  notifierOkay = true;
} else {
  notifierOkay = false;
}
 
function displayNotifier(mode) {
  if (notifierOkay) {
    if (mode == ‘time’) {
      $(‘#notifierTime’).show();
    } else if (mode == ‘place’) {
       $(‘#notifierPlace’).show();
    } else {
      alert(«We\’ll automatically notify the organizer when you’re done making changes.»);
    }
    notifierOkay=false;
  }
}
Уведомления планировщика собраний - шаблон электронного письма с начальным уведомлением

Я описал видение, которое я пытаюсь создать с помощью планирования действий между участниками. В следующем уроке я покажу вам, как мы отслеживаем время, чтобы знать, когда и как доставлять обновления. И я покажу вам, как мы доставляем обновления по электронной почте, как показано выше. Я также собираюсь показать вам, как мы создаем текстовое резюме недавних изменений, которые были сделаны.

Пока вы ждете, опробуйте функцию уведомлений и запланируйте свою первую встречу . Кроме того, я буду признателен, если вы поделитесь своим опытом ниже в комментариях, и мне всегда интересны ваши предложения. Вы также можете связаться со мной через Twitter @reifman напрямую.

Следите за будущими уроками в серии « Построение стартапа с помощью PHP» . Есть еще несколько важных функций.