Implement server scases data synchronization
This commit is contained in:
parent
c7796ac341
commit
67a89bb091
20 changed files with 1265 additions and 126 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
/config.local.yml
|
||||||
# Ignore vim temporary/backup files
|
# Ignore vim temporary/backup files
|
||||||
*~
|
*~
|
||||||
.*.swp
|
.*.swp
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"license": "GPL3",
|
"license": "GPL3",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Brenard\\Mysc\\": "src/"
|
"MySC\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
|
@ -22,5 +22,8 @@
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"php-http/discovery": true
|
"php-http/discovery": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "2.0.x-dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
config.yml
30
config.yml
|
@ -112,12 +112,12 @@ session:
|
||||||
#
|
#
|
||||||
db:
|
db:
|
||||||
# Sqlite
|
# Sqlite
|
||||||
#dsn: "sqlite:${data_directory}/db.sqlite3"
|
dsn: "sqlite:${data_directory}/db.sqlite3"
|
||||||
#options: null
|
options: null
|
||||||
|
|
||||||
# Date/Datetime format in database (strptime format)
|
# Date/Datetime format in database (strptime format)
|
||||||
#date_format: '%s'
|
date_format: '%s'
|
||||||
#datetime_format: '%s'
|
datetime_format: '%s'
|
||||||
|
|
||||||
# Postgresql
|
# Postgresql
|
||||||
#dsn: "pgsql:host=localhost;port=5432;dbname=items"
|
#dsn: "pgsql:host=localhost;port=5432;dbname=items"
|
||||||
|
@ -144,19 +144,7 @@ db:
|
||||||
#
|
#
|
||||||
auth:
|
auth:
|
||||||
# Enabled authentication
|
# Enabled authentication
|
||||||
enabled: false
|
enabled: true
|
||||||
|
|
||||||
# Methods to authenticate users
|
|
||||||
methods:
|
|
||||||
- form
|
|
||||||
- http
|
|
||||||
#- cas
|
|
||||||
|
|
||||||
# User backends
|
|
||||||
backends:
|
|
||||||
#- ldap
|
|
||||||
#- db
|
|
||||||
#- casuser
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Login form
|
# Login form
|
||||||
|
@ -257,14 +245,6 @@ auth:
|
||||||
# Database user backend
|
# Database user backend
|
||||||
#
|
#
|
||||||
db:
|
db:
|
||||||
# DSN (required)
|
|
||||||
dsn: "${db.dsn}"
|
|
||||||
# Username (optional but could be required with some PDO drivers)
|
|
||||||
user: "${db.user}"
|
|
||||||
# Password (optional)
|
|
||||||
password: "${db.password}"
|
|
||||||
# PDO options (optional)
|
|
||||||
options: "${db.options}"
|
|
||||||
# Users table name (optional, default: users)
|
# Users table name (optional, default: users)
|
||||||
#users_table: "users"
|
#users_table: "users"
|
||||||
# Username field name (optional, default: username)
|
# Username field name (optional, default: username)
|
||||||
|
|
1
data/.gitignore
vendored
Normal file
1
data/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
db.sqlite3
|
45
data/sqlite.init-db.sql
Normal file
45
data/sqlite.init-db.sql
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
CREATE TABLE users (
|
||||||
|
username text NOT NULL PRIMARY KEY,
|
||||||
|
name text COLLATE NOCASE NOT NULL,
|
||||||
|
mail text COLLATE NOCASE,
|
||||||
|
password text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users (username, name, mail, password) VALUES (
|
||||||
|
"admin", "Administrator", "admin@example.com",
|
||||||
|
"$argon2id$v=19$m=65536,t=4,p=1$WTQ0di44NW11MUJ1b3RMQw$+LRAQRaIXE2jhfavNFNuxnEtEUT6tEBz/98pTtD0EnM"
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE auth_tokens (
|
||||||
|
token text NOT NULL PRIMARY KEY,
|
||||||
|
username text NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
creation_date REAL,
|
||||||
|
expiration_date REAL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE scases (
|
||||||
|
uuid text NOT NULL PRIMARY KEY,
|
||||||
|
username text NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
last_change REAL NOT NULL,
|
||||||
|
removed INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE categories (
|
||||||
|
uuid text NOT NULL PRIMARY KEY,
|
||||||
|
scase_uuid text NOT NULL REFERENCES scases(uuid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
color text NOT NULL,
|
||||||
|
last_change REAL NOT NULL,
|
||||||
|
removed INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE things (
|
||||||
|
uuid text NOT NULL PRIMARY KEY,
|
||||||
|
category_uuid text NOT NULL REFERENCES categories(uuid) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
label text NOT NULL,
|
||||||
|
nb INTEGER NOT NULL,
|
||||||
|
checked INTEGER NOT NULL,
|
||||||
|
last_change REAL NOT NULL,
|
||||||
|
removed INTEGER NOT NULL
|
||||||
|
);
|
|
@ -1,9 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use EesyPHP\App;
|
use EesyPHP\App;
|
||||||
|
use EesyPHP\Db;
|
||||||
use EesyPHP\I18n;
|
use EesyPHP\I18n;
|
||||||
use EesyPHP\SentrySpan;
|
use EesyPHP\SentrySpan;
|
||||||
|
|
||||||
|
use MySC\Auth\API;
|
||||||
|
|
||||||
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
|
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
|
||||||
|
|
||||||
// Root directory path
|
// Root directory path
|
||||||
|
@ -38,8 +41,25 @@ App::init(
|
||||||
"$root_dir_path/static"
|
"$root_dir_path/static"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
'auth' => array(
|
||||||
|
'enabled' => true,
|
||||||
|
'methods' => array(
|
||||||
|
'\\MySC\\Auth\\API',
|
||||||
|
),
|
||||||
|
'backends' => array(
|
||||||
|
'db',
|
||||||
|
),
|
||||||
|
),
|
||||||
'default' => array(
|
'default' => array(
|
||||||
// Set here your configuration parameters default value
|
'auth' => array(
|
||||||
|
'enabled' => true,
|
||||||
|
'methods' => array(
|
||||||
|
'\\MySC\\Auth\\API',
|
||||||
|
),
|
||||||
|
'auth_token' => array(
|
||||||
|
'expiration_delay' => 31536000,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
$root_dir_path
|
$root_dir_path
|
||||||
|
@ -48,7 +68,8 @@ App::init(
|
||||||
$sentry_span = new SentrySpan('core.init', 'Core initialization');
|
$sentry_span = new SentrySpan('core.init', 'Core initialization');
|
||||||
|
|
||||||
// Put here your own initialization stuff
|
// Put here your own initialization stuff
|
||||||
|
Db :: init();
|
||||||
|
API :: init();
|
||||||
require 'views/index.php';
|
require 'views/index.php';
|
||||||
|
|
||||||
$sentry_span->finish();
|
$sentry_span->finish();
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use EesyPHP\Auth;
|
||||||
|
use EesyPHP\Db;
|
||||||
|
use EesyPHP\Log;
|
||||||
use EesyPHP\Tpl;
|
use EesyPHP\Tpl;
|
||||||
use EesyPHP\Url;
|
use EesyPHP\Url;
|
||||||
|
|
||||||
|
use MySC\Db\Category;
|
||||||
|
use MySC\Db\SCase;
|
||||||
|
use MySC\Db\Thing;
|
||||||
|
|
||||||
|
use function EesyPHP\vardump;
|
||||||
|
use function EesyPHP\generate_uuid;
|
||||||
|
|
||||||
|
if (php_sapi_name() == "cli")
|
||||||
|
return true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to homepage
|
* Redirect to homepage
|
||||||
* @param EesyPHP\UrlRequest $request
|
* @param EesyPHP\UrlRequest $request
|
||||||
|
@ -11,7 +24,7 @@ use EesyPHP\Url;
|
||||||
function handle_redirect_homepage($request) {
|
function handle_redirect_homepage($request) {
|
||||||
Url::redirect("home");
|
Url::redirect("home");
|
||||||
}
|
}
|
||||||
Url :: add_url_handler(null, 'handle_redirect_homepage');
|
Url :: add_url_handler(null, 'handle_redirect_homepage', null, false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Homepage
|
* Homepage
|
||||||
|
@ -21,7 +34,7 @@ Url :: add_url_handler(null, 'handle_redirect_homepage');
|
||||||
function handle_homepage($request) {
|
function handle_homepage($request) {
|
||||||
Tpl :: display("index.tpl");
|
Tpl :: display("index.tpl");
|
||||||
}
|
}
|
||||||
Url :: add_url_handler("#^home$#", 'handle_homepage');
|
Url :: add_url_handler("#^home$#", 'handle_homepage', null, false);
|
||||||
|
|
||||||
function _list_static_directory_files($root_dir, $dir, &$result, &$last_updated) {
|
function _list_static_directory_files($root_dir, $dir, &$result, &$last_updated) {
|
||||||
foreach (array_diff(scandir($dir), array('.','..')) as $file) {
|
foreach (array_diff(scandir($dir), array('.','..')) as $file) {
|
||||||
|
@ -61,4 +74,88 @@ function handle_cache_manifest($request) {
|
||||||
}
|
}
|
||||||
Url :: add_url_handler("#^cache\.manifest$#", 'handle_cache_manifest');
|
Url :: add_url_handler("#^cache\.manifest$#", 'handle_cache_manifest');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data
|
||||||
|
* @param EesyPHP\UrlRequest $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function handle_sync($request) {
|
||||||
|
$data = json_decode($_POST["data"], true);
|
||||||
|
Log::debug("Sync scases data: %s", vardump($data["scases"]));
|
||||||
|
$user = Auth::user();
|
||||||
|
$updated = false;
|
||||||
|
$db_scases = SCase :: list(['username' => $user -> username]);
|
||||||
|
foreach($data["scases"] as $scase_uuid => $scase_data) {
|
||||||
|
Log::debug("sync(): scase %s", $scase_uuid);
|
||||||
|
$scase = Scase :: from_json($scase_data, $user);
|
||||||
|
if (array_key_exists($scase_uuid, $db_scases)) {
|
||||||
|
Log::debug("sync(): scase %s exist in DB", $scase_uuid);
|
||||||
|
$db_scases[$scase_uuid] -> sync($scase, $updated);
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
$db_categories = $db_scases[$scase_uuid] -> categories();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log::debug("sync(): scase %s does not exist in DB, create it", $scase_uuid);
|
||||||
|
$scase -> save();
|
||||||
|
$updated = true;
|
||||||
|
$db_categories = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($scase_data['cats'] as $category_uuid => $category_data) {
|
||||||
|
Log::debug("sync(): scase %s / category %s", $scase_uuid, $category_uuid);
|
||||||
|
$category = Category :: from_json($category_data, $scase);
|
||||||
|
if (array_key_exists($category_uuid, $db_categories)) {
|
||||||
|
Log::debug("sync(): scase %s / category %s exists in DB, sync it", $scase_uuid, $category_uuid);
|
||||||
|
$db_categories[$category_uuid] -> sync($category, $updated);
|
||||||
|
$db_things = $db_categories[$category_uuid] ->things();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log::debug("sync(): scase %s / category %s does not exists in DB, create it", $scase_uuid, $category_uuid);
|
||||||
|
$category -> save();
|
||||||
|
$updated = true;
|
||||||
|
$db_things = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($category_data['things'] as $thing_uuid => $thing_data) {
|
||||||
|
$thing = Thing :: from_json($thing_data, $category);
|
||||||
|
if (array_key_exists($thing_uuid, $db_things)) {
|
||||||
|
$db_things[$thing_uuid] -> sync($thing, $updated);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$thing -> save();
|
||||||
|
$updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
"scases" => [],
|
||||||
|
"updated" => $updated,
|
||||||
|
];
|
||||||
|
foreach (SCase :: list(['username' => $user -> username]) as $uuid => $scase) {
|
||||||
|
$data["scases"][$uuid] = $scase -> to_json();
|
||||||
|
}
|
||||||
|
Tpl::display_ajax_return($data);
|
||||||
|
}
|
||||||
|
Url :: add_url_handler("#^sync$#", 'handle_sync', null, true, true, true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support info page
|
||||||
|
* @param EesyPHP\UrlRequest $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function handle_support_info($request) {
|
||||||
|
if (isset($_REQUEST['download'])) {
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
header('Content-disposition: attachment; filename="'.date('Ymd-His-').Auth::user()->username.'-aide-support.txt"');
|
||||||
|
}
|
||||||
|
Tpl :: display(
|
||||||
|
isset($_REQUEST['download'])?
|
||||||
|
'support_info_content.tpl':"support_info.tpl",
|
||||||
|
"Page d'aide à l'assistance utilisateurs"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Url :: add_url_handler('|^support/?$|', 'handle_support_info');
|
||||||
|
|
||||||
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|
||||||
|
|
14
phpstan.neon
Normal file
14
phpstan.neon
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
paths:
|
||||||
|
- bin
|
||||||
|
- includes
|
||||||
|
- src
|
||||||
|
- public_html
|
||||||
|
universalObjectCratesClasses:
|
||||||
|
- EesyPHP\HookEvent
|
||||||
|
- EesyPHP\UrlRequest
|
||||||
|
- EesyPHP\Auth\User
|
||||||
|
- EesyPHP\Db\DbObject
|
||||||
|
bootstrapFiles:
|
||||||
|
- includes/core.php
|
128
src/Auth/API.php
Normal file
128
src/Auth/API.php
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Auth;
|
||||||
|
|
||||||
|
use EesyPHP\Auth;
|
||||||
|
use EesyPHP\Auth\Method;
|
||||||
|
use EesyPHP\Log;
|
||||||
|
use EesyPHP\Url;
|
||||||
|
use EesyPHP\Tpl;
|
||||||
|
|
||||||
|
use MySC\Db\AuthToken;
|
||||||
|
|
||||||
|
use function EesyPHP\vardump;
|
||||||
|
|
||||||
|
class API extends Method {
|
||||||
|
|
||||||
|
protected static $initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
if (php_sapi_name() != "cli" && !self :: $initialized) {
|
||||||
|
Url :: add_url_handler(
|
||||||
|
'#^login$#',
|
||||||
|
['MySC\\Auth\\API', 'handle_login'],
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
self :: $initialized = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user
|
||||||
|
* @param bool $force Force user authentication
|
||||||
|
* @return \EesyPHP\Auth\User|null
|
||||||
|
*/
|
||||||
|
public static function login($force=false) {
|
||||||
|
$user = null;
|
||||||
|
if (isset($_REQUEST['username']) && isset($_REQUEST['password'])) {
|
||||||
|
Log::debug("API::login(): Try to authenticate user by username & password");
|
||||||
|
$user = Auth :: authenticate($_REQUEST['username'], $_REQUEST['password']);
|
||||||
|
if (!$user)
|
||||||
|
Tpl :: add_error(_('Invalid username or password.'));
|
||||||
|
}
|
||||||
|
elseif (isset($_REQUEST['username']) && isset($_REQUEST['token'])) {
|
||||||
|
Log::debug("API::login(): Try to authenticate user by token");
|
||||||
|
$auth_token = AuthToken :: get($_REQUEST['token']);
|
||||||
|
Log::debug("API::login(): auth token = %s", vardump($auth_token));
|
||||||
|
if (!$auth_token || $auth_token->username != $_REQUEST['username'])
|
||||||
|
Tpl :: add_error(_('Invalid authentication token.'));
|
||||||
|
elseif ($auth_token->expired()) {
|
||||||
|
Tpl :: add_error(_('Authentication token expired.'));
|
||||||
|
$auth_token->delete();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log::debug("API::login(): auth token valid, retrieve corresponding user");
|
||||||
|
$user = Auth::get_user($auth_token->username);
|
||||||
|
if (!$user) {
|
||||||
|
Tpl :: add_error(_('Your account appears to have been deleted.'));
|
||||||
|
$auth_token->delete();
|
||||||
|
}
|
||||||
|
Log::debug("login(): authenticated as %s via token", $user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The login form view
|
||||||
|
* @param \EesyPHP\UrlRequest $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle_login($request) {
|
||||||
|
$user = Auth :: login(false, '\\mysc\\auth\\api');
|
||||||
|
if ($user)
|
||||||
|
Tpl :: display_ajax_return([
|
||||||
|
"success" => true,
|
||||||
|
"token" => AuthToken :: create($user) -> token,
|
||||||
|
"username" => $user -> username,
|
||||||
|
"name" => $user->name,
|
||||||
|
"mail" => $user->mail,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Tpl :: display_ajax_return([
|
||||||
|
"success" => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logout method
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function logout() {
|
||||||
|
Log::debug("API::logout()");
|
||||||
|
if (isset($_REQUEST['token'])) {
|
||||||
|
$auth_token = AuthToken :: get($_REQUEST['token']);
|
||||||
|
Log::debug("API::logout(): auth token = %s", vardump($auth_token));
|
||||||
|
if (!$auth_token) {
|
||||||
|
Log::debug("API::logout(): unknown auth token");
|
||||||
|
}
|
||||||
|
else if (isset($_REQUEST['username']) && $auth_token->username != $_REQUEST['username']) {
|
||||||
|
Log::warning(
|
||||||
|
"API::logout(): bad auth token owner ('%s' vs '%s')",
|
||||||
|
$auth_token->username,
|
||||||
|
$_REQUEST['username']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if ($auth_token->delete()) {
|
||||||
|
Log::debug("API::logout(): auth token deleted");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log::error("API::logout(): error deleting the auth token %s", $_REQUEST['token']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log::warning("API::logout(): no token provided by the request");
|
||||||
|
}
|
||||||
|
Tpl :: display_ajax_return([
|
||||||
|
"success" => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
55
src/Db/AuthToken.php
Normal file
55
src/Db/AuthToken.php
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\App;
|
||||||
|
use EesyPHP\Auth;
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
use EesyPHP\Db\AttrTimestamp;
|
||||||
|
use EesyPHP\Db\DbObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySC database object
|
||||||
|
* @property string|null $token
|
||||||
|
* @property string|null $username
|
||||||
|
* @property \DateTime|null $creation_date
|
||||||
|
* @property \DateTime|null $expiration_date
|
||||||
|
* @property boolean $removed
|
||||||
|
*/
|
||||||
|
class AuthToken extends DbObject {
|
||||||
|
protected const TABLE = 'auth_tokens';
|
||||||
|
protected const PRIMARY_KEYS = ['token'];
|
||||||
|
protected const DEFAULT_ORDER = 'creation_date';
|
||||||
|
protected const DEFAULT_ORDER_DIRECTION = 'DESC';
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return [
|
||||||
|
'token' => new AttrStr(['required' => true, 'default' => '\\EesyPHP\\generate_uuid']),
|
||||||
|
'username' => new AttrStr(['required' => true]),
|
||||||
|
'creation_date' => new AttrTimestamp(['default' => 'time']),
|
||||||
|
'expiration_date' => new AttrTimestamp(
|
||||||
|
['default' => ['\\MySC\\Db\\AuthToken', 'generate_expiration_date']]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generate_expiration_date() {
|
||||||
|
return time() + App::get('auth.auth_token.expiration_delay', null, 'int');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create($user=null) {
|
||||||
|
$user = $user ? $user : Auth::user();
|
||||||
|
$token = new AuthToken();
|
||||||
|
$token -> username = $user->username;
|
||||||
|
$token->save();
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user() {
|
||||||
|
return User::get($this -> username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function expired() {
|
||||||
|
return $this -> expiration_date -> getTimestamp() <= time();
|
||||||
|
}
|
||||||
|
}
|
56
src/Db/Category.php
Normal file
56
src/Db/Category.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCase category database object
|
||||||
|
* @property string $uuid
|
||||||
|
* @property string $scase_uuid
|
||||||
|
* @property string $name
|
||||||
|
* @property string $color
|
||||||
|
* @property \DateTime|null $last_change
|
||||||
|
* @property boolean $removed
|
||||||
|
*/
|
||||||
|
class Category extends DbObject {
|
||||||
|
protected const TABLE = "categories";
|
||||||
|
protected const DEFAULT_ORDER = "name";
|
||||||
|
protected const DEFAULT_ORDER_DIRECTION = "ASC";
|
||||||
|
protected const POSSIBLE_ORDERS = ["name", "last_change"];
|
||||||
|
protected const UPDATABLE_FIELDS = ["name", "color"];
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return array_merge(
|
||||||
|
parent :: get_schema(),
|
||||||
|
[
|
||||||
|
"scase_uuid" => new AttrStr(["required" => true]),
|
||||||
|
"name" => new AttrStr(["required" => true]),
|
||||||
|
"color" => new AttrStr(["required" => true]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function to_json() {
|
||||||
|
$data = parent :: to_json();
|
||||||
|
$data["things"] = [];
|
||||||
|
foreach($this -> things() as $uuid => $thing)
|
||||||
|
$data["things"][$uuid] = $thing -> to_json();
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function from_json($data, $scase) {
|
||||||
|
$obj = parent :: _from_json($data);
|
||||||
|
$obj -> scase_uuid = $scase -> uuid;
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function things() {
|
||||||
|
return Thing :: list(["category_uuid" => $this -> uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete_all_things() {
|
||||||
|
return Thing :: deleteAll(["category_uuid" => $this -> uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
107
src/Db/DbObject.php
Normal file
107
src/Db/DbObject.php
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\Date;
|
||||||
|
use EesyPHP\Log;
|
||||||
|
use EesyPHP\Db\AttrBool;
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
use EesyPHP\Db\AttrTimestamp;
|
||||||
|
|
||||||
|
use function EesyPHP\vardump;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySC database object
|
||||||
|
* @property string $uuid
|
||||||
|
* @property boolean $removed
|
||||||
|
* @property \DateTime|null $last_change
|
||||||
|
*/
|
||||||
|
class DbObject extends \EesyPHP\Db\DbObject {
|
||||||
|
protected const PRIMARY_KEYS = ['uuid'];
|
||||||
|
|
||||||
|
protected const UPDATABLE_FIELDS = [];
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return [
|
||||||
|
'uuid' => new AttrStr(['required' => true]),
|
||||||
|
'last_change' => new AttrTimestamp(['default' => 'time']),
|
||||||
|
'removed' => new AttrBool(['default' => false]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function to_json() {
|
||||||
|
$data = [
|
||||||
|
'uuid' => $this -> uuid,
|
||||||
|
'lastChange' => intval($this -> last_change -> format('Uv')),
|
||||||
|
'removed' => boolval($this -> removed),
|
||||||
|
];
|
||||||
|
foreach(static :: UPDATABLE_FIELDS as $field)
|
||||||
|
$data[$field] = $this -> $field;
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function _from_json($data) {
|
||||||
|
Log::debug(
|
||||||
|
"from_json(): input=%s",
|
||||||
|
vardump($data),
|
||||||
|
);
|
||||||
|
$class = get_called_class();
|
||||||
|
$obj = new $class();
|
||||||
|
$obj -> apply([
|
||||||
|
'uuid' => $data['uuid'],
|
||||||
|
'last_change' => Date :: from_timestamp($data['lastChange'] / 1000),
|
||||||
|
'removed' => $data['removed'] == "true",
|
||||||
|
]);
|
||||||
|
foreach(static :: UPDATABLE_FIELDS as $field)
|
||||||
|
$obj -> $field = $data[$field];
|
||||||
|
Log::debug(
|
||||||
|
"%s::from_json(): result=%s",
|
||||||
|
$obj,
|
||||||
|
vardump($obj->to_json()),
|
||||||
|
);
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sync(&$other, &$updated) {
|
||||||
|
if ($this -> last_change >= $other -> last_change) {
|
||||||
|
Log::debug(
|
||||||
|
"%s::sync(): keep current (%s >= %s)",
|
||||||
|
$this,
|
||||||
|
Date::format($this->last_change),
|
||||||
|
Date::format($other->last_change),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Log::debug(
|
||||||
|
"%s::sync(): update from other (%s < %s) : %s",
|
||||||
|
$this,
|
||||||
|
Date::format($this->last_change),
|
||||||
|
Date::format($other->last_change),
|
||||||
|
vardump($other->to_json()),
|
||||||
|
);
|
||||||
|
$this -> apply([
|
||||||
|
'last_change' => $other -> last_change,
|
||||||
|
'removed' => $other -> removed,
|
||||||
|
]);
|
||||||
|
foreach(static :: UPDATABLE_FIELDS as $field)
|
||||||
|
$this -> $field = $other -> $field;
|
||||||
|
$updated = true;
|
||||||
|
return $this -> save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List objects
|
||||||
|
* @param array<string,mixed> $where Where clauses as associative array of field name and value
|
||||||
|
* @return array<string,DbObject>|false
|
||||||
|
*/
|
||||||
|
public static function list($where=null) {
|
||||||
|
$objs = [];
|
||||||
|
foreach(parent :: list($where) as $obj)
|
||||||
|
$objs[$obj -> uuid] = $obj;
|
||||||
|
return $objs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
return get_called_class()."<".$this -> uuid.">";
|
||||||
|
}
|
||||||
|
}
|
81
src/Db/SCase.php
Normal file
81
src/Db/SCase.php
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
use EesyPHP\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SCase database object
|
||||||
|
* @property string $uuid
|
||||||
|
* @property string $name
|
||||||
|
* @property \DateTime|null $last_change
|
||||||
|
* @property boolean $removed
|
||||||
|
*/
|
||||||
|
class SCase extends DbObject {
|
||||||
|
protected const TABLE = 'scases';
|
||||||
|
protected const DEFAULT_ORDER = 'name';
|
||||||
|
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
|
||||||
|
protected const POSSIBLE_ORDERS = ['name', 'last_change'];
|
||||||
|
protected const UPDATABLE_FIELDS = ['name'];
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return array_merge(
|
||||||
|
parent :: get_schema(),
|
||||||
|
[
|
||||||
|
'username' => new AttrStr(['required' => true]),
|
||||||
|
'name' => new AttrStr(['required' => true]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function to_json() {
|
||||||
|
$data = parent :: to_json();
|
||||||
|
$data["cats"] = [];
|
||||||
|
foreach($this -> categories() as $uuid => $cat)
|
||||||
|
$data["cats"][$uuid] = $cat -> to_json();
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function from_json($data, $user) {
|
||||||
|
$obj = parent :: _from_json($data);
|
||||||
|
$obj -> username = $user -> username;
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get scase's categories
|
||||||
|
* @return array<Category>|false
|
||||||
|
*/
|
||||||
|
public function categories() {
|
||||||
|
return Category :: list(['scase_uuid' => $this -> uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
foreach($this -> categories() as $category) {
|
||||||
|
if (!$category->delete_all_things()) {
|
||||||
|
Log::error(
|
||||||
|
"Db: error occurred deleting things of category '%s' (%s)",
|
||||||
|
$category->name, $category->uuid
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!$category->delete()) {
|
||||||
|
Log::error(
|
||||||
|
"Db: error occurred deleting category '%s' (%s)",
|
||||||
|
$category->name, $category->uuid
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parent::delete()) {
|
||||||
|
Log::error(
|
||||||
|
"Db: error occurred deleting scases '%s' (%s)",
|
||||||
|
$this->name, $this->uuid
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
44
src/Db/Thing.php
Normal file
44
src/Db/Thing.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\Db\AttrBool;
|
||||||
|
use EesyPHP\Db\AttrInt;
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thing database object
|
||||||
|
* @property string $uuid
|
||||||
|
* @property string $category_uuid
|
||||||
|
* @property string $label
|
||||||
|
* @property int $nb
|
||||||
|
* @property boolean $checked
|
||||||
|
* @property \DateTime|null $last_change
|
||||||
|
* @property boolean $removed
|
||||||
|
*/
|
||||||
|
class Thing extends DbObject {
|
||||||
|
protected const TABLE = 'things';
|
||||||
|
protected const DEFAULT_ORDER = 'label';
|
||||||
|
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
|
||||||
|
protected const POSSIBLE_ORDERS = ['label', 'last_change'];
|
||||||
|
protected const UPDATABLE_FIELDS = ['label', 'nb', 'checked'];
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return array_merge(
|
||||||
|
parent :: get_schema(),
|
||||||
|
[
|
||||||
|
'category_uuid' => new AttrStr(['required' => true]),
|
||||||
|
'label' => new AttrStr(['required' => true]),
|
||||||
|
'nb' => new AttrInt(['required' => true, 'default' => 1]),
|
||||||
|
'checked' => new AttrBool(['required' => false, 'default' => false]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function from_json($data, $category) {
|
||||||
|
$obj = parent :: _from_json($data);
|
||||||
|
$obj -> category_uuid = $category -> uuid;
|
||||||
|
return $obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
src/Db/User.php
Normal file
39
src/Db/User.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MySC\Db;
|
||||||
|
|
||||||
|
use EesyPHP\Db\AttrStr;
|
||||||
|
use EesyPHP\Db\DbObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySC database object
|
||||||
|
* @property string|null $username
|
||||||
|
* @property string|null $name
|
||||||
|
* @property string|null $password
|
||||||
|
* @property string|null $mail
|
||||||
|
* @property boolean $removed
|
||||||
|
*/
|
||||||
|
class User extends DbObject {
|
||||||
|
protected const TABLE = 'users';
|
||||||
|
protected const PRIMARY_KEYS = ['username'];
|
||||||
|
protected const DEFAULT_ORDER = 'name';
|
||||||
|
protected const DEFAULT_ORDER_DIRECTION = 'ASC';
|
||||||
|
|
||||||
|
protected static function get_schema() {
|
||||||
|
return [
|
||||||
|
'username' => new AttrStr(['required' => true]),
|
||||||
|
'name' => new AttrStr(['required' => true]),
|
||||||
|
'password' => new AttrStr(['required' => true]),
|
||||||
|
'mail' => new AttrStr(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's scases
|
||||||
|
* @return bool|SCase[]
|
||||||
|
*/
|
||||||
|
public function scases() {
|
||||||
|
return SCase :: list(['username' => $this -> username]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
463
static/main.js
463
static/main.js
|
@ -62,9 +62,13 @@ on_title_click=function(event) {
|
||||||
$('.panel-collapse').each(function(idx,div) {
|
$('.panel-collapse').each(function(idx,div) {
|
||||||
$(div).collapse('hide');
|
$(div).collapse('hide');
|
||||||
});
|
});
|
||||||
if (show) {
|
if (show)
|
||||||
panel_collapse.collapse('show');
|
panel_collapse.collapse('show');
|
||||||
}
|
set_location(
|
||||||
|
title.data('scase').name,
|
||||||
|
show ? title.data('cat').name : null,
|
||||||
|
title.data('trash') ? 'trash' : null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************
|
/***********************
|
||||||
|
@ -96,6 +100,7 @@ on_valid_add_scase_modal=function (e) {
|
||||||
if (scase) {
|
if (scase) {
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
$('#add_scase_modal').modal('hide');
|
$('#add_scase_modal').modal('hide');
|
||||||
}
|
}
|
||||||
|
@ -140,6 +145,7 @@ on_valid_rename_scase_modal=function (e) {
|
||||||
if (scase) {
|
if (scase) {
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
alertify.notify('Une erreur est survenue en renomant la valise...', "error", 5);
|
alertify.notify('Une erreur est survenue en renomant la valise...', "error", 5);
|
||||||
|
@ -185,6 +191,7 @@ on_valid_copy_scase_modal=function (e) {
|
||||||
if (scase) {
|
if (scase) {
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
alertify.notify('Une erreur est survenue en copiant la valise...', "error", 5);
|
alertify.notify('Une erreur est survenue en copiant la valise...', "error", 5);
|
||||||
|
@ -214,6 +221,7 @@ on_reset_scase_btn_click=function(event) {
|
||||||
scases.resetSCase(scase.name);
|
scases.resetSCase(scase.name);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -233,6 +241,7 @@ on_delete_scase_btn_click=function(event) {
|
||||||
scases.removeSCase(scase.name);
|
scases.removeSCase(scase.name);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scases();
|
show_scases();
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -296,6 +305,7 @@ on_valid_add_cat_modal=function (e) {
|
||||||
if (cat) {
|
if (cat) {
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('#add_cat_modal').modal('hide');
|
$('#add_cat_modal').modal('hide');
|
||||||
|
@ -342,6 +352,7 @@ on_valid_rename_cat_modal=function (e) {
|
||||||
if (cat) {
|
if (cat) {
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('#rename_cat_modal').modal('hide');
|
$('#rename_cat_modal').modal('hide');
|
||||||
|
@ -370,6 +381,7 @@ on_delete_cat_btn_click=function(event) {
|
||||||
scase.cats.removeCat(cat);
|
scase.cats.removeCat(cat);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -388,6 +400,7 @@ on_restore_cat_btn_click=function(event) {
|
||||||
scase.cats.restoreCat(cat);
|
scase.cats.restoreCat(cat);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase);
|
show_scase(scase);
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -398,16 +411,8 @@ on_restore_cat_btn_click=function(event) {
|
||||||
* Check/Uncheck thing
|
* Check/Uncheck thing
|
||||||
***********************/
|
***********************/
|
||||||
on_li_click=function(event) {
|
on_li_click=function(event) {
|
||||||
if (event.target.tagName!='LI') {
|
if (event.target.tagName!='LI') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
var li=$(this);
|
var li=$(this);
|
||||||
if (li.hasClass('checked')) {
|
|
||||||
li.removeClass('checked');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
li.addClass('checked');
|
|
||||||
}
|
|
||||||
var ul=li.parent();
|
var ul=li.parent();
|
||||||
var scase=scases.byName($('#cats').data('scase'));
|
var scase=scases.byName($('#cats').data('scase'));
|
||||||
if (scase) {
|
if (scase) {
|
||||||
|
@ -415,8 +420,10 @@ on_li_click=function(event) {
|
||||||
if (cat) {
|
if (cat) {
|
||||||
var thing=cat.byLabel(li.data('label'));
|
var thing=cat.byLabel(li.data('label'));
|
||||||
if (thing) {
|
if (thing) {
|
||||||
|
li.toggleClass('checked');
|
||||||
thing.setChecked(li.hasClass('checked'));
|
thing.setChecked(li.hasClass('checked'));
|
||||||
scases.save();
|
scases.save();
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
}
|
}
|
||||||
|
@ -485,6 +492,7 @@ on_valid_add_thing_modal=function (e) {
|
||||||
}
|
}
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
|
@ -562,6 +570,7 @@ on_valid_edit_thing_modal=function (e) {
|
||||||
thing.setNb(nb);
|
thing.setNb(nb);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -593,6 +602,7 @@ on_delete_thing_btn_click=function(event) {
|
||||||
cat.removeThing(thing);
|
cat.removeThing(thing);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -614,6 +624,7 @@ on_restore_thing_btn_click=function(event) {
|
||||||
cat.restoreThing(thing);
|
cat.restoreThing(thing);
|
||||||
scases.save();
|
scases.save();
|
||||||
show_scase(scase,cat.name);
|
show_scase(scase,cat.name);
|
||||||
|
auto_sync_local_data();
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
@ -624,10 +635,12 @@ on_restore_thing_btn_click=function(event) {
|
||||||
/********************
|
/********************
|
||||||
* Show one scase
|
* Show one scase
|
||||||
*******************/
|
*******************/
|
||||||
show_cat=function(cat,displayed) {
|
show_cat=function(scase, cat, displayed) {
|
||||||
var panel=$('<div class="panel panel-default"></div>');
|
var panel=$('<div class="panel panel-default"></div>');
|
||||||
var panel_heading=$('<div class="panel-heading" role="tab"></div>');
|
var panel_heading=$('<div class="panel-heading" role="tab"></div>');
|
||||||
var panel_title=$('<h4 class="panel-title">'+cat.name+' </h4>');
|
var panel_title=$('<h4 class="panel-title">'+cat.name+' </h4>');
|
||||||
|
panel_title.data('scase', scase);
|
||||||
|
panel_title.data('cat', cat);
|
||||||
panel_title.bind('click',on_title_click);
|
panel_title.bind('click',on_title_click);
|
||||||
|
|
||||||
var stats=cat.stats();
|
var stats=cat.stats();
|
||||||
|
@ -649,7 +662,7 @@ show_cat=function(cat,displayed) {
|
||||||
|
|
||||||
panel_title.append(tag);
|
panel_title.append(tag);
|
||||||
|
|
||||||
|
|
||||||
panel_heading.append(panel_title);
|
panel_heading.append(panel_title);
|
||||||
panel.append(panel_heading);
|
panel.append(panel_heading);
|
||||||
var panel_collapse=$('<div class="panel-collapse collapse" role="tabpanel"></div>');
|
var panel_collapse=$('<div class="panel-collapse collapse" role="tabpanel"></div>');
|
||||||
|
@ -698,8 +711,9 @@ show_scase=function(scase,display_cat) {
|
||||||
if (cat.removed) {
|
if (cat.removed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
show_cat(cat,(cat.name==display_cat));
|
show_cat(scase, cat,(cat.name==display_cat));
|
||||||
});
|
});
|
||||||
|
set_location(scase.name, display_cat);
|
||||||
show_menu('scase');
|
show_menu('scase');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -720,18 +734,22 @@ show_scase_trash=function(scase,display_cat) {
|
||||||
});
|
});
|
||||||
|
|
||||||
scase.cats.each(function(idx,cat) {
|
scase.cats.each(function(idx,cat) {
|
||||||
show_cat_trash(cat,(cat.name==display_cat));
|
show_cat_trash(scase, cat, (cat.name==display_cat));
|
||||||
});
|
});
|
||||||
if ($('#cats .panel').length==0) {
|
if ($('#cats .panel').length==0) {
|
||||||
$('#content').append('<p class="center">La corbeille est vide.</p>');
|
$('#content').append('<p class="center">La corbeille est vide.</p>');
|
||||||
}
|
}
|
||||||
|
set_location(scase.name, display_cat, 'trash');
|
||||||
show_menu('scase');
|
show_menu('scase');
|
||||||
}
|
}
|
||||||
|
|
||||||
show_cat_trash=function(cat,displayed) {
|
show_cat_trash=function(scase, cat,displayed) {
|
||||||
var panel=$('<div class="panel panel-default"></div>');
|
var panel=$('<div class="panel panel-default"></div>');
|
||||||
var panel_heading=$('<div class="panel-heading" role="tab"></div>');
|
var panel_heading=$('<div class="panel-heading" role="tab"></div>');
|
||||||
var panel_title=$('<h4 class="panel-title">'+cat.name+' </h4>');
|
var panel_title=$('<h4 class="panel-title">'+cat.name+' </h4>');
|
||||||
|
panel_title.data('scase', scase);
|
||||||
|
panel_title.data('cat', cat);
|
||||||
|
panel_title.data('trash', true);
|
||||||
|
|
||||||
var tag=$('<span class="count-tag pull-right"></span>');
|
var tag=$('<span class="count-tag pull-right"></span>');
|
||||||
|
|
||||||
|
@ -816,6 +834,7 @@ show_scases=function() {
|
||||||
li.bind('click',on_scase_click);
|
li.bind('click',on_scase_click);
|
||||||
$('#scases').append(li);
|
$('#scases').append(li);
|
||||||
});
|
});
|
||||||
|
set_location();
|
||||||
show_menu('scases');
|
show_menu('scases');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -918,6 +937,25 @@ on_confirm_clear_local_data=function(data) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/********************
|
||||||
|
* Clear local data
|
||||||
|
********************/
|
||||||
|
load_example_data=function() {
|
||||||
|
navbar_collapse_hide();
|
||||||
|
alertify.confirm(
|
||||||
|
"Chargement des données d'exemple",
|
||||||
|
"Etes-vous sûre de vouloir charger les données d'exemple à la place de vos propres données (action irréversible) ?",
|
||||||
|
function() {
|
||||||
|
delete localStorage.scases;
|
||||||
|
scases=new SCaseList();
|
||||||
|
scases.importExampleData();
|
||||||
|
scases.save();
|
||||||
|
show_scases();
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/*******************************
|
/*******************************
|
||||||
* Import/Export local data
|
* Import/Export local data
|
||||||
*******************************/
|
*******************************/
|
||||||
|
@ -936,37 +974,47 @@ import_local_data=function() {
|
||||||
var file=input.prop('files')[0];
|
var file=input.prop('files')[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
$(reader).bind('load',function(e) {
|
$(reader).bind('load', function(e) {
|
||||||
if ($.type(e.target.result)=='string') {
|
if (
|
||||||
if (e.target.result.startsWith('data:application/json;base64,')) {
|
$.type(e.target.result)!='string'
|
||||||
try {
|
|| ! e.target.result.startsWith('data:application/json;base64,')
|
||||||
json_data=atob(e.target.result.replace('data:application/json;base64,',''));
|
) {
|
||||||
data=JSON.parse(json_data);
|
pleaseWaitHide();
|
||||||
pleaseWaitHide();
|
alertify.notify('Fichier.', "error", 5);
|
||||||
alertify.confirm(
|
return;
|
||||||
"Importation depuis un fichier",
|
|
||||||
"Etes-vous sûre de vouloir écraser vos données locales par celle issues de ce fichier ?",
|
|
||||||
function() {
|
|
||||||
scases.save();
|
|
||||||
var backData=localStorage.scases;
|
|
||||||
localStorage.scases=json_data;
|
|
||||||
scases=new SCaseList();
|
|
||||||
scases.loadFromLocalStorage(backData);
|
|
||||||
show_scases();
|
|
||||||
},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
alertify.notify('Impossible de décodé le fichier.', "error", 5);
|
|
||||||
pleaseWaitHide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
alertify.notify('Fichier invalide.', "error", 5);
|
|
||||||
pleaseWaitHide();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
json_data=atob(e.target.result.replace('data:application/json;base64,',''));
|
||||||
|
data=JSON.parse(json_data);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
pleaseWaitHide();
|
||||||
|
alertify.notify('Impossible de décodé le fichier.', "error", 5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pleaseWaitHide();
|
||||||
|
alertify.confirm(
|
||||||
|
"Importation depuis un fichier",
|
||||||
|
"Etes-vous sûre de vouloir écraser vos données locales par celle issues de ce fichier ?",
|
||||||
|
function() {
|
||||||
|
var backData=scases.export();
|
||||||
|
scases=new SCaseList();
|
||||||
|
if (scases.loadFromJsonData(data)) {
|
||||||
|
alertify.notify("Le fichier a bien été importé.", "success", 3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en chargeant ce fichier. Restauration des données précédentes...", "error", 5);
|
||||||
|
if (scases.loadFromJsonData(backData)) {
|
||||||
|
alertify.notify("Les données précédentes ont bien été restaurées.", "success", 5);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en restaurant les données précédentes.", "error", 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
show_scases();
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
@ -975,25 +1023,283 @@ import_local_data=function() {
|
||||||
input[0].click();
|
input[0].click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Authentication
|
||||||
|
******************************/
|
||||||
|
|
||||||
|
show_user=function() {
|
||||||
|
if (user.connected()) {
|
||||||
|
$('#login').parent().css('display', 'none');
|
||||||
|
$('#logout').parent().css('display', 'block');
|
||||||
|
$('#username').html(user.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#login').parent().css('display', 'block');
|
||||||
|
$('#logout').parent().css('display', 'none');
|
||||||
|
$('#username').html("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_login_button_click=function() {
|
||||||
|
if (user.connected()) return;
|
||||||
|
navbar_collapse_hide();
|
||||||
|
$('#login_modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
on_valid_login_modal=function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var username=$('#login_username').val();
|
||||||
|
var password=$('#login_password').val();
|
||||||
|
if (!username || !password) {
|
||||||
|
alertify.notify("Vous devez saisir votre nom d'utilisateur et votre mot de passe !", "error", 5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.post(
|
||||||
|
'login',
|
||||||
|
{
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
}
|
||||||
|
).done(function(data) {
|
||||||
|
if (data.username && data.token) {
|
||||||
|
user.token = data.token;
|
||||||
|
user.username = data.username;
|
||||||
|
user.name = data.name?data.name:username;
|
||||||
|
user.save();
|
||||||
|
$('#login_modal').modal('hide');
|
||||||
|
show_user();
|
||||||
|
alertify.notify("Connecté.", "success", 3);
|
||||||
|
propose_sync_local_data();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Nom d'utilisateur ou mot de passe.", "error", 5);
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
alertify.notify('Une erreur est survenue en vous identifiant. Merci de réessayer ultèrieument.', "error", 5);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
on_show_login_modal=function () {
|
||||||
|
$('#login_username').val(user.username?user.username:"");
|
||||||
|
if (user.username)
|
||||||
|
$('#login_password').focus();
|
||||||
|
else
|
||||||
|
$('#login_username').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_close_login_modal=function () {
|
||||||
|
$('#login_modal form')[0].reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_logout_button_click=function() {
|
||||||
|
if (!user.connected()) return;
|
||||||
|
navbar_collapse_hide();
|
||||||
|
alertify.confirm(
|
||||||
|
"Déconnexion",
|
||||||
|
"Voulez-vous vraiment vous déconnecter ?",
|
||||||
|
function(data) {
|
||||||
|
$.post(
|
||||||
|
'logout',
|
||||||
|
{
|
||||||
|
username: user.username,
|
||||||
|
token: user.token,
|
||||||
|
}
|
||||||
|
).done(function(data) {
|
||||||
|
if (data.success) {
|
||||||
|
user.reset();
|
||||||
|
user.save();
|
||||||
|
show_user();
|
||||||
|
alertify.notify("Déconnecté.", "success", 3);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en vous déconnectant. Merci de réessayer ultèrieument.", "error", 5);
|
||||||
|
}
|
||||||
|
}).fail(function() {
|
||||||
|
alertify.notify('Une erreur est survenue en vous déconnectant. Merci de réessayer ultèrieument.', "error", 5);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
is_connected=function() {
|
||||||
|
return user && user.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************
|
||||||
|
* Sync local data with server
|
||||||
|
*******************************/
|
||||||
|
sync_local_data = function(callback) {
|
||||||
|
if (!is_connected()) {
|
||||||
|
if (callback) callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.post(
|
||||||
|
'sync',
|
||||||
|
{
|
||||||
|
username: user.username,
|
||||||
|
token: user.token,
|
||||||
|
data: JSON.stringify(scases.export()),
|
||||||
|
}
|
||||||
|
).done(function(data) {
|
||||||
|
if (data.scases) {
|
||||||
|
var backData=scases.export();
|
||||||
|
scases=new SCaseList();
|
||||||
|
if (scases.loadFromJsonData(data)) {
|
||||||
|
scases.save();
|
||||||
|
alertify.notify("Données synchronisées.", "success", 3);
|
||||||
|
if (callback) callback(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en chargeant les données issues du serveur. Restauration des données précédentes...", "error", 5);
|
||||||
|
if (scases.loadFromJsonData(backData)) {
|
||||||
|
alertify.notify("Les données précédentes ont bien été restaurées.", "success", 5);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en restaurant les données précédentes.", "error", 5);
|
||||||
|
}
|
||||||
|
if (callback) callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en synchronisant vos données. Merci de réessayer ultèrieument.", "error", 5);
|
||||||
|
if (callback) callback(false);
|
||||||
|
}
|
||||||
|
}).fail(function(xhr, status, error) {
|
||||||
|
if (xhr.status == 401) {
|
||||||
|
user.token = null;
|
||||||
|
user.save();
|
||||||
|
show_user();
|
||||||
|
alertify.notify("Votre session semble avoir expirée, merci de vous réauthentifier.", "error", 8);
|
||||||
|
$('#login_modal').modal('show');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alertify.notify("Une erreur est survenue en synchronisant vos données. Merci de réessayer ultèrieument.", "error", 5);
|
||||||
|
}
|
||||||
|
if (callback) callback(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_auto_sync_timeout = false;
|
||||||
|
auto_sync_local_data = function() {
|
||||||
|
if (_auto_sync_timeout) clearTimeout(_auto_sync_timeout);
|
||||||
|
if (!is_connected() || !navigator.onLine) return;
|
||||||
|
_auto_sync_timeout = setTimeout(sync_local_data, 3000);
|
||||||
|
}
|
||||||
|
on_sync_local_data_btn_click=function() {
|
||||||
|
navbar_collapse_hide();
|
||||||
|
if (!is_connected()) {
|
||||||
|
alertify.notify("Vous devez vous connecter avant de pouvoir synchroniser vos données.", "error", 5);
|
||||||
|
$('#login_modal').modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pleaseWaitShow();
|
||||||
|
sync_local_data(function(success) {
|
||||||
|
pleaseWaitHide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
propose_sync_local_data = function() {
|
||||||
|
alertify.confirm(
|
||||||
|
"Synchronisation de vos valises depuis le serveur",
|
||||||
|
"Voulez-vous synchroniser vos valises depuis le serveur ?",
|
||||||
|
function() {
|
||||||
|
pleaseWaitShow();
|
||||||
|
sync_local_data(
|
||||||
|
function(success) {
|
||||||
|
pleaseWaitHide();
|
||||||
|
if (!success) return;
|
||||||
|
if (scases.count() == 0)
|
||||||
|
propose_example_data();
|
||||||
|
else
|
||||||
|
refresh_location();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
refresh_location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********************
|
||||||
|
* Manage location hash
|
||||||
|
***********************/
|
||||||
|
|
||||||
|
set_location = function(scase, cat, detail) {
|
||||||
|
console.log(`set_location(${scase}, ${cat}, ${detail})`);
|
||||||
|
var parts = [];
|
||||||
|
if (scase) parts[0] = scase;
|
||||||
|
if (cat) parts[1] = cat;
|
||||||
|
if (detail) parts[2] = detail;
|
||||||
|
location.hash = parts.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_location = function(value) {
|
||||||
|
value = typeof value == "undefined" ? location.hash : value;
|
||||||
|
console.log(`parse_location(${value})`);
|
||||||
|
parts = (
|
||||||
|
typeof value == "undefined" ? location.hash : value
|
||||||
|
).split("|");
|
||||||
|
return {
|
||||||
|
scase: parts[0]?decodeURI(parts[0].substring(1)):null,
|
||||||
|
cat: parts[0] && parts[1]?decodeURI(parts[1]):null,
|
||||||
|
detail: parts[2] ? decodeURI(parts[2]) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_location = function() {
|
||||||
|
var info = parse_location();
|
||||||
|
console.log(`refresh_location(${info.scase}, ${info.cat}, ${info.detail})`);
|
||||||
|
var scase = info.scase ? scases.byName(info.scase) : null;
|
||||||
|
if (!scase)
|
||||||
|
show_scases();
|
||||||
|
else if (info.detail == 'trash')
|
||||||
|
show_scase_trash(scase, info.cat);
|
||||||
|
else
|
||||||
|
show_scase(scase, info.cat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***********************
|
||||||
|
* Welcome modal
|
||||||
|
***********************/
|
||||||
|
|
||||||
|
on_welcome_connect_click = function() {
|
||||||
|
$('#welcome_modal').modal('hide');
|
||||||
|
$('#login_modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
on_welcome_annonymous_click = function() {
|
||||||
|
$('#welcome_modal').modal('hide');
|
||||||
|
if (scases.count() == 0) propose_example_data();
|
||||||
|
}
|
||||||
|
|
||||||
|
propose_example_data = function() {
|
||||||
|
alertify.confirm(
|
||||||
|
"Un exemple de valise ?",
|
||||||
|
"Souhaitez-vous charger un exemple de valise pour commencer ?",
|
||||||
|
function() {
|
||||||
|
scases.importExampleData();
|
||||||
|
scases.save();
|
||||||
|
refresh_location();
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/*********************
|
/*********************
|
||||||
* Activate
|
* Activate
|
||||||
*********************/
|
*********************/
|
||||||
$( document ).ready( function() {
|
$( document ).ready( function() {
|
||||||
pleaseWaitShow();
|
if(typeof(localStorage)==="undefined"){
|
||||||
if(typeof(localStorage)!=="undefined"){
|
alertify.notify(
|
||||||
scases=new SCaseList();
|
'Votre navigateur internet ne support pas le stockage de données locale. Vous ne pouvez donc malheureusment pas utiliser cette application.', "error", 5);
|
||||||
scases.loadFromLocalStorage();
|
|
||||||
show_scases();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
alertify.notify('Local storage not supported !', "error", 5);
|
|
||||||
pleaseWaitHide();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
pleaseWaitShow();
|
||||||
|
|
||||||
$('#clear_local_data').bind('click',clear_local_data);
|
$('#clear_local_data').bind('click',clear_local_data);
|
||||||
|
$('#load_example_data').bind('click',load_example_data);
|
||||||
$('#import_local_data').bind('click',import_local_data);
|
$('#import_local_data').bind('click',import_local_data);
|
||||||
$('#export_local_data').bind('click',export_local_data);
|
$('#export_local_data').bind('click',export_local_data);
|
||||||
|
$('#sync_local_data').bind('click',on_sync_local_data_btn_click);
|
||||||
|
|
||||||
$('#add_scase_btn').bind('click',on_add_scase_btn_click);
|
$('#add_scase_btn').bind('click',on_add_scase_btn_click);
|
||||||
$('#add_scase_submit').bind('click',on_valid_add_scase_modal);
|
$('#add_scase_submit').bind('click',on_valid_add_scase_modal);
|
||||||
|
@ -1031,7 +1337,7 @@ $( document ).ready( function() {
|
||||||
$("#rename_cat_modal form").bind('submit',on_valid_rename_cat_modal);
|
$("#rename_cat_modal form").bind('submit',on_valid_rename_cat_modal);
|
||||||
|
|
||||||
$('#back_to_scases').bind('click',on_back_to_scases_btn_click);
|
$('#back_to_scases').bind('click',on_back_to_scases_btn_click);
|
||||||
|
|
||||||
$('input.add_thing_label').bind('focus',on_add_thing_label_focus);
|
$('input.add_thing_label').bind('focus',on_add_thing_label_focus);
|
||||||
$('#add_thing_submit').bind('click',on_valid_add_thing_modal);
|
$('#add_thing_submit').bind('click',on_valid_add_thing_modal);
|
||||||
$("#add_thing_modal").on('shown.bs.modal',on_show_add_thing_modal);
|
$("#add_thing_modal").on('shown.bs.modal',on_show_add_thing_modal);
|
||||||
|
@ -1043,7 +1349,50 @@ $( document ).ready( function() {
|
||||||
$("#edit_thing_modal").on('hidden.bs.modal',on_close_edit_thing_modal);
|
$("#edit_thing_modal").on('hidden.bs.modal',on_close_edit_thing_modal);
|
||||||
$("#edit_thing_modal form").bind('submit',on_valid_edit_thing_modal);
|
$("#edit_thing_modal form").bind('submit',on_valid_edit_thing_modal);
|
||||||
|
|
||||||
|
$('#login').bind('click',on_login_button_click);
|
||||||
|
$('#logout').bind('click',on_logout_button_click);
|
||||||
|
$('#login_submit').bind('click',on_valid_login_modal);
|
||||||
|
$("#login_modal").on('shown.bs.modal',on_show_login_modal);
|
||||||
|
$("#login_modal").on('hidden.bs.modal',on_close_login_modal);
|
||||||
|
$("#login_modal form").bind('submit',on_valid_login_modal);
|
||||||
|
|
||||||
|
$('#welcome_connect').bind('click',on_welcome_connect_click);
|
||||||
|
$('#welcome_annonymous').bind('click',on_welcome_annonymous_click);
|
||||||
|
|
||||||
$('#app-name').bind('click', show_scases);
|
$('#app-name').bind('click', show_scases);
|
||||||
|
|
||||||
pleaseWaitHide();
|
user=new User();
|
||||||
|
user.loadFromLocalStorage();
|
||||||
|
show_user();
|
||||||
|
|
||||||
|
scases=new SCaseList();
|
||||||
|
switch(scases.loadFromLocalStorage()) {
|
||||||
|
case null:
|
||||||
|
pleaseWaitHide();
|
||||||
|
if (is_connected())
|
||||||
|
propose_sync_local_data();
|
||||||
|
else
|
||||||
|
$('#welcome_modal').modal('show');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case false:
|
||||||
|
alertify.confirm(
|
||||||
|
"Erreur en chargeant les données locales",
|
||||||
|
'Une erreur est survenue en chargeant les données locales. On les purges ?',
|
||||||
|
function(data) {
|
||||||
|
delete localStorage.scases;
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
function(data) {
|
||||||
|
pleaseWaitHide();
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case true:
|
||||||
|
refresh_location();
|
||||||
|
pleaseWaitHide();
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,51 +35,34 @@ function SCaseList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadFromLocalStorage=function(backData) {
|
this.loadFromLocalStorage=function(data) {
|
||||||
if (jQuery.type(localStorage.scases)!='undefined') {
|
if (jQuery.type(localStorage.scases)!='undefined') {
|
||||||
try {
|
try {
|
||||||
var data=JSON.parse(localStorage.scases);
|
return this.loadFromJsonData(JSON.parse(localStorage.scases));
|
||||||
this.lastChange=data.lastChange;
|
|
||||||
for (el in data.scases) {
|
|
||||||
this[el]=new SCase(false,false,data.scases[el]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch(e) {
|
||||||
for (el in this) {
|
return false;
|
||||||
if (this.isSCase(this[el])) {
|
}
|
||||||
delete this[el];
|
}
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
if (jQuery.type(backData)!='undefined') {
|
|
||||||
alert('Erreur en chargeant les données. Restauration des données précédentes');
|
this.loadFromJsonData=function(data) {
|
||||||
localStorage.scases=backData;
|
try {
|
||||||
return this.loadFromLocalStorage();
|
this.lastChange=data.lastChange;
|
||||||
}
|
for (el in data.scases) {
|
||||||
else {
|
this[el]=new SCase(false,false,data.scases[el]);
|
||||||
alertify.confirm(
|
}
|
||||||
"Erreur en chargeant les données locales",
|
return true;
|
||||||
'Une erreur est survenue en chargeant les données locales. On les purges ?',
|
}
|
||||||
function(data) {
|
catch(e) {
|
||||||
delete localStorage.scases;
|
for (el in this) {
|
||||||
location.reload();
|
if (this.isSCase(this[el])) {
|
||||||
},
|
delete this[el];
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
return false;
|
||||||
alertify.confirm(
|
|
||||||
"Bienvenu",
|
|
||||||
"<h2>Bienvenu !</h2><p>Souhaitez-vous charger les données d'exemple ?</p>",
|
|
||||||
function() {
|
|
||||||
this.importExampleData();
|
|
||||||
this.save();
|
|
||||||
show_scases();
|
|
||||||
}.bind(this),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.export=function() {
|
this.export=function() {
|
||||||
|
@ -143,6 +126,10 @@ function SCaseList() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.byUUID=function(uuid) {
|
||||||
|
return this.isCase(this[uuid]) ? this[uuid] : null;
|
||||||
|
}
|
||||||
|
|
||||||
this.removeSCase=function(name) {
|
this.removeSCase=function(name) {
|
||||||
for (el in this) {
|
for (el in this) {
|
||||||
if (this.isSCase(this[el]) && this[el].name==name) {
|
if (this.isSCase(this[el]) && this[el].name==name) {
|
||||||
|
@ -361,6 +348,10 @@ function CatList(data) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.byUUID=function(uuid) {
|
||||||
|
return this.isCas(this[uuid]) ? this[uuid] : null;
|
||||||
|
}
|
||||||
|
|
||||||
this.newCat=function(name) {
|
this.newCat=function(name) {
|
||||||
if (this.byName(name)) {
|
if (this.byName(name)) {
|
||||||
var cat=this.byName(name);
|
var cat=this.byName(name);
|
||||||
|
@ -606,6 +597,7 @@ function Thing(uuid,label,nb,checked) {
|
||||||
this.setChecked=function(value) {
|
this.setChecked=function(value) {
|
||||||
this.checked=value;
|
this.checked=value;
|
||||||
this.lastChange=new Date().getTime();
|
this.lastChange=new Date().getTime();
|
||||||
|
console.log(`Thing<${this.uuid}>.setChecked(${this.checked}): ${this.lastChange}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.remove=function() {
|
this.remove=function() {
|
||||||
|
@ -618,3 +610,40 @@ function Thing(uuid,label,nb,checked) {
|
||||||
this.lastChange=new Date().getTime();
|
this.lastChange=new Date().getTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function User() {
|
||||||
|
this.username = null;
|
||||||
|
this.name = null;
|
||||||
|
this.token = null;
|
||||||
|
this.loadFromLocalStorage=function() {
|
||||||
|
if (jQuery.type(localStorage.user) == 'undefined')
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
var data=JSON.parse(localStorage.user);
|
||||||
|
this.username = data.username;
|
||||||
|
this.name = data.name;
|
||||||
|
this.token = data.token;
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
alert('Erreur en chargeant vos informations de connexion. Merci de vous reconnecter.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.connected=function() {
|
||||||
|
return this.username && this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reset=function() {
|
||||||
|
this.username = null;
|
||||||
|
this.name = null;
|
||||||
|
this.token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save=function() {
|
||||||
|
localStorage.user = JSON.stringify({
|
||||||
|
username: this.username,
|
||||||
|
name: this.name,
|
||||||
|
token: this.token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" id='app-name'>MySC</a>
|
<a class="navbar-brand" id='app-name'>MySC</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse navbar-collapse" id="navbar-top-collapse">
|
<div class="collapse navbar-collapse" id="navbar-top-collapse">
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li class="menu menu-scases"><a href="#add_scase" id="add_scase_btn"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une valise</a></li>
|
<li class="menu menu-scases"><a href="#add_scase" id="add_scase_btn"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une valise</a></li>
|
||||||
<li class="menu menu-scases"><a href="#scases_trash" id="scases_trash_btn"><span class="glyphicon glyphicon-trash"></span> Voir la corbeille</a></li>
|
<li class="menu menu-scases"><a href="#scases_trash" id="scases_trash_btn"><span class="glyphicon glyphicon-trash"></span> Voir la corbeille</a></li>
|
||||||
|
@ -58,7 +58,17 @@
|
||||||
<li><a id='export_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-save'></span> Sauvegarder vos données</a></li>
|
<li><a id='export_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-save'></span> Sauvegarder vos données</a></li>
|
||||||
<li><a id='import_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-open'></span> Restaurer vos données</a></li>
|
<li><a id='import_local_data' href='#' download='mysc_export.json'><span class='glyphicon glyphicon-open'></span> Restaurer vos données</a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
|
<li><a id='sync_local_data'><span class='glyphicon glyphicon-refresh'></span> Synchroniser les données locales</a></li>
|
||||||
|
<li class="divider"></li>
|
||||||
<li><a id='clear_local_data'><span class='glyphicon glyphicon-trash'></span> Purger les données locales</a></li>
|
<li><a id='clear_local_data'><span class='glyphicon glyphicon-trash'></span> Purger les données locales</a></li>
|
||||||
|
<li><a id='load_example_data'><span class='glyphicon glyphicon-trash'></span> Charger les données d'exemple</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class='glyphicon glyphicon-user'></span> <span id="username"></span> <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a id='login' href='#'><span class='glyphicon glyphicon-loging'></span> Connexion</a></li>
|
||||||
|
<li><a id='logout' href='#'><span class='glyphicon glyphicon-logout'></span> Déconnexion</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -249,6 +259,64 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal" id="login_modal" tabindex="-1" role="dialog" aria-labelledby="loginModal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Connexion</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="form-horizontal" role="form" action="login">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type='text' id='login_username' class="form-control" placeholder="Nom d'utilisateur" required/>
|
||||||
|
<input type='password' id='login_password' class="form-control" placeholder="Mot de passe" required/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
|
||||||
|
<button type="button" class="btn btn-primary" id='login_submit'>Connexion</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal" id="welcome_modal" tabindex="-1" role="dialog" aria-labelledby="welcomeModal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Bienvenu</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
Cette application vous permet de gérer des listes de choses à ne pas oublier de glisser dans votre valise avant votre départ :
|
||||||
|
<ul>
|
||||||
|
<li>Pour commencer, créer une valise, ajouter les catégories de choses que vous aurez à y glisser et ajouter y toutes ces choses.</li>
|
||||||
|
<li>Avant votre départ, vous pourrez alors cocher petit à petit toutes les choses que vous aurez déjà réunies et vous asurez de ne rien oublier !</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Note :</strong> Cette application a été conçue pour pouvoir fonctionner complètement localement. Les données manipulées (vos valises, ...) sont stockés
|
||||||
|
uniquement dans votre navigateur internet. Il est par ailleurs possible d'exporter/importer ces informations au format JSON.<br/>
|
||||||
|
Cependant, pour faciliter la gestion de vos valises depuis plusieurs appareils, il est possible de synchroniser ces informations sur le serveur. Pour cela,
|
||||||
|
il vous faut un compte et à ce jour, les inscriptions ne sont pas ouvertes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Si vous disposez d'un compte, vous pouvez vous connecter en cliquant sur le bouton <em>Connexion</em> ci-dessous.
|
||||||
|
À défaut, cliquer sur le bouton <em>Utilisation annonyme</em> pour commencer à utiliser l'application.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" id='welcome_connect'>Connexion</button>
|
||||||
|
<button type="button" class="btn btn-default" id='welcome_annonymous'>Utilisation annonyme</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||||
<script src="{static_url path="lib/jquery.min.js"}"></script>
|
<script src="{static_url path="lib/jquery.min.js"}"></script>
|
||||||
<!-- Latest compiled and minified JavaScript -->
|
<!-- Latest compiled and minified JavaScript -->
|
||||||
|
|
13
templates/support_info.tpl
Normal file
13
templates/support_info.tpl
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{extends file='Tpl:empty.tpl'}
|
||||||
|
{block name="pagetitle"}{/block}
|
||||||
|
{block name="content"}
|
||||||
|
<h1>Page d'aide à l'assistance utilisateurs</h1>
|
||||||
|
<p>À leur demande, merci de télécharger et transmettre les informations ci-dessous au service support :</p>
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
<a href="{$request->current_url}?download" class="btn btn-primary">
|
||||||
|
<i class="fa fa-download" aria-hidden="true"></i>
|
||||||
|
Télécharger
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<pre class="text-bg-light p-3 copyable">{include file="Tpl:support_info_content.tpl"}</pre>
|
||||||
|
{/block}
|
8
templates/support_info_content.tpl
Normal file
8
templates/support_info_content.tpl
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
URL de l'application : {$public_root_url}
|
||||||
|
URL de la page consultée : {$public_root_url}/{$request->current_url}
|
||||||
|
|
||||||
|
Utilisateur connecté : {$auth_user->username}
|
||||||
|
|
||||||
|
Informations supplémentaires sur l'utilisateur :
|
||||||
|
================================================
|
||||||
|
{var_dump data=$auth_user->info}
|
Loading…
Reference in a new issue