[JS] 予約管理システム改

タグ :

これまでの基礎知識を結集して簡易アプリを作成するシリーズです。
今回は前記事に引き続き、お店等で役立つ Web 上での予約管理システムを改善します。

前記事では CakePHP のみで作成した予約管理システムでしたが、今回は jQuery(Ajax) を使って色々と動的処理を施しました。右側にあるオーダーボックスをドラッグ&ドロップで予約することができます。さらにページ遷移無しで動的に予約ができます。前回同様にログインが必要ですが、ユーザー登録も予約も勝手にしちゃって大丈夫です。自由に遊んでくださいませ。

上記では操作しにくいという方は、こちら でお試しください。

アプリ概要

以下のような仕様とします。

ドラッグ&ドロップで予約
 - オーダーをドラッグできる
 - 席領域の指定の時間帯にドロップできる
 - 上記操作で予約処理を実行する
 - 席の指定もできる
ページ遷移させない
 - 予約してもページ遷移させない
 - 予約したデータを予約後に表示する

必要となる処理は以下になります。

  • ドラッグ&ドロップ
  • 座標位置の取得
  • POST送信(Ajax)
  • ページ切り換え

ページ遷移は以下になります。

js_appo_page

作成したサンプル

処理の流れとコードは以下になります。

[ 動作フロー ]
js_appo_flow
[ コード ]
$(function() {
	var flg = true;
	$('.order1,.order2,.order3,.order4').draggable({
		revert: true,
		helper: 'clone',
		drag: function(e, ui) {
			$(this).addClass('dragout');
			$('.timeline li').removeClass('hover');
			var y = ui.position.top;
			var tag = null;
			if (y <= 30) { tag = '.time' + 9;
			} else if (y > 30 && y <= 70) { tag = '.time' + 10;
			} else if (y > 70 && y <= 110) { tag = '.time' + 11;
			} else if (y > 110 && y <= 150) { tag = '.time' + 12;
			} else if (y > 150 && y <= 190) { tag = '.time' + 13;
			} else if (y > 190 && y <= 230) { tag = '.time' + 14;
			} else if (y > 230 && y <= 270) { tag = '.time' + 15;
			} else if (y > 270 && y <= 310) { tag = '.time' + 16;
			} else if (y > 310 && y <= 350) { tag = '.time' + 17;
			} else if (y > 350 && y <= 390) { tag = '.time' + 18;
			}
			if (tag) {
				$(tag).addClass('hover');
			}
		},
		stop: function() {
			$(this).removeClass('dragout');
			$('.timeline li').removeClass('hover');
			flg = true;
		}
	});
	$('.appo-area1,.appo-area2,.appo-area3').droppable({
		accept: '.order1,.order2,.order3,.order4',
		tolerance: 'intersect',
		hoverClass: 'hover',
		drop: function(e, ui) {
			ui.draggable.removeClass('dragout');
			boxDropping(ui, $(this));
			flg = false;
		},
		deactivate: function(e, ui) {
			ui.draggable.draggable({ revert: flg });
		}
	});
	function boxDropping(ui, obj) {
		var data;
		var order = ui.draggable.attr('class').split(" ");
		var link = $('#link').attr('value');
		var table = obj.attr('class').split(" ");
		data = {
			user_id: $('#user_id').attr('value'),
			order_id: order[0].slice(5),
			date: $('#date').attr('value'),
			start: $('.timeline li.hover').text(),
			table: table[0].slice(9)
		};
		$.ajax({
			type: 'POST',
			url: '/sample/cakejs/appo_js/appointments/index/' + link,
			data: data,
			success: function(data, dataType) {
				$('body').html(data);
			}
		});
	}
});
<div class="appointments index">
	<h2><?php echo __('Appointments'); ?></h2>
	<?php echo $this->Session->flash(); ?>
	<div class="paging">
	<?php
		echo $this->Html->link('< '.__('prev day'), array('action' => 'index/'.$prev));
		echo $this->Html->link(__('next day').' >', array('action' => 'index/'.$next));
		echo $this->Form->hidden('user_id', array(
				'value' => $user_id
		));
		echo $this->Form->hidden('date', array(
				'value' => $date
		));
		echo $this->Form->hidden('link', array(
				'value' => $link
		));
	?>
	</div>
	<h4><?php echo $strdate . __(' appointments'); ?></h4>
	
	<ul class="timeline">
		<li class="time9">09:00</li>
		<li class="time10">10:00</li>
		<li class="time11">11:00</li>
		<li class="time12">12:00</li>
		<li class="time13">13:00</li>
		<li class="time14">14:00</li>
		<li class="time15">15:00</li>
		<li class="time16">16:00</li>
		<li class="time17">17:00</li>
		<li class="time18">18:00</li>
		<li class="time19">19:00</li>
	</ul>
	<p class="appo-area-p1">1番席</p>
	<div class="appo-area1">
	<?php foreach ($appointments as $appointment): ?>
		<?php if ($appointment['Appointment']['table'] == 1): ?>
			<div class="<?php echo $appointment['Appointment']['class']; ?>" style="height:<?php echo $appointment['Appointment']['height']; ?>px">
				<p><?php echo $appointment['Appointment']['name'] ?></p>
			</div>
		<?php endif; ?>
	<?php endforeach; ?>
	</div>
	<p class="appo-area-p2">2番席</p>
	<div class="appo-area2">
	<?php foreach ($appointments as $appointment): ?>
		<?php if ($appointment['Appointment']['table'] == 2): ?>
			<div class="<?php echo $appointment['Appointment']['class']; ?>" style="height:<?php echo $appointment['Appointment']['height']; ?>px">
				<p><?php echo $appointment['Appointment']['name'] ?></p>
			</div>
		<?php endif; ?>
	<?php endforeach; ?>
	</div>
	<p class="appo-area-p3">3番席</p>
	<div class="appo-area3">
	<?php foreach ($appointments as $appointment): ?>
		<?php if ($appointment['Appointment']['table'] == 3): ?>
			<div class="<?php echo $appointment['Appointment']['class']; ?>" style="height:<?php echo $appointment['Appointment']['height']; ?>px">
				<p><?php echo $appointment['Appointment']['name'] ?></p>
			</div>
		<?php endif; ?>
	<?php endforeach; ?>
	</div>
	<p class="dragarea-p">オーダー</p>
	<div class="dragarea">
		<div class="order1"><p>カット</p></div>
		<div class="order2"><p>カラー<br>リング</p></div>
		<div class="order3"><p>パーマ</p></div>
		<div class="order4"><p>その他</p></div>
	</div>
	
</div>

<div class="actions">
	<h3><?php echo __('Actions'); ?></h3>
	<ul>
		<li><?php echo $this->Html->link(__($str), array('controller' => 'users', 'action' => $page)); ?> </li>
		<li><?php echo $this->Html->link(__('Logout'), array('controller' => 'users', 'action' => 'logout')); ?> </li>
	</ul>
</div>
.dragarea-p {
	position: absolute;
	margin-top: -470px; margin-left: 370px;
}
.dragarea {
	position: relative;
	margin-top: -450px; margin-left: 360px;
	width: 80px; height: 450px;
	background: #aaa;
}
.order1, .order2, .order3, .order4 {
	position: absolute;
	margin-left: 10px;
	width: 60px;
	text-align: center; background: blue; cursor: move;
}
.order1 {
	top: 50px; height: 20px;
}
.order2 {
	top: 120px; height: 40px;
}
.order3 {
	top: 210px; height: 60px;
}
.order4 {
	top: 320px; height: 80px;
}
.hover {
	background: red;
}
ul.timeline li.hover {
	background: red;
}
.dragout {
	opacity: 0.4;
}
詳しい説明は後述します。
過去記事「ドラッグ&ドロップ」をコピーして作成したものですので、基礎的な部分は過去記事を参照してください。

コード解説

jQuery のコードの説明をします。
ドラッグ&ドロップの部分は過去記事「ドラッグ&ドロップ」を参照してください。ここでは新たに追加した処理についてのみ説明します。

[ jQuery の説明 ]
[ 3行目〜31行目 ボックスのドラッグ設定 ]
var y = ui.position.top;
var tag = null;
if (y <= 30) { tag = '.time' + 9;
} else if (y > 30 && y <= 70) { tag = '.time' + 10;
} else if (y > 70 && y <= 110) { tag = '.time' + 11;
} else if (y > 110 && y <= 150) { tag = '.time' + 12;
} else if (y > 150 && y <= 190) { tag = '.time' + 13;
} else if (y > 190 && y <= 230) { tag = '.time' + 14;
} else if (y > 230 && y <= 270) { tag = '.time' + 15;
} else if (y > 270 && y <= 310) { tag = '.time' + 16;
} else if (y > 310 && y <= 350) { tag = '.time' + 17;
} else if (y > 350 && y <= 390) { tag = '.time' + 18;
}
if (tag) {
	$(tag).addClass('hover');
}

予約する際に時間帯を指定しますが、ドロップ領域のY軸座標により時間帯が選択できるようにするための処理です。
ドラッグ要素の座標位置を取得して、その位置に応じて時間帯要素部分のクラス名を変更します。座標位置が時間帯要素の Y 軸上に移動した場合、対応する時間帯要素のクラス名に「hover」というクラスを追加します。このクラスは CSS により背景色が赤になるように設定しておきます。

上記以外は過去記事「ドラッグ&ドロップ」と同様の設定なので割愛します。

[ 32行目〜44行目 ドロップ領域の設定 ]
$('.appo-area1,.appo-area2,.appo-area3').droppable({

ドロップ領域を「1番席」「2番席」「3番席」の 3 つともに設定します。

accept: '.order1,.order2,.order3,.order4',
tolerance: 'intersect',
hoverClass: 'hover',

ドロップの設定です。
ドロップ許可要素にオーダーボックス(1〜4)を設定します。
ドロップの可能位置を ‘intersect’ とし、要素の半分が領域内に入った場合にドロップ可能状態になるようにします。
ドロップ可能状態になった場合にクラス名「hover」を追加します。

上記以外は過去記事「ドラッグ&ドロップ」と同様の設定なので割愛します。

[ 45行目〜65行目 ドロップ時の処理 ]
var data;
var order = ui.draggable.attr('class').split(" ");
var link = $('#link').attr('value');
var table = obj.attr('class').split(" ");
data = {
	user_id: $('#user_id').attr('value'),
	order_id: order[0].slice(5),
	date: $('#date').attr('value'),
	start: $('.timeline li.hover').text(),
	table: table[0].slice(9)
};

送信するデータを取得します。
それぞれのデータは、HTML 要素内の value値、class名、text値から取得してきます。

$.ajax({
	type: 'POST',
	url: '/sample/cakejs/appo_js/appointments/index/' + link,
	data: data,
	success: function(data, dataType) {
		$('body').html(data);
	}
});

Ajax による POST送信する処理です。
url には予約一覧ページを指定することでページ遷移を無くすことができます。
data には上記で取得したデータを設定します。
success は POST送信が成功した時の処理を記述します。コールバック関数の引数の data には、POST送信後のページの HTML データが格納されています。そのデータを利用して、現在のページの body 部分を切り換えることで、予約したデータを含んだ最新の予約一覧が表示されます。

CakePHP コードの変更点

jQuery を用いたアプリに改変したことで、CakePHP のコントローラにも変更を加えています。その変更点だけをまとめます。また、ビューの変更としては、上記コードの HTML タブ内のコードですので割愛します。

[ コード ]
public function index($date_id = 0) {
	// 指定した日付データを取得
	if ($date_id) {
		$strdate = date('Y年m月d日', strtotime($date_id));
		$date = date('Y-m-d', strtotime($date_id));
		$link = date('Ymd', strtotime($date_id));
	} else {
		$strdate = date('Y年m月d日');
		$date = date('Y-m-d');
		$link = date('Ymd');
	}
	// 時間帯取得
	$times = $this->Time->find('list', array(
		'fields' => 'time'
	));
	$i = 1;
	foreach ($times as $time) {
		$times[$i] = substr($time, 0, 5);
		$i++;
	}
	// オーダー取得
	$orders = $this->Order->find('list', array(
		'fields' => 'order'
	));
	// ログイン状態チェック
	$user = $this->Auth->user();
	if(empty($user)){
		$this->set('str', 'Login');
		$this->set('page', 'login');
	}else{
		$this->set('str', 'MyPage');
		$this->set('page', 'view/'.$user['id']);
	}
	// 追加処理
	if ($this->request->is('post')) {
		$data['Appointment']['user_id'] = $_POST['user_id'];
		$data['Appointment']['order_id'] = $_POST['order_id'];
		$data['Appointment']['date'] = $_POST['date'];
		$data['Appointment']['start'] = $_POST['start'];
		$data['Appointment']['table'] = $_POST['table'];
		// 予約済データ取得
		$appo = $this->Appointment->find('all', array(
			'conditions' => array('date' => $data['Appointment']['date']),
			'order' => array('start' => 'ASC')
		));
		// 予約済データの比較 被りがあるなら席を変更
		$flgs = array(null, true, true, true);
		foreach ($appo as $ap) {
			if (substr($ap['Appointment']['start'],0,5) == $data['Appointment']['start']
				|| ($ap['Appointment']['order_id'] >= 3 && (int)substr($ap['Appointment']['start'],0,2) == substr($data['Appointment']['start'],0,2)-1)
				|| ($data['Appointment']['order_id'] >= 3 && (int)substr($ap['Appointment']['start'],0,2)-1 == substr($data['Appointment']['start'],0,2))) {
				$flgs[$ap['Appointment']['table']] = false;
				if (!$flgs[$data['Appointment']['table']]) {
					$this->Session->setFlash(__('The appointment could not be saved.'));
					$this->redirect(array('action' => 'index/'.$link));
				}
			}
		}
		$this->Appointment->create();
		if ($this->Appointment->save($data)) {
			$this->Session->setFlash(__('The appointment has been saved'));
		} else {
			$this->Session->setFlash(__('The appointment could not be saved. Please, try again.'));
		}
	}
	// 予約データ取得
	$appo = $this->Appointment->find('all', array(
		'conditions' => array('date' => $date),
		'order' => array('start' => 'ASC')
	));
	// タイムライン追加用クラス名生成
	for ($i = 0; $i < count($appo); $i++) {
		$appo[$i]['Appointment']['class'] = 'appo' . substr($appo[$i]['Appointment']['start'], 0, 2);
		$appo[$i]['Appointment']['height'] = $appo[$i]['Appointment']['order_id'] * 20;
		if ($appo[$i]['Appointment']['user_id'] == $user['id']) {
			$appo[$i]['Appointment']['class'] .= ' me';
			$appo[$i]['Appointment']['name'] = $appo[$i]['User']['name'];
		} else {
			$appo[$i]['Appointment']['name'] = 'Already';
		}
	}
	// データ渡し
	$this->set('appointments', $appo);
	$this->set('strdate', $strdate);
	$this->set('orders', $orders);
	$this->set('link', $link);
	$this->set('prev', date('Ymd', strtotime($date . ' -1 day')));
	$this->set('next', date('Ymd', strtotime($date . ' +1 day')));
	$this->set('user_id', $user['id']);
	$this->set('user_name', $user['name']);
	$this->set('date', $date);
	$this->set('times', $times);
	$this->set('orders', $orders);
}
簡単に言うと、index 関数内に add 関数を追加しただけです。add の予約追加処理には多少の変更があります。変更した部分だけを以下にまとめます。
[ 34行目〜71行目 追加処理 ]
$data['Appointment']['table'] = $_POST['table'];

POST送信により取得した席番号を登録用変数配列に格納します。改変前ではここの値は「1」でした。

$flgs[$ap['Appointment']['table']] = false;
if (!$flgs[$data['Appointment']['table']]) {
	$this->Session->setFlash(__('The appointment could not be saved.'));
	$this->redirect(array('action' => 'index/'.$link));
}

上記で格納した席番号が空いていなければ予約しない、という処理です。
上記処理と、この処理により、ドロップした席に予約することができるようになります。つまり、時間帯だけでなく席も選べるようになります。

まとめ

前回のアプリと比較してもかなり進化して扱い易いのではないかと思います。何よりページ遷移しなくても予約ができるというのが一番の売りです。

最近やけにドラッグ&ドロップ機能を多用していますが、残念なことに iPhone などのスマホでは動作しないことに気が付きました。今後のスマホの進化に期待するか、jQuery の UI の進化に期待するか、、、対応するようになればいいですけど。。。まあ対策はいくらでもありますので、気長に考えていきたいと思います。

Share

  • このエントリーをはてなブックマークに追加

Comment

コメントを残す

*がついている欄は必須項目です。

  • Twitter
  • Facebook
  • Google Plus
  • RSS Feed