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
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($acc['display_name'] ?: $acc['username']) ?>
+
+
+
= htmlspecialchars($acc['username']) ?>
+
+
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
+
+
+
+
+
+
+
= htmlspecialchars($acc['display_name'] ?: $acc['username']) ?>
+
+
= htmlspecialchars($acc['username']) ?>
+
+
+
+
+
+
+ Aucun poste disponible.
+
+
+
+
+
+
+
+
+
+
+
+
Sélectionnez vos documents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Document(s) envoyé(s)
+
+
+
+
+
+
+
+
+
+
+
+