diff --git a/docker-compose.yml b/docker-compose.yml index 60e13b0..46c815f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,12 +29,12 @@ services: restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS:-rootchangeme} - MYSQL_DATABASE: ${DB_NAME:-househub} + MYSQL_DATABASE: househub_meta MYSQL_USER: ${DB_USER:-househub} MYSQL_PASSWORD: ${DB_PASS:-changeme} volumes: - househub_db_data:/var/lib/mysql - - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro + - ./docker/init:/docker-entrypoint-initdb.d:ro networks: - househub_net healthcheck: diff --git a/docker/init/00-setup.sql b/docker/init/00-setup.sql new file mode 100644 index 0000000..17f1853 --- /dev/null +++ b/docker/init/00-setup.sql @@ -0,0 +1,27 @@ +-- Initialisation HouseHub — Meta DB + permissions +-- Exécuté automatiquement au premier démarrage MariaDB + +-- Tables meta (dans la DB créée par MYSQL_DATABASE=househub_meta) +USE househub_meta; + +CREATE TABLE IF NOT EXISTS families ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + db_name VARCHAR(64) NOT NULL UNIQUE, + invite_code VARCHAR(32) NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + display_name VARCHAR(100) NOT NULL, + family_id INT DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (family_id) REFERENCES families(id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Donner au user applicatif le droit de créer des DBs famille +GRANT ALL PRIVILEGES ON `househub_f%`.* TO 'househub'@'%'; +FLUSH PRIVILEGES; diff --git a/docker/schema_family.sql b/docker/schema_family.sql new file mode 100644 index 0000000..0bb5b8a --- /dev/null +++ b/docker/schema_family.sql @@ -0,0 +1,237 @@ +-- HouseHub — Schéma MySQL +-- Créer la base : CREATE DATABASE househub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ─── Utilisateurs ───────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + display_name VARCHAR(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Personnes ──────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_people ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Calendrier familial ────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + event_date DATE NOT NULL, + event_type VARCHAR(50) NOT NULL, + person_id INT NOT NULL, + duration DECIMAL(4,2) DEFAULT 1.0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_leaves ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + leave_date DATE NOT NULL, + duration DECIMAL(4,2) DEFAULT 1.0, + UNIQUE KEY uq_leave (person_id, leave_type, leave_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_leave_balances ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + initial_balance DECIMAL(6,2) DEFAULT 0, + balance_year INT NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_leave_snapshots ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL, + leave_type VARCHAR(50) NOT NULL, + snapshot_date DATE NOT NULL, + remaining_balance DECIMAL(6,2) DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_person_leave_meta ( + id INT AUTO_INCREMENT PRIMARY KEY, + person_id INT NOT NULL UNIQUE, + anniversary_date DATE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_calendar_weeks ( + id INT AUTO_INCREMENT PRIMARY KEY, + year INT NOT NULL, + week_iso_year INT NOT NULL, + week_iso_number INT NOT NULL, + week_label VARCHAR(20), + month INT, + month_name VARCHAR(20), + week_start_date DATE, + mon_date DATE, tue_date DATE, wed_date DATE, + thu_date DATE, fri_date DATE, sat_date DATE, sun_date DATE, + UNIQUE KEY uq_week (year, week_iso_year, week_iso_number) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Budget ─────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_budget_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + amount DECIMAL(10,2) DEFAULT 0, + category VARCHAR(100), + type VARCHAR(50), + payment_day INT DEFAULT NULL, + is_estimate TINYINT(1) DEFAULT 0, + reg_month VARCHAR(7) DEFAULT NULL, + mapping_keywords TEXT DEFAULT NULL, + holiday_id INT DEFAULT NULL, + is_checked TINYINT(1) DEFAULT 0, + sort_order INT DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_expenses ( + id INT AUTO_INCREMENT PRIMARY KEY, + date_exp DATE NOT NULL, + gestion_month VARCHAR(7) NOT NULL, + category VARCHAR(100), + label VARCHAR(255), + amount DECIMAL(10,2) DEFAULT 0, + import_ref VARCHAR(255) DEFAULT NULL, + budget_item_id INT DEFAULT NULL, + holiday_id INT DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_alloc_categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + target DECIMAL(10,2) DEFAULT 0, + holiday_id INT DEFAULT NULL, + sort_order INT DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_alloc_values ( + id INT AUTO_INCREMENT PRIMARY KEY, + month_date VARCHAR(7) NOT NULL, + cat_id INT NOT NULL, + amount_alex DECIMAL(10,2) DEFAULT 0, + amount_laia DECIMAL(10,2) DEFAULT 0, + UNIQUE KEY uq_alloc (month_date, cat_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_savings ( + id INT AUTO_INCREMENT PRIMARY KEY, + owner VARCHAR(50) NOT NULL, + month_date VARCHAR(7) NOT NULL, + category VARCHAR(100) NOT NULL, + amount DECIMAL(10,2) DEFAULT 0, + holiday_id INT DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_bank_snapshots ( + id INT AUTO_INCREMENT PRIMARY KEY, + snapshot_date DATE NOT NULL, + amount DECIMAL(12,2) DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_salary_config ( + id INT AUTO_INCREMENT PRIMARY KEY, + year INT NOT NULL, + person VARCHAR(50) NOT NULL, + salary DECIMAL(10,2) DEFAULT 0, + mensualite DECIMAL(10,2) DEFAULT 0, + frais_func DECIMAL(10,2) DEFAULT 0, + eco_perso DECIMAL(10,2) DEFAULT 0, + eco_family DECIMAL(10,2) DEFAULT 0, + UNIQUE KEY uq_salary (year, person) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_import_rules ( + id INT AUTO_INCREMENT PRIMARY KEY, + keyword VARCHAR(255) NOT NULL UNIQUE, + category VARCHAR(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_notes ( + id INT AUTO_INCREMENT PRIMARY KEY, + note_type VARCHAR(100) NOT NULL, + reference_id VARCHAR(100) NOT NULL, + content TEXT, + UNIQUE KEY uq_note (note_type, reference_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Voyages / Holidays ─────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_holidays ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + period_hint VARCHAR(100) DEFAULT NULL, + start_date DATE DEFAULT NULL, + end_date DATE DEFAULT NULL, + status VARCHAR(50) DEFAULT 'draft', + budget_food DECIMAL(10,2) DEFAULT 0, + budget_extra DECIMAL(10,2) DEFAULT 0, + notes TEXT DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_holidays_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + holiday_id INT NOT NULL, + category VARCHAR(100), + name VARCHAR(255), + amount DECIMAL(10,2) DEFAULT 0, + is_paid TINYINT(1) DEFAULT 0, + location_name VARCHAR(255) DEFAULT NULL, + lat DECIMAL(10,7) DEFAULT NULL, + lng DECIMAL(10,7) DEFAULT NULL, + sort_order INT DEFAULT 0, + notes TEXT DEFAULT NULL, + item_date DATE DEFAULT NULL, + item_time TIME DEFAULT NULL, + step_start_date DATE DEFAULT NULL, + step_end_date DATE DEFAULT NULL, + duration INT DEFAULT NULL, + is_return TINYINT(1) DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_holidays_ideas ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + period_hint VARCHAR(100) DEFAULT NULL, + start_date DATE DEFAULT NULL, + end_date DATE DEFAULT NULL, + status VARCHAR(50) DEFAULT 'idea', + budget_food DECIMAL(10,2) DEFAULT 0, + budget_extra DECIMAL(10,2) DEFAULT 0, + notes TEXT DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS pf_geocode_cache ( + q_hash CHAR(64) PRIMARY KEY, + q VARCHAR(255), + lat DECIMAL(10,7), + lng DECIMAL(10,7), + display_name TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- ─── Cadeaux ────────────────────────────────────────────────────────────────── +CREATE TABLE IF NOT EXISTS pf_gifts ( + id INT AUTO_INCREMENT PRIMARY KEY, + year INT NOT NULL, + adult_name VARCHAR(100) NOT NULL, + payer_name VARCHAR(100), + child_name VARCHAR(100) NOT NULL, + occasion VARCHAR(50) NOT NULL, + gift_description TEXT NOT NULL, + product_link VARCHAR(500) DEFAULT NULL, + amount DECIMAL(8,2) DEFAULT 0 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ─── Données initiales ──────────────────────────────────────────────────────── +-- Utilisateur admin (mot de passe : changeme → à modifier !) +INSERT IGNORE INTO pf_users (id, username, password_hash, display_name) +VALUES (1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin'); + +-- Personnes (IDs fixes correspondant aux constantes dans config.php) +INSERT IGNORE INTO pf_people (id, name) VALUES (2, 'Alex'); +INSERT IGNORE INTO pf_people (id, name) VALUES (3, 'Laia'); diff --git a/includes/db.php b/includes/db.php index 4dc0cbd..d2b2259 100644 --- a/includes/db.php +++ b/includes/db.php @@ -3,26 +3,38 @@ if (file_exists(__DIR__ . '/config.php')) { require_once __DIR__ . '/config.php'; } -$host = getenv('DB_HOST') ?: 'househub-db'; -$db = getenv('DB_NAME') ?: 'househub'; -$user = getenv('DB_USER') ?: 'househub'; -$pass = getenv('DB_PASS') ?: 'changeme'; -$charset = 'utf8mb4'; +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} -$dsn = "mysql:host=$host;dbname=$db;charset=$charset"; +$host = getenv('DB_HOST') ?: 'househub-db'; +$user = getenv('DB_USER') ?: 'househub'; +$pass = getenv('DB_PASS') ?: 'changeme'; +$db = $_SESSION['family_db'] ?? null; -$options = [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci", - PDO::ATTR_TIMEOUT => 30, -]; +if (!$db) { + $current = basename($_SERVER['PHP_SELF'] ?? ''); + if (!in_array($current, ['login.php', 'register.php'])) { + header('Location: /login.php'); + exit; + } + return; +} try { - $pdo = new PDO($dsn, $user, $pass, $options); + $pdo = new PDO( + "mysql:host=$host;dbname=$db;charset=utf8mb4", + $user, $pass, + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci", + PDO::ATTR_TIMEOUT => 30, + ] + ); $pdo->exec("SET collation_connection = utf8mb4_general_ci"); } catch (\PDOException $e) { - die("Erreur de connexion BDD : " . $e->getMessage()); + die("Erreur connexion BDD famille : " . $e->getMessage()); } ?> \ No newline at end of file diff --git a/includes/meta_db.php b/includes/meta_db.php new file mode 100644 index 0000000..88e7f39 --- /dev/null +++ b/includes/meta_db.php @@ -0,0 +1,21 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ] + ); +} catch (\PDOException $e) { + die(json_encode(['error' => 'Meta DB unavailable: ' . $e->getMessage()])); +} diff --git a/invite.php b/invite.php new file mode 100644 index 0000000..e520d8a --- /dev/null +++ b/invite.php @@ -0,0 +1,64 @@ +prepare("SELECT * FROM families WHERE id = ?"); +$family->execute([$_SESSION['user']['family_id']]); +$family = $family->fetch(); + +$members = $meta_pdo->prepare("SELECT display_name, username, created_at FROM users WHERE family_id = ? ORDER BY id"); +$members->execute([$_SESSION['user']['family_id']]); +$members = $members->fetchAll(); + +$pageTitle = "Invitation — HouseHub"; +$activePage = "invite"; +require __DIR__ . '/header.php'; +?> + +
+ +

👨‍👩‍👧 Votre espace familial

+

Partagez le code ci-dessous pour inviter quelqu'un dans votre espace.

+ +
+

Famille :

+

Code d'invitation

+ + + +

✓ Copié !

+

+ Lien d'inscription : + /register.php +

+
+ +

Membres de l'espace ()

+
+ +
+
+ + @ +
+ +
+ +
+ +
+ + + + diff --git a/login.php b/login.php index 208c9f3..40d3258 100644 --- a/login.php +++ b/login.php @@ -1,7 +1,7 @@ prepare("SELECT id, username, password_hash, display_name FROM pf_users WHERE username = ?"); + $stmt = $meta_pdo->prepare(" + SELECT u.id, u.username, u.password_hash, u.display_name, u.family_id, f.db_name + FROM users u + LEFT JOIN families f ON f.id = u.family_id + WHERE u.username = ? + "); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if ($user && password_verify($password, $user['password_hash'])) { $_SESSION['user'] = [ - 'id' => (int)$user['id'], - 'username' => $user['username'], + 'id' => (int)$user['id'], + 'username' => $user['username'], 'display_name' => $user['display_name'], + 'family_id' => (int)$user['family_id'], ]; + $_SESSION['family_db'] = $user['db_name']; $redirectTo = $_GET['redirect'] ?? '/index.php'; header('Location: ' . $redirectTo); exit; @@ -75,7 +82,11 @@ require __DIR__ . '/header.php'; - + +

+ Pas encore de compte ? Créer un espace +

+ diff --git a/register.php b/register.php new file mode 100644 index 0000000..eb37f15 --- /dev/null +++ b/register.php @@ -0,0 +1,251 @@ + PDO::ERRMODE_EXCEPTION] + ); + + $root_pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); + $root_pdo->exec("USE `$db_name`"); + + // Exécuter le schéma instruction par instruction + foreach (array_filter(array_map('trim', explode(';', $schema))) as $stmt) { + if ($stmt !== '') { + try { $root_pdo->exec($stmt); } catch (\PDOException $e) { /* ignore DROP IF EXISTS warnings */ } + } + } + + return $db_name; +} + +// ─── Traitement du formulaire ───────────────────────────────────────────────── + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $action = $_POST['action'] ?? 'create'; + $username = trim($_POST['username'] ?? ''); + $display_name = trim($_POST['display_name'] ?? ''); + $password = $_POST['password'] ?? ''; + $password2 = $_POST['password2'] ?? ''; + $family_name = trim($_POST['family_name'] ?? ''); + $invite_code = trim($_POST['invite_code'] ?? ''); + + // Validations communes + if (!$username || !$display_name || !$password) { + $error = "Tous les champs sont obligatoires."; + } elseif (strlen($password) < 6) { + $error = "Le mot de passe doit faire au moins 6 caractères."; + } elseif ($password !== $password2) { + $error = "Les mots de passe ne correspondent pas."; + } elseif (!preg_match('/^[a-zA-Z0-9_.-]{3,50}$/', $username)) { + $error = "Nom d'utilisateur invalide (3-50 car., lettres/chiffres/._-)"; + } else { + // Vérifier que l'username n'existe pas + $chk = $meta_pdo->prepare("SELECT id FROM users WHERE username = ?"); + $chk->execute([$username]); + if ($chk->fetch()) { + $error = "Ce nom d'utilisateur est déjà pris."; + } + } + + if (!$error) { + $db_host = getenv('DB_HOST') ?: 'househub-db'; + $db_user = getenv('DB_USER') ?: 'househub'; + $db_pass = getenv('DB_PASS') ?: 'changeme'; + $hash = password_hash($password, PASSWORD_BCRYPT); + + try { + if ($action === 'join' && $invite_code) { + // ── Rejoindre une famille existante ────────────────────────── + $fam = $meta_pdo->prepare("SELECT * FROM families WHERE invite_code = ?"); + $fam->execute([$invite_code]); + $family = $fam->fetch(); + + if (!$family) { + $error = "Code d'invitation invalide."; + } else { + $meta_pdo->prepare( + "INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)" + )->execute([$username, $hash, $display_name, $family['id']]); + + $_SESSION['user'] = [ + 'id' => (int)$meta_pdo->lastInsertId(), + 'username' => $username, + 'display_name' => $display_name, + 'family_id' => (int)$family['id'], + ]; + $_SESSION['family_db'] = $family['db_name']; + header('Location: /index.php'); + exit; + } + + } else { + // ── Créer une nouvelle famille ──────────────────────────────── + if (!$family_name) { + $error = "Le nom de la famille est obligatoire."; + } else { + $meta_pdo->beginTransaction(); + + // Créer la famille avec un invite_code unique + $invite = bin2hex(random_bytes(8)); + $meta_pdo->prepare( + "INSERT INTO families (name, db_name, invite_code) VALUES (?, '', ?)" + )->execute([$family_name, $invite]); + $family_id = (int)$meta_pdo->lastInsertId(); + + // Créer la DB famille + $db_name = createFamilyDb($meta_pdo, $db_host, $db_user, $db_pass, $family_id); + + // Mettre à jour db_name maintenant qu'on a l'ID + $meta_pdo->prepare("UPDATE families SET db_name = ? WHERE id = ?") + ->execute([$db_name, $family_id]); + + // Créer l'utilisateur + $meta_pdo->prepare( + "INSERT INTO users (username, password_hash, display_name, family_id) VALUES (?, ?, ?, ?)" + )->execute([$username, $hash, $display_name, $family_id]); + $user_id = (int)$meta_pdo->lastInsertId(); + + $meta_pdo->commit(); + + $_SESSION['user'] = [ + 'id' => $user_id, + 'username' => $username, + 'display_name' => $display_name, + 'family_id' => $family_id, + ]; + $_SESSION['family_db'] = $db_name; + header('Location: /index.php'); + exit; + } + } + } catch (\Exception $e) { + if ($meta_pdo->inTransaction()) $meta_pdo->rollBack(); + $error = "Erreur lors de la création : " . $e->getMessage(); + } + } +} + +$pageTitle = "Inscription — HouseHub"; +$activePage = "register"; +require __DIR__ . '/header.php'; +?> + +
+
+ + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ + + + + +
+ +

+ Déjà un compte ? Se connecter +

+ +
+
+ + + +