電子帳簿保存への応用編(webix+PHP)No.014
今回は、以前にも記述している電子帳簿管理にWebix(UI)とPHPの業務アプリを活用する応用編(ステップ1)です。今回紹介する機能で電子帳簿が全て実現できるわけではありませんが、基本動作を紹介します。スマホやパソコン上にメールや、指定サイトからダウンロードして、一時的に保存した領収書や請求書や、商店で購入したときにもらったレシートの写し(レシートは写真撮影した画像)をアップロードして格納する機能と、格納した情報の一覧及び、一覧から指定行クリックで、情報を閲覧したり、ダウンロードする機能です。
Webexには様々なコンポーネントが揃っていて、今回は、その中でUPLOAD機能を使って実装してみます。UPLOAD機能のマニュアルは、以下のリンクで確認して見てください。
ファイルをアップロードするには、対象ファイルを選択してリストアップし、アップロード操作をするイメージです。webixのライブラリは、いろいろな使用方法が記述されていて、今回はその一例です。ファイル選択と同時にアップロード動作をする実装も可能ですが、今回は、ファイル選択と格納を別操作で実装して見ました。
パソコン向けの画面であれば、指定エリアにドラックすることで、ファイルを指定する操作も実装できます。作成したサンプルは、スマホで操作できる画面デザインにしてみました。スマホでレシートを写真撮影してその画像を格納する操作です。ファイルは、PDFでもEXCELでも格納できます。画像ファイルやPDFの場合は、格納した後に一覧から対象ファイルを指定すると、ダウンロードではなく、ブラウザで確認する(ビューワー)操作としました。EXCELなどは、ダウンロード操作しました。実際に格納する操作や保存する機能を実装するときは、不用意に、アップロードしたファイルをダウンロードされないように、URLからはアクセスできない場所に格納し、指定された時に、一時フォルダに転送してダウンロードする実装などが必要ですが、サンプルですので、その辺りの実装は省略しています。また、不特定の方がサンプル動作を確認することを考慮して、格納した情報は、その画面上でしかダウンロードや閲覧できない実装としていますので了承ください。実際に操作した時には、最後に初期化操作をすれば、全て削除します。また、一定期間経過すると、cronで削除します。
サンプル画面は、開いた時点では、何もファイルが格納されていませんので、格納操作ボタンを押して、格納画面を開いてください。格納画面は、window機能で子画面として表示しています。
格納操作画面は、以下のような画面です
ファイルを格納する操作では、事前に同じファイルが存在するかチェックする操作と、上書き保存も許容する操作が選択できます。初期値は、上書き格納です。
格納ファイル指定ボタンを押すと、以下のような画面になります。(例は、iPhoneイメージです。PCでは、ファイル選択ダイアログが表示されます。)
カメラでレシートを撮影している場合は、写真ライブラリを、PDFなどで領収書を保存してしている場合は、ファイルを選択操作で、画像ファイルを選択します。選択操作が完了すると、画面にファイル名がリストアップされます。
記事では、スマホ操作の画面を表示していますので、PC操作の場合は、想定して操作してみてください。以下は、写真画像を選択した時のリスト例です。
ファイルを再選択したい場合は、バツ(X)アイコンクリックしてでクリアできるので、再度、格納ファイル指定で、ファイルを選択してください。この時点ででは、まだサーバには転送していません。格納操作ボタンクリックで、指定したファイルをサーバに転送して格納します。サンプル環境では、大きいサイズのファイルはアップロードできませんので注意してください。スマホ撮影の写真はアップロードできます。格納操作は、少し時間がかかります。(実際のアプリでは、アイコンを変更するなどして、実行中のイメージを表現できます)
正常に格納操作ができると、以下のようなポップアップ表示が出ます。
複数の画像をアプロードしたい場合は、同じ操作を繰り返してください。
アップロード操作が完了したら閉じるボタンクリックで、一覧表示に画面は戻ります。その時点でサーバ内の格納ファイルの一覧を取得して一覧表に反映します。以下、1ファイル格納した例です。
ファイル名とファイルの識別子が表示されます。
再度、格納操作をクリックすれば、追加でファイルを格納できます。
一覧からファイル名(行)を選択すると、ファイルの識別子におうじて、ビューワー画面か、ダウンロード操作が開始されます。
例では、画像ファイルなのでブラウザの画面になります。
PCでは、別タブで画面が開きますが、スマホでは、画像閲覧ページが開いて、そちらだけ表示されますので、確認後は、戻るアイコン操作で、一覧に戻れます。(閲覧画面を別ページでなく、windowsで表示する実装も可能ですが)
尚、保管したファイルは、初期化ボタンでクリアしてください。
一度、画面を閉じると、格納したファイルには、アクセスできない実装にしていますので、ご了承願います。(cronで一定期間経過後には、削除しますので、安心してください。)尚、アップロードする画像やPDFは、個人情報を含まない、かつ、閲覧されても問題ない情報で操作してください。サンプル公開環境であることを理解の上、操作してください。実際に電子帳簿保存サービスを実装するときには、ログイン機能などを使って、特定のユーザしか格納や閲覧ができない実装にする必要があります。
格納は、社外でも可能にして、閲覧やダウンロードは、社内環境のみ可能にする実装や、権限のある方だけ、閲覧できるようにする実装も可能です。
今回のサンプルでは、いくつかのファイルに分割して実装しています。
画面は、2つ(一覧画面とアップロード操作画面)
サーバ側は、一覧表示用のAPI,アプロード用API,初期化(削除)用APIの3つで実装しました。
以下、サンプルソースです。一覧画面:UD0010_upload_lists.php
<?php
//UD0010_upload_lists.php
//https://yamasanfarm.sakuraweb.com/webix01/view/UD0010/UD0010_upload_lists.php
$TITLE_INFO ="UploadList";
$VER_INFO ="V01L01";
$myfilename = basename(__FILE__);
define('ROOT_PATH','/home/sunsun/www/webix01'); //ソースを保存しているパス(動作環境に応じて記述する必要あり)
define('SUB_FOLDER','/webix01'); //サブフォルダを指定したURL
$userid = 'admin';
$logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
error_log($logheader.' start '.$myfilename);
if($_SERVER["REQUEST_METHOD"] != "GET"){
error_log($logheader.' REQUEST_METHOD not GET');
header("HTTP/1.0 404 Not Found");
return;
}
function random($length = 4)
{
return substr(str_shuffle(str_repeat('0123456789abcdefghijklmnopqrstuvwxyz', $length)), 0, $length);
}
$userid = 'u'.random();
$logheader = $userid.', '.$myfilename.':';
error_log($logheader."userid =".$userid);
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="<?php echo SUB_FOLDER; ?>/webix_GPL_1020/webix.css" type="text/css" charset="utf-8">
<script src="<?php echo SUB_FOLDER; ?>/webix_GPL_1020/webix.js"></script>
<link href="<?php echo SUB_FOLDER; ?>/webix_GPL_1020/skins/compact.css?<?php echo date('Ymd-H'); ?>" rel="stylesheet" type="text/css">
<link rel="icon" href="<?php echo SUB_FOLDER; ?>/image/webix_64.ico">
<script src="<?php echo SUB_FOLDER; ?>/commonlib/moment-with-locales.js"></script>
<title><?php echo $TITLE_INFO.' ('.$VER_INFO.')' ?></title>
<style>
</style>
<title><?php echo $TITLE_INFO.' ('.$VER_INFO.')' ?></title>
</head>
<body>
<script type="text/javascript" charset="utf-8">
webix.i18n.setLocale("ja-JP");
var userid = '<?php echo $userid; ?>';
<?php
require './UD0013_upload_win.php';
?>
function get_file_lists(){
$$("table01").clearAll();
var send_prm = {};
send_prm.userid = userid;
var folder_info = userid;
send_prm.folder_info = folder_info;
var xhr =webix.ajax().sync().get("<?php echo SUB_FOLDER; ?>/rest_api/UD0010/UD0014_get_filelists.php",send_prm);
var resp = xhr.responseText;
var resp_info = JSON.parse(resp);
var resp = resp_info["resp"];
if(resp_info["resp"] == "ok"){
$$("table01").parse(resp_info.file_array);
var array_count = resp_info.file_array.length;
$$("lists_count").setValue(array_count);
if(array_count > 0){
webix.message({type:"success",text:"格納ファイルを読出ししました。"});
}
else{
webix.alert("検索結果は0件です。");
}
}
else{
webix.message({type:"error",text:"格納ファイルを確認できませんでした。"});
}
}
//画面の初期化
webix.ui({ padding: 10,
rows:[
{view:"label", template:"<span style='font-weight:bold; font-size:180%;'>格納ファイル一覧[UD0010]<?php echo' ('.$VER_INFO.')' ?></span>"},
{ margin:5,
cols:[
{view:"text", label:"件数", labelWidth:50,name:"lists_count",id:"lists_count",value:0,width:100, labelAlign:"right",readonly:true,inputAlign:"right"},
{view:"button",label: "検索", id:"search_btn",name:"search_btn", width: 80
,click:function(){
get_file_lists();
}
},
{view:"button",label: "格納操作", id:"upload_btn",name:"upload_btn", width: 100
,click:function(){
upload_form_win.show();
}
},
{view:"button",label: "初期化", id:"delete_btn",name:"delete_btn", width: 80, css:"webix_danger"
,click:function(){
webix.confirm({
title:"確認",
ok:"はい",
cancel:"いいえ",
text:"格納ファイルを全て削除しますか?"
})
.then(function(result){ //はいのとき
var formData = new FormData();
var folder_info = userid;
formData.append("userid", userid);
formData.append("folder_info",folder_info);
var xhr =webix.ajax().sync().post("<?php echo SUB_FOLDER; ?>/rest_api/UD0010/UD0016_delete_files.php",formData);
var resp = JSON.parse(xhr.responseText);
if(resp.resp =="ok"){
get_file_lists();
webix.message({type:"success",text:"格納ファイルを削除しました。"});
return;
}
else{
webix.alert("格納ファイルの削除でエラーしました。 code="+resp_info.error_code);
return;
}
})
.fail(function(){ //いいえの時
webix.message({type:"debug",text:"操作をキャンセルしました。"});
});
}
},
]
},
{view:"datatable", id:"table01",
columns:[
{ id:"id" ,header:"id" ,width:40 , css:{"text-align":"right"},sort:"int"},
{ id:"filename" , header:["ファイル名",{content:"textFilter"} ] ,width:280,sort:"string"},
{ id:"extension" , header:["種別",{content:"selectFilter"} ] ,width:80,sort:"string"},
],
data: [],
resizeColumn:true,
select:"row",
clipboard:true,
on:{
"onItemClick":function(id){
//対象行をクリックしたときの動作(対象ファイルをダウンロードする)
var item = this.getItem(id);
if(item.extension == "pdf" || item.extension == "jpg" || item.extension == "JPG" || item.extension == "jpeg"|| item.extension == "png"){
//ビューワ表示
var url_info = "<?php echo SUB_FOLDER; ?>/folder/"+userid+"/"+item.filename;
var new_window = window.open(url_info, '_blank');
if(new_window){
//正常にOPEN
//何もしない
}
else{
webix.alert({
title:"表示エラー",
ok:"確認",
text:"ファイルが取得できません。",
type:"alert-error"
});
}
}
else{
//ダウンロード
webix.ajax().response("blob").get("<?php echo SUB_FOLDER; ?>/folder/"+userid+"/"+item.filename, function(text, data, xhr) {
//console.log(xhr.getResponseHeader("Content-Disposition"));
webix.html.download(data, item.filename);
});
webix.alert("ダウンロードが完了しました。");
}
}
}
}
]
});
webix.ui.fullScreen();
</script>
</body>
</html>
アップロード画面:UD0013_upload_win.php
<?php
//UD0013_upload_win.php
?>
var upload_filename = "";
var upload_filename_array = [];
var result_upload_filename = "";
let upload_mode_options = [
{ "id":1, "value":"上書き格納" },
{ "id":2, "value":"上書きチェック" }
];
//個別編集画面
var upload_form_win = webix.ui({
view:"window",
id:"upload_form_win",
move:true,
resize:true,
width: 600,
height: 500,
left: 10,
top: 100,
head:{
cols:[
{template:"画像アップロード", type:"header", borderless:true},
{view:"icon", icon:"wxi-close", tooltip:"画面を閉じます", click: function(){
upload_form_win.hide();
}}
]
},
body:{view:"form",
rows: [
{ view:"label", height:30, template:"<span style='font-weight:bold; font-size:150%;'>画像アップロード</span>",width:360},
{view: "template",
template: "レシートなど撮影した画像をアップロードします"+
"<br>重複しないファイルを指定してください"+
"<br>格納モードは、上書きと事前確認が選択可能です"+
"<br>格納ファイルは、一度閉じると閲覧できません。"
, height:100,width:200},
{view:"select", label:"格納モード",value:1, options:upload_mode_options, name:"upload_mode",id:"upload_mode" ,width: 250,labelWidth:250,labelPosition:"top"},
{view:"uploader", id:"uploader1",name:"uploader1",width:150,
autosend:false,
multiple:false,
value: '格納ファイル指定',
link:"upload_list",
upload:"<?php echo SUB_FOLDER; ?>/rest_api/UD0010/UD0012_upload_action.php",
on: {
onFileUpload: function(response){
//webix.alert("格納処理成功");
},
onFileUploadError: function(response){
let resp = JSON.parse(response.xhr.response);
if(resp.error_code == -1){
webix.alert("格納先に同一ファイルが<br>存在します。<br> error code=" + resp.error_code);
}
else{
webix.alert("格納処理が失敗しました。<br>error code=" + resp.error_code);
}
},
}
},
{view:"list", id:"upload_list", type:"uploader",width:200,autoheight:true, borderless:true},
{margin:5,
cols:[
{ view: "button", label: "格納操作", width:150,
click: function() {
var folder_info = userid;
var upload_mode = $$("upload_mode").getValue();
var file_count = 0;
var replace_count = 0;
$$("uploader1").files.data.each(function(obj,index){
if (obj.status == "client")
{
upload_filename = obj.name;
if(upload_filename.indexOf('/') > -1){
file_count = -1;
}
else if(upload_filename.indexOf('(')> -1 || upload_filename.indexOf(')')> -1 || upload_filename.indexOf(' ')> -1 ){
upload_filename = upload_filename .replace(/ /g, '');
upload_filename = upload_filename .replace(/\)/g, '_');
upload_filename = upload_filename .replace(/\(/g, '_');
obj.formData = { upload_filename:upload_filename ,upload_mode:upload_mode ,folder_info:folder_info};
replace_count = 1;
file_count += 1;
}
else{
obj.formData = { upload_filename:upload_filename ,upload_mode:upload_mode ,folder_info:folder_info};
file_count += 1;
}
}
});
if(file_count == 0){
webix.alert("アップロードする画像が<br>存在しません。");
return;
}
else if(file_count == -1){
webix.alert("ファイル名に/が含まれています。<br>事前にファイル名を変更してください。");
return;
}
$$("upload_filename").setValue(upload_filename);
$$("uploader1").send(function(response){
if(response){
if(response.status == "server"){
if(replace_count == 1){
webix.alert("指定ファイル名に空白やカッコ()が含まれていたので、削除・アンダーラインに変更して格納しました。<br>変更後のファイル名<br>"+upload_filename+"<br>別ファイルを再度、格納する場合は、格納ファイル指定から実施してください。");
}
else{
$$("upload_list").clearAll();
$$("upload_list").refresh();
webix.alert(upload_filename+"の格納操作が完了しました。<br>別ファイルを再度、格納する場合は、格納ファイル指定から実施してください。");
}
}
else{
webix.alert(upload_filename+"の<br>格納操作に失敗しました。");
}
}
else{
webix.alert("アップロードに失敗しました。");
}
});
}
},
{ view: "button", label: "閉じる", width:150,
click: function() {
upload_form_win.hide();
get_file_lists();
}
}
]
},
{view:"text",label:"格納ファイル名", id:"upload_filename",name:"upload_filename",value:"",labelWidth: 150,width:250, labelPosition:"top",hidden:true}
],
scroll:"y"
}
});
webix.ui.fullScreen();
一覧表API:UD0014_get_filelists.php
<?php
//UD0014_get_filelists.php
// UIからのリクエストを受信し、フォルダ内のファイル一覧をJSONデータで返す
//
$FUNC_INFO = "UD0014";
$VER_INFO ="V01L01";
$myfilename = basename(__FILE__); //自分自身のファイル名取得
define('SUB_FOLDER','/webix01');
$base_folder = '/home/sunsun/www/webix01/folder/';
$userid = 'admin';
$logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
if($_SERVER["REQUEST_METHOD"] != "GET"){
//GET以外ははじく
header("HTTP/1.0 404 Not Found");
return;
}
if(isset($_GET['userid'])){
$userid = $_GET['userid'];
}
$logheader = 'userid='.$userid.', '.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名,ログインIDを付与)
$folder_info = "";
if(isset($_GET['folder_info'])){
$folder_info = $_GET['folder_info'];
}
//
//メインルーチン
//
error_log($logheader.' rest api request to '.$myfilename.' search folder='.$base_folder.$folder_info.'/*');
$result = glob($base_folder.$folder_info.'/*');
error_log($logheader.' file count='.count($result));
//
$file_array = array();
for($i=0;$i<count($result);$i++){
$file_array[$i]["id"] = $i+1;
$file_array[$i]["filename"] = basename($result[$i]);
$file_array[$i]["extension"] =pathinfo($result[$i])['extension'];
}
$resp = "ok";
$error_code = 0;
//compact関数とjson_encode関数で、JSON形式の文字列に変換
$json_data = json_encode(compact("resp","error_code","file_array"),JSON_UNESCAPED_UNICODE);
echo $json_data; //結果をecho関数で出力
?>
アップロード操作API:UD0012_upload_action.php
<?php
header("Content-Type: text/javascript; charset=utf-8");
//UD0012_upload_action.php
$myfilename = basename(__FILE__); //自分自身のファイル名取得
$logheader = 'log:'.$myfilename.':';//ログ出力時のヘッダー情報(自ファイル名を付与)
if($_SERVER["REQUEST_METHOD"] != "POST"){
error_log($logheader."REQUEST_METHOD NOT POST");
header("HTTP/1.0 404 Not Found");
return;
}
error_log($logheader."start UD0012_upload_action.php");
$base_folder = '/home/sunsun/www/webix01/folder/';
function random($length = 8)
{
return substr(str_shuffle(str_repeat('0123456789abcdefghijklmnopqrstuvwxyz', $length)), 0, $length);
}
$upload_filename = "";
if(isset($_POST['upload_filename'])){
$upload_filename = $_POST['upload_filename'];
}
error_log($logheader."upload_filename =".$upload_filename);
$upload_mode = "1";
if(isset($_POST['upload_mode'])){
$upload_mode = $_POST['upload_mode'];
}
error_log($logheader."upload_mode =".$upload_mode);
$folder_info = "admin";
if(isset($_POST['folder_info'])){
$folder_info = $_POST['folder_info'];
}
error_log($logheader."folder_info =".$folder_info);
//格納先フォルダの有無確認
if(file_exists($base_folder.$folder_info)){
error_log($logheader."folder=".$base_folder.$folder_info.' は存在');
}else{
//存在しないときの処理
mkdir($base_folder.$folder_info, 0777);
error_log($logheader."create folder=".$base_folder.$folder_info);
}
if(isset($_FILES['upload'])){
$file = $_FILES['upload'];
//getting a file object
$filename_tmp = $file["name"];
error_log($logheader."filename =".$filename_tmp);
//$filename_tmp = "test.jpg";
if($upload_filename != ""){
$filename = $base_folder.$folder_info."/".preg_replace("|[\\\/]|", "",$upload_filename);
}
else{
$filename = $base_folder.$folder_info."/".preg_replace("|[\\\/]|", "",$filename_tmp);
}
error_log($logheader."filename =".$filename);
if($upload_mode == "2"){
if(file_exists($filename)){
$res = array("status" => "error","error_code" => -1);
echo json_encode($res);
exit();
}
}
else{
if(file_exists($filename)){
if (unlink($filename)){ //ファイル削除
error_log($logheader."file deleted file=".$filename);
}
}
}
error_log($logheader."move_uploaded_file:".$file["tmp_name"] );
error_log($logheader."move_type:".$file["type"] );
error_log($logheader."move_uploaded_name:".$file["name"] );
error_log($logheader."move_error:".$file["error"] );
error_log($logheader."move_size:".$file["size"] );
move_uploaded_file($file["tmp_name"], $filename);
$res = array("status" => "server", "error_code" => 0);
echo json_encode($res);
exit;
}
else{
error_log($logheader." not foud upload file error_code:-1");
$res = array("status" => "error","error_code" => -2);
//$res = array("status" => "server");
echo json_encode($res);
}
?>
削除APIは、記載を省略します。
基本機能だけ紹介しましたが、ログイン画面やメニュー画面、管理用のデータベースを実装すれば、十分、電子帳簿保存サービスにすることができます。PDFの場合、PDFを格納して、金額などを抽出する実装もできます。
相手先をデータベース化すれば、管理もしやすくなります。
機会をみて、電子帳簿系の業務アプリの実装を紹介してゆきます。