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

タグ :

これまでの基礎知識を結集して簡易アプリを作成するシリーズです。
今回は前記事「POSレジシステム改 その2」に引き続き、お店等で使用できる Web 上での POSレジシステム を改善します。
※また、前回までのシステムでは、色々とバグがあったので、気付いた点を修正しています。それも併せて説明します。

js_posreg3_preview

今回のテーマとして「商品設定箇所のユーザビリティ向上」としました。
ログインして左メニューの「POS設定」という項目をクリックすると、レジの編集が行えます。実際のレジと同じ画面が表示され、商品をクリックすると、モーダルウィンドウが表示され、そこで商品の情報を変更できるようになっています。さらに、今回は商品の在庫も管理できるようにしました。
いろいろと試してみてください。

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

アプリ概要

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

POS設定のユーザビリティ向上
 - 余分なページ遷移を無くす
 - 分かり易い表示と操作性の向上
 - 今までと同じように登録ができる
在庫管理
 - 在庫の設定ができる
 - 売上により在庫を減らす
 - 在庫切れ時の警告を追加

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

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

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

作成したサンプル

コードは以下になります。
在庫管理に関しての処理を追加しました。長くなったので、タブで分割して表示します。

[ jQuery ]
var idArray = new Array;
var stockArray = new Array;
$('.index .item-area li').click(function() {
	AddItem($(this));
});
$('.index .item-delete').live('click', function() {
	DeleteItem($(this));
});
$('.form .item-area li').click(function() {
	SetEditItem($(this));
	ShowModal('.modal-edit');
});
$('.account-btn').click(function(){
	ShowModal('.modal-account');
});
$('.close,.modal-back').click(function(){
	$('.modal').fadeOut(300);
});
function AddItem($obj) {
	// 在庫数チェック
	if($obj.attr('stock') == 0) {
		alert('No Stock!\r\n在庫がありません。');
		return false;
	} else {
		$obj.attr('stock', $obj.attr('stock') - 1);
		var flg = true;
		for(i = 0; i <= idArray.length - 1; i++) {
			if(idArray[i] == $obj.attr('id')) {
				stockArray[i]--;
				flg = false;
			}
		}
		if(flg) {
			idArray[idArray.length] = $obj.attr('id');
			stockArray[stockArray.length] = parseInt($obj.attr('stock'));
		}
	}
	// 合計行削除処理
	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 id="' + $obj.attr('id') + '" 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;
		} else if ($(this).attr('class') == 'item-value') {
			vals += $(this).text() + ',';
		} else if ($(this).attr('class') == 'item-delete') {
			sales += $(this).attr('id') + ',';
		}
	});
	$('input#PosSaleSales').val(sales);
	$('input#PosSaleVals').val(vals);
	$('input#PosSaleItemId').val(idArray);
	$('input#PosSaleItemStock').val(stockArray);
}
function DeleteItem($obj) {
	// 在庫数復元
	var sVal = '.' + $obj.attr('value');
	var valval = parseInt($('.item-value', sVal).text()) + parseInt($(sVal).attr('stock'));
	$(sVal).attr('stock', valval);
	for(i = 0; i <= idArray.length - 1; i++) {
		if (idArray[i] == $obj.attr('id')) {
			stockArray[i] = valval;
		}
	}
	// リスト削除処理
	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;
		} else if ($(this).attr('class') == 'item-value') {
			vals += $(this).text() + ',';
		} else if ($(this).attr('class') == 'item-delete') {
			sales += $(this).attr('id') + ',';
		}
	});
	$('input#PosSaleSales').val(sales);
	$('input#PosSaleVals').val(vals);
	$('input#PosSaleItemStock').val(stockArray);
}
function SetEditItem($obj) {
	$('input#PosItemId').val($obj.attr('id'));
	$('input#PosItemName').val($obj.text());
	$('input#PosItemPrice').val($obj.val());
	$('input#PosItemStock').val($obj.attr('stock'));
}

function ShowModal(mClass) {
	var w = $(mClass).innerWidth() / 2;
	var h = $(mClass).innerHeight() / 2;
	$(mClass).css({
		'margin-left' : -w,
		'margin-top' : -h
	});
	$('.modal').fadeIn(300);
}
詳細は後述します。
操作系では、各要素のクリック操作時処理を並べています。
AddItem()は、リストへの商品追加処理です。前記事からの変更点は在庫処理に関するものだけです。
DeleteItem()は、リストからの商品削除処理です。これも在庫処理に関するものだけを追加しているだけです。
その他のSetEditItem()は、商品編集に関する処理です。ShowModal()は、モーダルウィンドウ表示部の処理を関数化しただけです。
[ HTML ]
<div class="posItems index">
	<h2><?php echo __('Pos Items'); ?></h2>
	<ul class="item-area">
<?php $i = 1; foreach ($posItems as $posItem): ?>
		<li id="<?php echo $posItem['PosItem']['id']; ?>"
			<?php if ($posItem['PosItem']['stock'] == 0): ?>
			class="item<?php echo $i.' none'; ?>"
			<?php  else: ?>
			class="item<?php echo $i; ?>"
			<?php endif; ?>
			value="<?php echo $posItem['PosItem']['price']; ?>"
			stock="<?php echo $posItem['PosItem']['stock']; ?>"
		><?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-account">
		<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->hidden('item_id',array(
					'value' => null
				));
				echo $this->Form->hidden('item_stock',array(
					'value' => null
				));
				echo $this->Form->end(__('Receipt'));
			?>
		</div>
	</div>
</div>
<div class="posItems form">
	<h2><?php echo __('Edit Pos Item'); ?></h2>
	<h3><?php echo __('Click the items you want to edit.'); ?></h3>
	<ul class="item-area">
<?php $i = 1; foreach ($posItems as $posItem): ?>
		<li id="<?php echo $posItem['PosItem']['id']; ?>"
			<?php if ($posItem['PosItem']['stock'] == 0): ?>
			class="item<?php echo $i.' none'; ?>"
			<?php  else: ?>
			class="item<?php echo $i; ?>"
			<?php endif; ?>
			value="<?php echo $posItem['PosItem']['price']; ?>"
			stock="<?php echo $posItem['PosItem']['stock']; ?>"
		><?php echo $posItem['PosItem']['name']; ?></li>
<?php $i++; endforeach; ?>
	</ul>
	<?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(__('Pos Items'), array('action' => 'index')); ?></li>
	</ul>
</div>
<div class="modal">
	<div class="modal-back"></div>
	<div class="modal-edit">
		<p class="close">close</p>
		<div class="edit">
			<?php
				echo $this->Form->create('PosItem');
				echo $this->Form->hidden('id', array(
					'value' => null
				));
				echo $this->Form->hidden('user_id', array(
					'value' => $user_id
				));
				echo $this->Form->input('name',array(
					'label' => __('Item'),
					'value' => null,
					'type' => 'text'
				));
				echo $this->Form->input('price',array(
					'value' => null,
					'type' => 'text'
				));
				echo $this->Form->input('stock',array(
					'value' => null,
					'type' => 'text'
				));
				echo $this->Form->end(__('Regist'));
			?>
		</div>
	</div>
</div>
「edit.ctp」は「index.ctp」を流用して作成しました。
「index.ctp」にも若干の変更があります。変更は以下。
[ index.ctp – 5行目〜13行目 在庫数チェック ]

在庫数が 0 の商品に関してのみクラス名に「none」を追加させました。この「none」は CSS にて背景色を通常よりも暗くしているだけです。

[ AddItem – 59行目〜64行目 在庫情報追加 ]

会計フォーム部に在庫情報として商品ID「item_id」と在庫数「item_stock」を追加しました。

その他は特に変更していませんので、詳細は前記事を参照してください。

コード解説

jQuery のコードの説明をします。前回からの追加点・変更点のみを説明します。

[ jQuery の説明 ]
[ AddItem – 3行目〜19行目 在庫数チェック ]
if($obj.attr('stock') == 0) {
	alert('No Stock!\r\n在庫がありません。');
	return false;

在庫があるかチェックします。
商品の要素「stock」には在庫数を格納しているので、attr(‘stock’) により在庫数を取得し、0 でないかを確認します。0 の場合はリスト追加処理を行わずに警告を表示した後、終了します。

$obj.attr('stock', $obj.attr('stock') - 1);
var flg = true;
for(i = 0; i <= idArray.length - 1; i++) {
	if(idArray[i] == $obj.attr('id')) {
		stockArray[i]--;
		flg = false;
	}
}

商品の要素「stock」の数値を -1 します。
また、リストからの削除時のために、残り在庫数を保持しておきます。保持には変数配列「stockArray」を用います。配列内には、商品をリストに追加した順に在庫数を格納させています。

if(flg) {
	idArray[idArray.length] = $obj.attr('id');
	stockArray[stockArray.length] = parseInt($obj.attr('stock'));
}

上記で使用する変数配列「stockArray」にクリックした商品の在庫数を配列の最後に格納します。
上記処理内の分岐条件で使用している変数配列「idArray」にはクリックした商品のIDを配列の最後に格納しています。
また、変数「flg」により、この処理はクリックした商品がリストに無い場合にのみ実行されます。

[ AddItem – 74行目〜75行目 フォーム値の更新 ]
$('input#PosSaleItemId').val(idArray);
$('input#PosSaleItemStock').val(stockArray);

POST送信用フォームに在庫処理に必要なデータを挿入します。

【バグの修正箇所】
[内容]
前回までのシステムでは、商品の売上登録をした際に会計額と売上額が違うというバグが発生していました。


[原因]
前回までのコードでは、変数「sales」にリスト内の各商品要素のクラス名を格納していました。ここは、クラス名ではなく、IDでなければなりませんでした。うっかりミスです。。。

[ AddItem – 41行目〜46行目 バグの修正 1 ]
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 id="' + $obj.attr('id') + '" class="item-delete" value="' + $obj.attr('class') + '">削除</td></tr>';
$tObj.append(out);

会計時に商品IDを格納するには、商品要素にIDを持たせておかなければいけないので、「id=”(商品ID)”」となるように追加しました。
変更したのは 45 行目だけです。

[ AddItem – 64行目〜70行目 バグの修正 2 ]
if ($(this).attr('class').slice(-4) == 'last') {
	return false;
} else if ($(this).attr('class') == 'item-value') {
	vals += $(this).text() + ',';
} else if ($(this).attr('class') == 'item-delete') {
	sales += $(this).attr('id') + ',';
}

先程の処理でリストに追加したIDを商品IDとして変数「sales」に格納します。attr(‘id’) としてIDを取得します。
条件式もちょっと回りくどいことをしていたので修正しておきました。前回のコードと見比べると分かると思います。。。

[ DeleteItem – 3行目〜10行目 在庫数復元 ]
var sVal = '.' + $obj.attr('value');
var valval = parseInt($('.item-value', sVal).text()) + parseInt($(sVal).attr('stock'));
$(sVal).attr('stock', valval);

商品追加時に更新した在庫数を元に戻します。
変数「valval」にリストに表示中の商品個数と商品要素の現在の在庫数の加算値を格納します。それを商品要素の在庫数情報に挿入します。
元の在庫数に戻すという処理は、値を保持させておく必要があるので面倒くさいです。今回は結構無理矢理な処理で行いましたが、やり方は色々とあると思います。。。

for(i = 0; i <= idArray.length - 1; i++) {
	if (idArray[i] == $obj.attr('id')) {
		stockArray[i] = valval;
	}
}

商品の在庫数保持に使用している変数配列「stockArray」も元に戻します。
リストの商品IDとID保持用の変数配列「idArray」の値を比較して、一致した配列番号の中身を更新させています。もっとスマートなやり方があると思いますが。。。

[ DeleteItem – 43行目 フォーム値の更新 ]
$('input#PosSaleItemStock').val(stockArray);

追加時と同じようにPOST送信用フォームに在庫処理に必要なデータを挿入します。

[ その他 – 1行目〜6行目 フォーム値の更新 ]

POST送信用フォームに編集処理に必要なデータを挿入します。

[ その他 – 8行目〜16行目 モーダルウィンドウの表示 ]

前回のモーダルウィンドウ表示処理を関数化しただけです。

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']);
		$pos_item_id = split(",", $this->request->data['PosSale']['item_id']);
		$pos_item_stock = split(",", $this->request->data['PosSale']['item_stock']);
		// 登録用売上データ生成
		$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'] = $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++;
		}
		// 登録用商品データ生成
		$i = 0;
		foreach($pos_item_id as $item_id) {
			$item_data[$i]['PosItem']['id'] = $item_id;
			$item_data[$i]['PosItem']['stock'] = $pos_item_stock[$i];
			$i++;
		}
		// 登録処理
		if (!empty($data)) {
			$this->PosSale->create();
			if ($this->PosSale->saveAll($data)) {
				$this->PosItem->create();
				$this->PosItem->saveAll($item_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"));
}
public function edit() {
	// ポスト送信有なら商品を更新
	if ($this->request->is('post')) {
		$this->PosItem->create();
		if ($this->PosItem->save($this->request->data)) {
			$this->Session->setFlash(__('The pos item has been saved'));
			$this->redirect(array('action' => 'edit'));
		} else {
			$this->Session->setFlash(__('The pos item could not be saved. Please, try again.'));
		}
	}
	$user = $this->Auth->user();
	$items = $this->PosItem->find('all', array(
		'conditions' => array('user_id' => $user['id']),
		'order' => array('table' => 'ASC')
	));
	$this->set('posItems', $items);
	$this->set('user_id', $user['id']);
}
/* 〜 削除 〜 */
変更点だけを以下にまとめます。
[ index – 34行目〜39行目 登録用商品データ生成 ]
$i = 0;
foreach($pos_item_id as $item_id) {
	$item_data[$i]['PosItem']['id'] = $item_id;
	$item_data[$i]['PosItem']['stock'] = $pos_item_stock[$i];
	$i++;
}

商品データ更新用のデータを生成します。
今回からは在庫も処理できるようにしたので、在庫データのある商品テーブルへ更新をかけています。更新するデータは、在庫(stock)フィールドのみなので、更新に必要なidと在庫(stock)のデータのみを生成すれば大丈夫です。データはフォームのポスト送信データから引っ張ってきています。

[ index – 44行目〜45行目 商品データの更新 ]
this->PosItem->create();
$this->PosItem->saveAll($item_data);

商品テーブルを更新します。
保存チェックは省略しています。本当はした方が良いのですが、、、

[ edit – 1行目〜19行目 編集処理 ]

前回までは、editページから「編集」をクリックすることにより、edit_itemページに遷移して、そのページのフォームを編集することで商品の変更を行っていました。今回は、全てeditページにまとめたので、edit_itemページは削除しました。フォーム部分はモーダルウィンドウで表示させることで、ページ遷移を無くしています。

まとめ

徐々に使えそうなものになってきました。バグも多々ありますが、今後、発見次第調整していこうと思います。

本物のPOSレジシステムは、もっと複雑な処理が必要です。商品の消費期限の管理とか、在庫処分に関する処理など、、、これらの作成はちょいと厳しいので作成する予定はありませんが、簡単なレジシステムとしてなら使えるのでは、、、という期待です。

あと追加できることと言えば、売上管理のユーザビリティ向上くらいです。ソースコードも徐々に長々としたものになってきてしまったので、POSレジに関しては、そろそろ終わりにしようかとも思っています。。。

ご指摘・ご要望は受付けています。気軽にご連絡くださいませ。

Share

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

Comment

コメントを残す

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

  • Twitter
  • Facebook
  • Google Plus
  • RSS Feed