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

タグ :

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

js_posreg4_preview

今回のテーマとして「売上管理のユーザビリティ向上」としました。
ログインして左メニューから「マイショップ」、「売上管理」のページが今回追加作成した部分になります。月間・日毎に加え、商品別・時間帯別での売上表示が可能となりました。
いろいろと試してみてください。

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

アプリ概要

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

売上管理画面のユーザビリティ向上
 - 余分なページ遷移を無くす
 - 日付入力が簡単にできる
 - 今までと同じように表示ができる
売上管理
 - 月間・日毎の表示ができる
 - 商品別・時間帯別で条件が指定できる

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

  • クリック処理
  • 動的に要素を変更
  • 日付入力時のカレンダー表示
  • 条件入力時のリスト表示

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

作成したサンプル

コードは以下、追加した箇所のみ記述します。
売上管理に関しての処理を追加しました。長くなったので、それぞれの処理別にタブで分割して表示します。

[ jQuery ]
$(function($) {
	var path = '/sample/cakejs/posreg_js4/users/detail/';
	$('input#UserDatepicker').datepicker({
		closeText:"閉じる",
		prevText:"<前",
		nextText:"次>",
		currentText:"今日",
		monthNames:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],
		monthNamesShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],
		dayNames:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],
		dayNamesShort:["日","月","火","水","木","金","土"],
		dayNamesMin:["日","月","火","水","木","金","土"],
		weekHeader:"週",
		dateFormat:"yy/mm/dd",
		firstDay:0,isRTL:!1,
		showMonthAfterYear:!0,
		yearSuffix:"年",
		onClose: function(e) {
			if ($('#ShowDateMonth').attr('checked') == 'checked') {
				$('#UserDetailForm').attr('action',
					path + e.split('/').join('').slice(0,6)
				);
				$(this).attr('value', e.slice(0,7));
			} else {
				$('#UserDetailForm').attr('action',
					path + e.split('/').join('')
				);
			}
		}
	});
});
var path = '/sample/cakejs/posreg_js4/users/detail/';
$('#ShowDateMonth').click(function() {
	$('input:radio').attr('checked', false);
	$(this).attr('checked', 'checked');
	$('.detail h2').text('売上管理(月間)');
	$('.t-radio td:eq(0)').css('background', 'red');
	$('.t-radio td:eq(1)').css('background', '#eee');
	$('.t-radio td:eq(2)').css('background', '#eee');
	$('.t-radio td:eq(3)').css('background', '#eee');
	$('.select-btn').hide();
	$('#UserDatepicker').attr('value', $('#hideDate').attr('value').slice(0,7));
	$('#UserDetailForm').attr('action',
		path + $('#hideDate').attr('value').split('/').join('').slice(0,6)
	);
});
$('#ShowDateDay').click(function() {
	$('input:radio').attr('checked', false);
	$(this).attr('checked', 'checked');
	$('.detail h2').text('売上管理(日毎)');
	$('.t-radio td:eq(0)').css('background', '#eee');
	$('.t-radio td:eq(1)').css('background', 'red');
	$('.t-radio td:eq(2)').css('background', '#eee');
	$('.t-radio td:eq(3)').css('background', '#eee');
	$('.select-btn').hide();
	$('#UserDatepicker').attr('value', $('#hideDate').attr('value'));
	$('#UserDetailForm').attr('action',
		path + $('#hideDate').attr('value').split('/').join('')
	);
});
var flgList = false;
$('#ShowDateItem').click(function() {
	$('input:radio').attr('checked', false);
	$(this).attr('checked', 'checked');
	$('.detail h2').text('売上管理(商品別)');
	$('.t-radio td:eq(0)').css('background', '#eee');
	$('.t-radio td:eq(1)').css('background', '#eee');
	$('.t-radio td:eq(2)').css('background', 'red');
	$('.t-radio td:eq(3)').css('background', '#eee');
	$('.select-btn').show();
});
$('.select-btn').click(function() {
	if ($('#ShowDateItem').attr('checked') == 'checked') {
		$('.select-list').toggle();
	} else if ($('#ShowDateTime').attr('checked') == 'checked') {
		$('.time-list').toggle();
	}
});
$('.select-btn').hover(function() {
	flgList = true;
}, function() {
	flgList = false;
});
$('body').click(function(){
	if (!flgList) {
		$('.select-list').hide();
		$('.time-list').hide();
	}
});
$('.select-list ul li').click(function() {
	var pos = $('#UserDetailForm').attr('action').indexOf('-');
	if (pos <= 0) {
		pos = $('#UserDetailForm').attr('action').length;
	}
	$('#UserDetailForm').attr('action',
		$('#UserDetailForm').attr('action').substr(0, pos) + '-i' + $(this).attr('value')
	);
	var pos = $('#UserDatepicker').val().indexOf(' - ');
	if (pos <= 0) {
		pos = $('#UserDatepicker').val().length;
	}
	$('#UserDatepicker').val($('#UserDatepicker').val().substr(0, pos) + ' - ' + $(this).text());
});
$('#ShowDateTime').click(function() {
	('input:radio').attr('checked', false);
	$(this).attr('checked', 'checked');
	$('.detail h2').text('売上管理(時間帯)');
	$('.t-radio td:eq(0)').css('background', '#eee');
	$('.t-radio td:eq(1)').css('background', '#eee');
	$('.t-radio td:eq(2)').css('background', '#eee');
	$('.t-radio td:eq(3)').css('background', 'red');
	$('.select-btn').show();
	$('#UserDatepicker').attr('value', $('#hideDate').attr('value'));
	$('#UserDetailForm').attr('action',
		path + $('#hideDate').attr('value').split('/').join('')
	);
});
$('.time-list ul li').click(function() {
	var pos = $('#UserDetailForm').attr('action').indexOf('-');
	if (pos <= 0) {
		pos = $('#UserDetailForm').attr('action').length;
	}
	$('#UserDetailForm').attr('action',
		$('#UserDetailForm').attr('action').substr(0, pos) + '-t' + $(this).attr('value')
	);
	var pos = $('#UserDatepicker').val().indexOf(' - ');
	if (pos <= 0) {
		pos = $('#UserDatepicker').val().length;
	}
	$('#UserDatepicker').val($('#UserDatepicker').val().substr(0, pos) + ' - ' + $(this).text());
});
詳細は後述します。
日付入力箇所にはカレンダーを表示して、選択した日付を記述するUI「datepicker」を使用します。
月間・日毎に加えて、商品別・時間帯の指定もできるようにしています。
[ HTML ]
<div class="users view detail">
	<h2><?php  echo __($title); ?></h2>
	<?php echo $this->Session->flash(); ?>
	<?php
		echo '<table class="t-radio"><tr><td>';
		echo $this->Form->radio('ShowDate',
			array(
				'month' => '月間',
				'day' => '日毎',
				'item' => '商品別',
				'time' => '時間帯'
			),
			array(
				'legend' => false,
				'separator' => '</td><td>',
				'value' => $radio_val
			)
		);
		echo '</td></tr></table>';
		echo $this->Form->hidden('hideDate', array(
			'value' => $hide_date
		));
		echo $this->Form->create('User', array(
			'url' => 'detail/'.$post_date,
			'inputDefaults' => array(
				'label' => false,
				'div' => false
			)
		));
		echo $this->Form->input('datepicker');
		echo '<span class="select-btn">▼</span>';
		echo $this->Form->end(__('Show'));
	?>
	<div class="select-list">
		<ul>
<?php foreach ($items as $item): ?>
			<li value="<?php echo $item['PosItem']['id']; ?>"><?php echo $item['PosItem']['name']; ?></li>
<?php endforeach; ?>
		</ul>
	</div>
	<div class="time-list">
		<ul>
			<li value="1">00:00 〜 05:59</li>
			<li value="2">06:00 〜 11:59</li>
			<li value="3">12:00 〜 17:59</li>
			<li value="4">18:00 〜 23:59</li>
		</ul>
	</div>
	<h3><?php echo $str_date; ?></h3>
	<h3><?php echo __('Total').' '.$total_price.' '.__('yen').' ( '.$total_value.' '.__('item').' )'; ?></h3>
	<table cellpadding="0" cellspacing="0">
		<tr>
			<th><?php echo __('Date'); ?></th>
			<th><?php echo __('Item'); ?></th>
			<th><?php echo __('Value'); ?></th>
			<th><?php echo __('Price'); ?></th>
		</tr>
<?php foreach ($lists as $list): ?>
		<tr>
			<td><?php echo $list['PosSale']['date']; ?></td>
			<td><?php echo $list['PosItem']['name']; ?></td>
			<td><?php echo $list['PosSale']['value']; ?></td>
			<td><?php echo $list['PosSale']['price']; ?></td>
		</tr>
<?php endforeach; ?>
	</table>
</div>

<div class="actions">
	<h3><?php echo __('Actions'); ?></h3>
	<ul>
		<li><?php echo $this->Html->link(__('My Shop'), array('action' => 'view/'.$user_id)); ?> </li>
		<li><?php echo $this->Html->link(__('Logout'), array('action' => 'logout')); ?> </li>
	</ul>
</div>
#UserDatepicker {
	width: 360px;
}
#UserDetailForm .submit {
	margin-top: -47px; margin-left: 400px;
}
.t-radio {
	width: 300px;
}
.t-radio td {
	text-align: center;
	border: none;
	background: #eee;
}
.t-radio td:hover {
	background: red!important;
}
.t-radio td input {
	display: none;
}
.select-btn {
	padding: 6px 3px 6px 3px;
	font-size: 150%;
	background: gray;
	cursor: pointer;
	display: none;
}
.select-list, .time-list {
	position: absolute;
	top: 166px; margin-left: 100px;
	width: 273px;
	background: #eee;
	text-align: center;
	display: none;
}
.select-list ul, .time-list ul {
	list-style-type: none;
	margin: 0;
}
.select-list ul li, .time-list ul li {
	margin: 0;
}
.select-list ul li:hover, .time-list ul li:hover {
	background: red;
}
売上管理画面としては「detail.ctp」にて表示させています。変更点は以下。
[ 4行目〜33行目 日付・条件指定箇所 ]

日付・条件を指定するためのクリック箇所を設けました。
指定には、ラジオボタンを使用していますが、見た目ではラジオボタンとは分からないようにしています(CSSで)。

[ 34行目〜40行目 商品別用の商品リスト ]

条件指定の商品別を選択した場合にのみ、テキスト横の矢印をクリックすることで商品リストが表示されるようにしています。

[ 41行目〜48行目 時間帯用の時間帯リスト ]

条件指定の時間帯を選択した場合にのみ、テキスト横の矢印をクリックすることで時間帯リストが表示されるようにしています。

コード解説

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

[ jQuery の説明 ]
[ datepicker – 3行目〜17行目 日本語化の設定 ]

色々とネットに転がっているものを流用しました。

[ datepicker – 18行目〜29行目 テキストへの記入処理 ]
onClose: function(e) {
	if ($('#ShowDateMonth').attr('checked') == 'checked') {
		$('#UserDetailForm').attr('action',
			path + e.split('/').join('').slice(0,6)
		);
		$(this).attr('value', e.slice(0,7));
	} else {
		$('#UserDetailForm').attr('action',
			path + e.split('/').join('')
		);
	}
}

カレンダーを閉じると同時にテキストへ選択内容を記入します。
月間とそれ以外を指定している場合で記入内容を分けています。onCloseメソッドの引数「e」には選択した日付文字列が格納されているので、それをちょいと編集してテキストへ記入するようにしています。

[ 月間 – 2行目〜15行目 「月間」指定時の処理 ]
$('input:radio').attr('checked', false);
$(this).attr('checked', 'checked');

ラジオボタンのチェックを入れます。
その他のラジオボタンでも同様の処理になりますが、全部のラジオボタンのチェックを外した後で、クリックしたラジオボタンだけにチェックを入れることで、毎回どれか一つにチェックが入っているようにしています。

$('#UserDatepicker').attr('value', $('#hideDate').attr('value').slice(0,7));
$('#UserDetailForm').attr('action',
	path + $('#hideDate').attr('value').split('/').join('').slice(0,6)
);

テキストへの記入とページ遷移のためのパラメータを付加させます。

[ 日毎 – 2行目〜13行目 「日毎」指定時の処理 ]

「月間」とほとんど同じですので割愛します。

[ 商品別 – 3行目〜10行目 「商品別」指定時の処理 ]

「月間」とほとんど同じです。違うのは、テキスト横の矢印を表示させているところだけです。

[ 商品別 – 12行目〜18行目 テキスト横の矢印クリック時の処理 ]
if ($('#ShowDateItem').attr('checked') == 'checked') {
	$('.select-list').toggle();
} else if ($('#ShowDateTime').attr('checked') == 'checked') {
	$('.time-list').toggle();
}

リストを表示させます。
条件の商品別と時間帯で表示するリストを変えています。表示には「toggle()」を用いているので、表示中にクリックすると非表示になるようにしています。

[ 商品別 – 19行目〜29行目 リスト以外をクリックしたときの処理 ]
$('.select-btn').hover(function() {
	flgList = true;
}, function() {
	flgList = false;
});
$('body').click(function(){
	if (!flgList) {
		$('.select-list').hide();
		$('.time-list').hide();
	}
});

テキスト横の矢印にカーソルがあるときには変数「flgList」の値は true となるようにし、それ以外のときは false となるようにしています。24行目からの body のクリック処理内で変数「flgList」が false の場合のみリストを非表示にするようにしています。つまり、リスト以外の部分をクリックしてもリストが消えるようにしてるってことです。

[ 商品別 – 30行目〜43行目 リストクリック時の処理 ]
var pos = $('#UserDetailForm').attr('action').indexOf('-');
if (pos <= 0) {
	pos = $('#UserDetailForm').attr('action').length;
}
$('#UserDetailForm').attr('action',
	$('#UserDetailForm').attr('action').substr(0, pos) + '-i' + $(this).attr('value')
);
var pos = $('#UserDatepicker').val().indexOf(' - ');
if (pos <= 0) {
	pos = $('#UserDatepicker').val().length;
}
$('#UserDatepicker').val($('#UserDatepicker').val().substr(0, pos) + ' - ' + $(this).text());

リスト内をクリックした時の処理として、テキスト内文字列の変更と、ページ遷移のためのURLのパラメータを変更させています。
商品別、時間帯の条件を指定するときのパラメータには「-(ハイフン)」を日付の後ろに付けてそれに続けて商品IDや時間帯IDを付加するようにしています。ちょっと無理矢理な感じはありますが、、、

[ 時間帯 – 2行目〜13行目 「時間帯」指定時の処理 ]

「商品別」とほとんど同じなので割愛します。

[ 時間帯 – 15行目〜28行目 リストクリック時の処理 ]

「商品別」のリストクリック時の処理とほとんど同じなので割愛します。

CakePHP コードの変更点

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

[ コード(UsersController.php) ]
public function detail($param = null) {
	$user = $this->Auth->user();
	// パラメータチェック
	$itemNo = 0;
	$timeNo = 0;
	if (strstr($param, 'i')) {
		$itemNo = substr($param, strpos($param, 'i') + 1);
		$param = substr($param, 0, strpos($param, 'i') - 1);
	} else if (strstr($param, 't')) {
		$timeNo = substr($param, strpos($param, 't') + 1);
		$param = substr($param, 0, strpos($param, 't') - 1);
	}
	if (strlen($param) == 6) {
		// 文字数が 6 なら「月」
		$y = substr($param, 0, 4);
		$m = substr($param, 4);
		$str_date = date('Y/m', mktime(0,0,0,$m,1,$y));
		$radio_val = 'month';
		// 日付チェック
		if (!checkdate($m, '01', $y)) {
			$this->Session->setFlash(__('Invalid date'));
			$this->redirect(array('action' => 'view/'.$user['id']));
		}
		// データ取得期間を取得
		$date_first = date('Y-m-d', mktime(0,0,0,$m,1,$y));
		$date_last = date('Y-m-d', mktime(0,0,0,$m+1,1,$y));
		$this->set('post_date', date('Ym', mktime(0,0,0,$m,1,$y)));
		$this->set('title', 'Month Detail');
		$this->set('str_page', 'Day Detail');
		$this->set('link_page', date('Ymd'));
	} else if (strlen($param) == 8 || strlen($param) == 10) {
		// 文字数が 8 なら「日」
		$y = substr($param, 0, 4);
		$m = substr($param, 4, 2);
		$d = substr($param, 6, 2);
		$str_date = date('Y/m/d', mktime(0,0,0,$m,$d,$y));
		$radio_val = 'day';
		// 日付チェック
		if (!checkdate($m, $d, $y)) {
			$this->Session->setFlash(__('Invalid date'));
			$this->redirect(array('action' => 'view/'.$user['id']));
		}
		// データ取得期間を取得
		$date_first = date('Y-m-d', mktime(0,0,0,$m,$d,$y));
		$date_last = date('Y-m-d', mktime(0,0,0,$m,$d+1,$y));
		$this->set('post_date', date('Ymd', mktime(0,0,0,$m,$d,$y)));
		$this->set('title', 'Day Detail');
		$this->set('str_page', 'Month Detail');
		$this->set('link_page', date('Ym'));
	} else {
		// それ以外はNG
		$this->Session->setFlash(__('Invalid date'));
		$this->redirect(array('action' => 'view/'.$user['id']));
	}
	// 時間帯指定
	if ($timeNo) {
		switch ($timeNo) {
		case 1:
			$t_first = '00:00'; $t_last = '05:59'; break;
		case 2:
			$t_first = '06:00'; $t_last = '11:59'; break;
		case 3:
			$t_first = '12:00'; $t_last = '17:59'; break;
		case 4:
			$t_first = '18:00'; $t_last = '23:59'; break;
		default:
			$t_fitst = ''; $t_last = ''; break;
		}
		$date_last = $date_first.' '.$t_last;
		$date_first .= ' '.$t_first;
		$str_date .= ' - '.$t_first.' 〜 '.$t_last;
		$radio_val = 'time';
	}
	// 売上データ取得
	if ($itemNo) {
		$lists = $this->PosSale->find('all', array(
			'conditions' => array(
				'PosSale.user_id' => $user['id'],
				'PosSale.pos_item_id' => $itemNo,
				'PosSale.date >=' => $date_first,
				'PosSale.date <=' => $date_last
			)
		));
		$radio_val = 'item';
	} else {
		$lists = $this->PosSale->find('all', array(
			'conditions' => array(
				'PosSale.user_id' => $user['id'],
				'PosSale.date >=' => $date_first,
				'PosSale.date <=' => $date_last
			)
		));
	}
	// 商品データ取得
	$items = $this->PosItem->find('all', array(
		'conditions' => array('PosItem.user_id' => $user['id']),
		'fields' => array('PosItem.id', 'PosItem.name')
	));
	foreach ($items as $item) {
		if ($item['PosItem']['id'] == $itemNo) {
			$str_date .= ' - '.$item['PosItem']['name'];
			break;
		}
	}
	// 合計金額と合計個数の取得
	$total_price = $total_value = 0;
	foreach($lists as $list) {
		$total_price += $list['PosSale']['price'];
		$total_value += $list['PosSale']['value'];
	}
	$this->set('lists', $lists);
	$this->set('items', $items);
	$this->set('radio_val', $radio_val);
	$this->set('str_date', $str_date);
	$this->set('total_price', $total_price);
	$this->set('total_value', $total_value);
	$this->set('user_id', $user['id']);
	$this->set('hide_date', date('Y/m/d'));
}
変更点だけを以下にまとめます。
[ 6行目〜12行目 パラメータチェック ]
if (strstr($param, 'i')) {
	$itemNo = substr($param, strpos($param, 'i') + 1);
	$param = substr($param, 0, strpos($param, 'i') - 1);
} else if (strstr($param, 't')) {
	$timeNo = substr($param, strpos($param, 't') + 1);
	$param = substr($param, 0, strpos($param, 't') - 1);
}

パラメータに「i」がある場合は条件に商品が指定されているので変数「$itemNo」に商品IDを格納します。
パラメータに「t」がある場合は条件に時間帯が指定されているので変数「$timeNo」に時間帯IDを格納します。
さらに、どちらの場合でも変数「$param」にパラメータから条件を除去したもの(日付のみ)を格納します。

[ 56行目〜73行目 時間帯指定 ]
if ($timeNo) {
	switch ($timeNo) {
	case 1:
		$t_first = '00:00'; $t_last = '05:59'; break;
	case 2:
		$t_first = '06:00'; $t_last = '11:59'; break;
	case 3:
		$t_first = '12:00'; $t_last = '17:59'; break;
	case 4:
		$t_first = '18:00'; $t_last = '23:59'; break;
	default:
		$t_fitst = ''; $t_last = ''; break;
	}
	$date_last = $date_first.' '.$t_last;
	$date_first .= ' '.$t_first;
	$str_date .= ' - '.$t_first.' 〜 '.$t_last;
	$radio_val = 'time';
}

前の処理にて格納した変数「$timeNo」の値に応じて時間帯を指定します。
その後で日付指定用変数の「$date_first」と「$date_last」に時間帯を追加しています。時間帯は24時間を4分割した時間になります。

[ 75行目〜84行目 商品指定時の売上データ取得 ]
if ($itemNo) {
	$lists = $this->PosSale->find('all', array(
		'conditions' => array(
			'PosSale.user_id' => $user['id'],
			'PosSale.pos_item_id' => $itemNo,
			'PosSale.date >=' => $date_first,
			'PosSale.date <=' => $date_last
		)
	));
	$radio_val = 'item';
}

商品指定時には、データベースからのデータ取得処理にて、条件(conditions)に商品IDも追加で指定することで対応します。

まとめ

売上管理としては良くなったのではないでしょうか。商品と時間帯の指定もできるようにしたのはかなり自信作です。というか指定部分の表示にはかなり力を入れたつもりです。

とりあえずのところ、POSレジシステムのカスタマイズはこのくらいにしておこうと思います。
実際のPOSレジにはまだまだ足りない機能がありますが、そんなにこのシステムに時間を使ってられないので、、、
バグがあったりしたら修正するくらいにしようかと思ってます。

Share

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

Comment

コメントを残す

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

  • Twitter
  • Facebook
  • Google Plus
  • RSS Feed