[JS] POSレジシステム改 その2

タグ :

これまでの基礎知識を結集して簡易アプリを作成するシリーズです。
今回は前記事「POSレジシステム改」に引き続き、お店等で使用できる Web 上での POSレジシステム を改善します。

js_posreg2_preview

前記事では jQuery を用いてページ遷移を無くすところまでを行いました。が、レスポンスが悪いという問題点があったので、今回はそこを改善しています。レジページにて、商品をクリックした時や削除をクリックした時の処理スピードが格段にアップしました。前回同様にログインが必要ですが、店舗登録も売上登録も勝手にしちゃって大丈夫です。自由に遊んでくださいませ。

操作は こちら でお試しください。

アプリ概要

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

レスポンス対応
 - リストへの追加は jQuery のみで実行
 - リストから削除も jQuery のみで実行
 - 今までと同じように登録ができる

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

  • クリック処理
  • 動的にリストを変更
  • 動的に会計値を変更
  • 動的に会計用フォームデータを変更

ページ遷移は前回からの変更はありません。

作成したサンプル

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

[ 動作フロー ]
js_posreg2_flow1 js_posreg2_flow2
[ コード ]
$(function() {
	$('.item-area li').click(function() {
		AddItem($(this));
	});
	$('.item-delete').live('click', function() {
		DeleteItem($(this));
	});
	$('.account-btn').click(function(){
		var w = $('.modal-body').innerWidth() / 2;
		var h = $('.modal-body').innerHeight() / 2;
		$('.modal-body').css({
			'margin-left' : -w,
			'margin-top' : -h
		});
		$('.modal').fadeIn(300);
	});
	$('.close,.modal-back').click(function(){
		$('.modal').fadeOut(300);
	});
	function AddItem($obj) {
		// 合計行削除処理
		var $tObj = $('table.sale-area tbody');
		var tHtml = $tObj.html();
		$('tr:last',$tObj).remove();
		// 既存リスト取得処理
		var num = 0;
		var tData = new Array;
		$('td',tHtml).each(function(i) {
			tData[i] = $(this).text();
			if ($(this).text() == $obj.text()) {
				num = i + 1;
			}
		});
		// リスト追加処理
		var out;
		if(num) {
			var hVal = parseInt(tData[num]) + 1;
			var hPrice = parseInt($obj.attr('value')) + parseInt(tData[num + 1]);
			$('td:eq(' + num + ')',$tObj).text(hVal);
			$('td:eq(' + (num + 1) + ')',$tObj).text(hPrice + "円");
		} else {
			out = '<tr class="' + $obj.attr('class') + '">';
			out += '<td class="item-name">' + $obj.text() + '</td>';
			out += '<td class="item-value">1</td>';
			out += '<td class="item-price">' + $obj.attr('value') + '円</td>';
			out += '<td class="item-delete" value="' + $obj.attr('class') + '">削除</td></tr>';
			$tObj.append(out);
		}
		// 合計行追加処理
		var calc = parseInt(tData[tData.length - 2]) + parseInt($obj.attr('value'));
		var val = parseInt(tData[tData.length - 3]) + 1;
		out = '<tr class="last">';
		out += '<td class="item-name-last">合計</td>';
		out += '<td class="item-value-last">' + val + '</td>';
		out += '<td class="item-price-last">' + calc + '円</td>';
		out += '<td class="item-delete" value="clear">クリア</td></tr>';
		$tObj.append(out);
		// 会計値変更処理
		$('.a-price').text('金額 ' + calc + ' 円');
		$('.a-value').text('( ' + val + ' 商品)');
		// フォームデータ変更処理
		var sales = '', vals = '';
		tHtml = $tObj.html();
		$('td',tHtml).each(function(i) {
			if ($(this).attr('class').slice(-4) == 'last') {
				return false;
			}
			if (i % 4 == 0) {
				sales += $('tr:eq('+(i/4)+')',$tObj).attr('class').slice(4) + ',';
			}
			if (i % 4 == 1) {
				vals += $(this).text() + ',';
			}
		});
		$('input#PosSaleSales').val(sales);
		$('input#PosSaleVals').val(vals);
	}
	function DeleteItem($obj) {
		// リスト削除処理
		if ($obj.attr('value') == 'clear') {
			location.reload();
		} else {
			var tData = new Array;
			$('.sale-area tr.' + $obj.attr('value') + ' td').each(function(i) {
				tData[i] = $(this).text();
			});
			var val = parseInt($('tr.last td:eq(1)').text()) - parseInt(tData[1].replace('円', ''));
			var calc = parseInt($('tr.last td:eq(2)').text()) - parseInt(tData[2].replace('円', ''));
			$('table tr.last td:eq(1)').text(val);
			$('table tr.last td:eq(2)').text(calc + '円');
			$('table tr.' + $obj.attr('value')).remove();
		}
		// 会計値変更処理
		$('.a-price').text('金額 ' + calc + ' 円');
		$('.a-value').text('( ' + val + ' 商品)');
		// フォームデータ変更処理
		var sales = '', vals = '';
		var $tObj = $('table.sale-area tbody');
		var tHtml = $tObj.html();
		$('td',tHtml).each(function(i) {
			if ($(this).attr('class').slice(-4) == 'last') {
				return false;
			}
			if (i % 4 == 0) {
				sales += $('tr:eq('+(i/4)+')',$tObj).attr('class').slice(4) + ',';
			}
			if (i % 4 == 1) {
				vals += $(this).text() + ',';
			}
		});
		$('input#PosSaleSales').val(sales);
		$('input#PosSaleVals').val(vals);
	}
});
<div class="posItems index">
	<h2><?php echo __('Pos Items'); ?></h2>
	<ul class="item-area">
<?php $i = 1; foreach ($posItems as $posItem): ?>
		<li class="item<?php echo $i; ?>" value="<?php echo $posItem['PosItem']['price']; ?>"><?php echo $posItem['PosItem']['name']; ?></li>
<?php $i++; endforeach; ?>
	</ul>
	<h4 class="sale-area-h"><?php echo __('Sale Item List'); ?></h4>
	<table class="sale-area">
		<tr class="last">
			<td class="item-name-last"><?php echo __('Total'); ?></td>
			<td class="item-value-last">0</td>
			<td class="item-price-last"><?php echo '0'.__('yen'); ?></td>
			<td class="item-delete" value="clear"><?php echo __('Clear'); ?></td>
		</tr>
	</table>
	<?php echo $this->Session->flash(); ?>
</div>
<div class="actions">
	<h3><?php echo __('Actions'); ?></h3>
	<ul>
		<li><?php echo $this->Html->link(__('My Shop'), array('controller' => 'users', 'action' => 'view/'.$user_id)); ?> </li>
		<li><?php echo $this->Html->link(__('Edit Pos'), array('action' => 'edit')); ?> </li>
		<li><?php echo $this->Html->link(__('Logout'), array('controller' => 'users', 'action' => 'logout')); ?> </li>
	</ul>
</div>
<div class="account-btn">会計へ</div>
<div class="modal">
	<div class="modal-back"></div>
	<div class="modal-body">
		<p class="close">close</p>
		<div class="account">
			<h2><?php echo __('Account'); ?></h2>
			<h4><?php echo $date; ?></h4>
			<h3 class="a-price"><?php echo __('Price').' 0 '.__('yen'); ?></h3>
			<h3 class="a-value"><?php echo '( '.' 0 '.' '.__('item').')'; ?></h3>
			<?php
				echo $this->Form->create('PosSale');
				echo $this->Form->hidden('user_id', array(
					'value' => $user_id
				));
				echo $this->Form->hidden('date', array(
					'value' => $date
				));
				echo $this->Form->hidden('sales',array(
					'value' => null
				));
				echo $this->Form->hidden('vals',array(
					'value' => null
				));
				echo $this->Form->end(__('Receipt'));
			?>
		</div>
	</div>
</div>
#content {
	min-width: 800px;
}
ul.item-area {
	list-style-type: none;
	width: 305px;
}
ul.item-area li {
	float: left;
	padding-top: 10px;
	margin-top: 1px; margin-left: 1px; margin-right: 0px;
	width: 100px; height: 40px;
	text-align: center; background: #ccc; cursor: pointer;
	font-size: 20px;
}
ul.item-area li:hover {
	background: red;
}
h4.sale-area-h {
	position: absolute;
	width: 150px;
	margin-left: 350px;
}
table.sale-area {
	position: absolute;
	width: 300px;
	margin-top: 30px; margin-left: 330px;
}
table.sale-area td.item-name {
	width: 120px;
	text-align: center;
}
table.sale-area td.item-value {
	width: 20px;
	text-align: center;
}
table.sale-area td.item-price {
	width: 80px;
	text-align: right;
}
table.sale-area td.item-delete {
	width: 80px;
	text-align: center;
}
table.sale-area td.item-name-last {
	width: 120px;
	text-align: center;
	border: 0px; background: #eee;
}
table.sale-area td.item-value-last {
	width: 50px;
	text-align: center;
	border: 0px; background: #eee;
}
table.sale-area td.item-price-last {
	width: 80px;
	text-align: right;
	border: 0px; background: #eee;
}
form#PosItemIndexForm .submit {
	position: absolute;
	width: 250px;
	margin-top: -280px; margin-left: 480px;
}
dl.month-sale {
	margin-left: 20px;
}
.detail-m {
	position: absolute;
	margin-top: -83px; margin-left: 300px;
}
.detail-d {
	position: absolute;
	margin-top: -43px; margin-left: 300px;
}
dd.month-price, dd.day-price {
	width: 130px;
	text-align: right;
}
.account h2 {
	margin-left: 40%;
}
.account h3, .account h4 {
	margin-left: 30%;
}
.account .submit {
	margin-left: 50%;
}
.submit input {
	cursor: pointer;
}
.item-delete {
	cursor: pointer;
}
.item-delete:hover {
	color: red;
}
.modal {
	display:none;
}
.modal-body {
	position: fixed;
	left: 50%; top: 50%;
	width: 400px; height: 300px;
	background: white;
}
.modal-back {
	position: fixed;
	left: 0; top: 0;
	height: 100%; width: 100%;
	background: gray;
	opacity: 0.8;
}
.account-btn {
	position: absolute;
	margin-top: 40px; margin-left: 680px;
	width: 70px; height: 40px;
	background: green; line-height: 40px;
	text-align: center; font-size: 18px; color: white;
	cursor: pointer;
}
.close {
	cursor: pointer;
}
詳しい説明は後述します。
商品をクリックしてリストを表示、リスト内の削除をクリックしてリストから除去、会計値の更新、登録用フォームの更新を全て jQuery で動的に行っています。POST 送信させていないので、前回よりもレスポンスが向上しています。

コード解説

jQuery のコードの説明をします。

[ jQuery の説明 ]
[ 2行目〜4行目 商品クリックイベント ]
$('.item-area li').click(function() {
	AddItem($(this));
});

商品クリック操作により動作します。
関数「AddItem()」を呼び出します。内容については後述します。

[ 5行目〜7行目 削除クリックイベント ]
$('.item-delete').live('click', function() {
	DeleteItem($(this));
});

リスト内の削除クリック操作により動作します。
関数「DeleteItem()」を呼び出します。内容については後述します。
削除のクリックでは、リストを生成した後でもクリックイベントが有効となるように、「live()」を用いています。これにより動的に生成した箇所でもイベントを動作させることができます。

[ 8行目〜19行目 モーダルウィンドウの表示・非表示 ]

前回からの変更は無いので割愛します。

[ 20行目〜77行目 リスト追加・会計値更新・フォーム更新 ]
var $tObj = $('table.sale-area tbody');
var tHtml = $tObj.html();
$('tr:last',$tObj).remove();

合計行を削除します。
リストの追加は append() による最終行への追加になるので、最終行となる合計行を一旦削除しておきます。セレクターにて「tr:last」と指定することによりテーブルの最後を指定できます。

var num = 0;
var tData = new Array;
$('td',tHtml).each(function(i) {
	tData[i] = $(this).text();
	if ($(this).text() == $obj.text()) {
		num = i + 1;
	}
});

既存リストのデータを取得します。
each ループにて、表示中のリスト全てを取得します。リストのデータは変数配列「tData」に格納します。
変数「num」は、クリックした商品名とリスト内の商品名に同じものが合った場合に更新されます。変数配列「tData」の要素番号を格納しておきます。

var out;
if(num) {
	var hVal = parseInt(tData[num]) + 1;
	var hPrice = parseInt($obj.attr('value')) + parseInt(tData[num + 1]);
	$('td:eq(' + num + ')',$tObj).text(hVal);
	$('td:eq(' + (num + 1) + ')',$tObj).text(hPrice + "円");
} else {
	out = '<tr class="' + $obj.attr('class') + '">';
	out += '<td class="item-name">' + $obj.text() + '</td>';
	out += '<td class="item-value">1</td>';
	out += '<td class="item-price">' + $obj.attr('value') + '円</td>';
	out += '<td class="item-delete" value="' + $obj.attr('class') + '">削除</td></tr>';
	$tObj.append(out);
}

リストへの追加処理です。
上記で更新した変数「num」の判定により、クリックした商品がリストに存在するか否かで処理を分けます。num が 0 でない、つまり、更新されていればリストに商品が存在することになるので、リストの値変更処理に移行します。num が 0 、つまり、初期値のままであれば、リストには商品が無いことになるので、リストへの追加処理に移行します。
リストの値変更処理では、表示中の「個数」に +1 、「値段」に商品の値段を加算させたものへと変更します。上記で格納した変数「num」を使用しているところがポイントです。
リストへの追加処理では、変数「out」にテーブル要素となる文字列を格納し、最後に append() によって追加します。テーブルの中身にはクリックした商品の情報を入れます。

var calc = parseInt(tData[tData.length - 2]) + parseInt($obj.attr('value'));
var val = parseInt(tData[tData.length - 3]) + 1;
out = '<tr class="last">';
out += '<td class="item-name-last">合計</td>';
out += '<td class="item-value-last">' + val + '</td>';
out += '<td class="item-price-last">' + calc + '円</td>';
out += '<td class="item-delete" value="clear">クリア</td></tr>';
$tObj.append(out);

合計行を追加します。
最初に削除したので、新たに追加します。追加時にクリックした商品の値段を加算するのと、個数を +1 します。加算値はそれぞれ変数「cals」と「val」に格納しておきます。

$('.a-price').text('金額 ' + calc + ' 円');
$('.a-value').text('( ' + val + ' 商品)');

会計値を変更します。
モーダルウィンドウで表示する用の要素を変更しておきます。上記の変数「cals」と「val」を流用します。

var sales = '', vals = '';
tHtml = $tObj.html();
$('td',tHtml).each(function(i) {
	if ($(this).attr('class').slice(-4) == 'last') {
		return false;
	}
	if (i % 4 == 0) {
		sales += $('tr:eq('+(i/4)+')',$tObj).attr('class').slice(4) + ',';
	}
	if (i % 4 == 1) {
		vals += $(this).text() + ',';
	}
});
$('input#PosSaleSales').val(sales);
$('input#PosSaleVals').val(vals);

フォームデータを変更します。
フォームに挿入するデータは「リストの商品ID全て」と「商品それぞれの個数」です。表示中のリストから each ループにて取得します。取得したデータは、「1,2,5,」という文字列データになります。
ここで、each ループする前に変数「tHtml」を再度更新しているところがポイントです。更新させないと、リストへ追加する前のデータしか取得できないので、必ず更新させておきましょう。

[ 78行目113〜行目 リストから削除、会計値更新、フォーム更新 ]
if ($obj.attr('value') == 'clear') {
	location.reload();
} else {
	var tData = new Array;
	$('.sale-area tr.' + $obj.attr('value') + ' td').each(function(i) {
		tData[i] = $(this).text();
	});
	var val = parseInt($('tr.last td:eq(1)').text()) - parseInt(tData[1].replace('円', ''));
	var calc = parseInt($('tr.last td:eq(2)').text()) - parseInt(tData[2].replace('円', ''));
	$('table tr.last td:eq(1)').text(val);
	$('table tr.last td:eq(2)').text(calc + '円');
	$('table tr.' + $obj.attr('value')).remove();
}

リストから商品の削除を行います。
リストの「削除」と「クリア」をクリックしたときに動作しますが、それぞれ処理が異なります。
クリアをクリックしたときは、初期状態にするだけでいいので、ページのリロードするだけです。
削除をクリックしたときは、行の削除の前に合計値の変更をします。合計値はリストの追加時と同じように算出させています。削除する行は、削除要素の「value」の値を目印に、テーブルの tr のクラス名で判定しています。
会計値更新とフォーム更新はリスト追加時の処理を同じです。

CakePHP コードの変更点

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

[ コード(PosItemsController.php) ]
public function index() {
	$user = $this->Auth->user();
	// 商品データを取得
	$items = $this->PosItem->find('all', array(
		'conditions' => array('user_id' => $user['id']),
		'order' => array('table' => 'ASC')
	));
	// ポスト送信有なら売上にデータ登録
	if ($this->request->data) {
		$pos_data_id = split(",", $this->request->data['PosSale']['sales']);
		$pos_data_val = split(",", $this->request->data['PosSale']['vals']);
		$i = 0;
		foreach($pos_data_id as $id) {
			$pos_val[$id] = $pos_data_val[$i];
			$i++;
		}
		$pos_data = $this->PosItem->find('all', array(
			'conditions' => array('PosItem.id' => $pos_data_id)
		));
		$i = 0;
		foreach($pos_data as $pos) {
			$data[$i]['PosSale']['user_id'] = $pos['PosItem']['user_id'];
			$data[$i]['PosSale']['date'] = $this->request->data['PosSale']['date'];
			$data[$i]['PosSale']['pos_item_id'] = $pos['PosItem']['id'];
			$data[$i]['PosSale']['value'] = $pos_val[$pos['PosItem']['id']];
			$data[$i]['PosSale']['price'] = $pos['PosItem']['price'] * $pos_val[$pos['PosItem']['id']];
			$i++;
		}
		if (!empty($data)) {
			$this->PosSale->create();
			if ($this->PosSale->saveAll($data)) {
				$this->Session->setFlash(__('The items sold.'));
				$this->redirect(array('action' => 'index'));
			} else {
				$this->Session->setFlash(__('Could not be sold. Please, try again.'));
			}
		}
	}
	$this->set('posItems', $items);
	$this->set('user_id', $user['id']);
	$this->set('date', date("Y-m-d H:i:s"));
}
変更箇所として、不要になった処理をいくつか削除しました。
・jQuery による POST 送信が無くなったので、セッションを使用して値を渡す処理を削除
・同じくパラメータもいらないのでそれに関わる処理を削除
・テーブル生成を jQuery で行うようにしたので、テーブル用変数生成処理を削除
・その他使用していない変数を削除
これにより、前回よりもかなりスッキリとしたコードになりました。

まとめ

動作のレスポンスも向上し、登録も今まで通り行えるようになりました。リストの追加・削除は過去記事「疑似買い物アプリ」を参考にしましたが、変更部分も多いです。CakePHP と連携を取るのが結構ややこしい印象を受けました。

レジの処理はこのくらいで良しとして、POSレジシステムとしてはまだまだ機能が必要です。売上管理のユーザビリティ向上や、さらには、在庫管理もさせたいなと考えています。

時間が掛かりそうですが、徐々に良いものにしていきたいと思います。。。

Share

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

Comment

コメントを残す

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

  • Twitter
  • Facebook
  • Google Plus
  • RSS Feed