From 8dd0e471fd3433f2dd3671c6a10181b3e577da5c Mon Sep 17 00:00:00 2001 From: Jules Date: Sat, 5 Sep 2026 15:13:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20t=C3=A9l=C3=A9versement=20mobile=20par?= =?UTF-8?q?=20QR=20code=20=E2=80=94=20alternative=20AirDrop=20pour=20clien?= =?UTF-8?q?ts=20en=20magasin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page publique /upload.php (mobile-first, sans auth) : le client scanne un QR code au comptoir, choisit le poste de destination, et envoie ses documents. Les fichiers sont déposés dans le share SMB du compte, enregistrés en BDD (mêmes tables que les mails) et déclenchent le webhook si configuré. Page admin /admin/qrcode.php pour générer/imprimer les QR codes par poste. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_017SChGCatJdbmZKCyYvTJMf --- web/admin/accounts.php | 6 + web/admin/qrcode.php | 106 +++++++++++ web/api/upload.php | 104 +++++++++++ web/upload.php | 407 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 623 insertions(+) create mode 100644 web/admin/qrcode.php create mode 100644 web/api/upload.php create mode 100644 web/upload.php diff --git a/web/admin/accounts.php b/web/admin/accounts.php index 4ea48cd..32a690a 100644 --- a/web/admin/accounts.php +++ b/web/admin/accounts.php @@ -123,6 +123,12 @@ html_navbar('accounts');
+ +
diff --git a/web/admin/qrcode.php b/web/admin/qrcode.php new file mode 100644 index 0000000..96fed9b --- /dev/null +++ b/web/admin/qrcode.php @@ -0,0 +1,106 @@ +query('SELECT id, username, display_name FROM accounts WHERE active = 1 ORDER BY username')->fetchAll(); + +html_head('QR Codes Upload'); +html_navbar('accounts'); +?> +
+
+
QR Codes — Téléversement mobile
+
+ + Comptes + + +
+
+ +
+ + Imprimez et affichez ces QR codes au comptoir. Le client scanne avec son téléphone et envoie ses documents directement. +
+ +
+ +
+
+
+
+ Tous les postes +
+

Le client choisit le poste sur la page

+
+ +
+
+
+ + +
+
+
+
+ + +
+ +

+ +

Accès direct à ce poste

+ +
+ +
+
+
+ +
+
+ + + + + + diff --git a/web/api/upload.php b/web/api/upload.php new file mode 100644 index 0000000..821f41a --- /dev/null +++ b/web/api/upload.php @@ -0,0 +1,104 @@ + 'Méthode non autorisée']); + exit; +} + +$accountId = (int)($_POST['account_id'] ?? 0); +if (!$accountId) { + http_response_code(400); + echo json_encode(['error' => 'Compte non spécifié']); + exit; +} + +$st = db()->prepare('SELECT id, username, smb_path, webhook_url FROM accounts WHERE id = ? AND active = 1'); +$st->execute([$accountId]); +$account = $st->fetch(); +if (!$account) { + http_response_code(404); + echo json_encode(['error' => 'Compte introuvable ou inactif']); + exit; +} + +if (empty($_FILES['files']) || !is_array($_FILES['files']['name'])) { + http_response_code(400); + echo json_encode(['error' => 'Aucun fichier envoyé']); + exit; +} + +$destDir = $account['smb_path']; +if (!is_dir($destDir)) mkdir($destDir, 0750, true); + +$st = db()->prepare('INSERT INTO source_file (account_id, title, sender, file_name) VALUES (?, ?, ?, ?)'); +$st->execute([$account['id'], 'Dépôt comptoir', 'Téléversement mobile', 'upload_' . time()]); +$sourceId = (int)db()->lastInsertId(); + +$uploaded = 0; +$errors = []; +$fileCount = count($_FILES['files']['name']); + +for ($i = 0; $i < $fileCount; $i++) { + if ($_FILES['files']['error'][$i] !== UPLOAD_ERR_OK) { + $errors[] = $_FILES['files']['name'][$i] . ' : erreur upload'; + continue; + } + + $origName = $_FILES['files']['name'][$i]; + $tmpPath = $_FILES['files']['tmp_name'][$i]; + + $safeName = preg_replace('/[^a-zA-Z0-9._\- ]/', '_', $origName); + $destPath = $destDir . '/' . $safeName; + + if (file_exists($destPath)) { + $ext = pathinfo($safeName, PATHINFO_EXTENSION); + $base = pathinfo($safeName, PATHINFO_FILENAME); + $destPath = $destDir . '/' . $base . '_' . time() . ($ext ? '.' . $ext : ''); + $safeName = basename($destPath); + } + + if (move_uploaded_file($tmpPath, $destPath)) { + chmod($destPath, 0644); + db()->prepare('INSERT INTO files (source_file_id, file_name, path) VALUES (?, ?, ?)') + ->execute([$sourceId, $safeName, $destPath]); + $uploaded++; + } else { + $errors[] = $origName . ' : échec écriture'; + } +} + +if ($uploaded === 0) { + db()->prepare('DELETE FROM source_file WHERE id = ?')->execute([$sourceId]); + http_response_code(500); + echo json_encode(['error' => 'Aucun fichier enregistré', 'details' => $errors]); + exit; +} + +if (!empty($account['webhook_url'])) { + $ctx = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\n", + 'content' => json_encode([ + 'account' => $account['username'], + 'subject' => 'Dépôt comptoir', + 'from' => 'Téléversement mobile', + 'source_id' => $sourceId, + 'files' => $uploaded, + 'received_at' => date('c'), + ]), + 'timeout' => 5, + 'ignore_errors' => true, + ]]); + @file_get_contents($account['webhook_url'], false, $ctx); +} + +echo json_encode([ + 'success' => true, + 'uploaded' => $uploaded, + 'account' => $account['username'], + 'errors' => $errors, +]); diff --git a/web/upload.php b/web/upload.php new file mode 100644 index 0000000..e8b8983 --- /dev/null +++ b/web/upload.php @@ -0,0 +1,407 @@ +query('SELECT id, username, display_name FROM accounts WHERE active = 1 ORDER BY username')->fetchAll(); +$preselect = $_GET['to'] ?? ''; +?> + + + + + + + Envoyer un document — CopyMail + + + + + +
+ CopyMail +
+ + +
+
+ Sélectionnez le poste +
+
+ +
+ +
+ + +
+ Aucun poste disponible. +
+ +
+
+ + + + + + + + +
+ +
+ + + + +