Spaces:
Sleeping
Sleeping
Add team invite system: collaboratori accedono al sistema senza login
Browse files- GET /join/<token> → log accesso + sessione guest → redirect guest_dashboard
- GET /guest-dashboard → lista piani in read-only (no login)
- GET /view-public/<plan_id> → accetta anche team token da sessione
- POST /api/admin/team-invite → genera link invito (solo super_admin)
- Template guest_dashboard.html con lista piani navigabile
- Fix plan_viewer.js: rimuovi codice rotto revokeShare, pulisci submit handler
- Aggiunto campo "Nome collaboratore" nel modale condivisione piano
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.py +71 -2
- static/js/plan_viewer.js +33 -76
- templates/guest_dashboard.html +63 -0
- templates/strategic_plan_viewer.html +5 -13
app.py
CHANGED
|
@@ -1424,6 +1424,74 @@ def api_delete_guest_token(plan_id: str, token: str):
|
|
| 1424 |
return jsonify({"ok": True})
|
| 1425 |
|
| 1426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1427 |
@app.route("/preview/<token>")
|
| 1428 |
def guest_preview(token: str):
|
| 1429 |
"""Visualizza piano senza login tramite guest token."""
|
|
@@ -1446,9 +1514,10 @@ def guest_preview(token: str):
|
|
| 1446 |
@app.route("/view-public/<plan_id>")
|
| 1447 |
def view_plan_public(plan_id: str):
|
| 1448 |
"""Viewer pubblico per guest token — nessun login."""
|
| 1449 |
-
token = request.args.get("token", "")
|
| 1450 |
rec = knowledge_manager.store.get_guest_token(token) if token else None
|
| 1451 |
-
|
|
|
|
| 1452 |
return render_template("error.html", message="Accesso non autorizzato."), 403
|
| 1453 |
if rec.get("expires_at") and rec["expires_at"] < datetime.now().isoformat():
|
| 1454 |
return render_template("error.html", message="Link scaduto."), 403
|
|
|
|
| 1424 |
return jsonify({"ok": True})
|
| 1425 |
|
| 1426 |
|
| 1427 |
+
# ─── TEAM INVITE (accesso sistema intero senza login) ────────────────────────
|
| 1428 |
+
|
| 1429 |
+
@app.route("/api/admin/team-invite", methods=["POST"])
|
| 1430 |
+
@login_required
|
| 1431 |
+
def api_create_team_invite():
|
| 1432 |
+
"""Crea un token di invito team — solo super_admin."""
|
| 1433 |
+
if current_user.role != "super_admin":
|
| 1434 |
+
return jsonify({"error": "Forbidden"}), 403
|
| 1435 |
+
import uuid
|
| 1436 |
+
from datetime import timedelta
|
| 1437 |
+
data = request.get_json(silent=True) or {}
|
| 1438 |
+
label = data.get("label", "Team Rooting Future")
|
| 1439 |
+
days = int(data.get("expires_days", 90))
|
| 1440 |
+
token = "team_" + str(uuid.uuid4()).replace("-", "")
|
| 1441 |
+
expires_at = (datetime.now() + timedelta(days=days)).isoformat() if days > 0 else ""
|
| 1442 |
+
# Riusa la tabella guest_tokens con plan_id="__team__"
|
| 1443 |
+
knowledge_manager.store.create_guest_token(
|
| 1444 |
+
token=token, plan_id="__team__",
|
| 1445 |
+
owner_id=int(current_user.id),
|
| 1446 |
+
label=label, expires_at=expires_at,
|
| 1447 |
+
)
|
| 1448 |
+
link = url_for("team_access", token=token, _external=True)
|
| 1449 |
+
return jsonify({"token": token, "link": link, "label": label, "expires_at": expires_at})
|
| 1450 |
+
|
| 1451 |
+
|
| 1452 |
+
@app.route("/join/<token>")
|
| 1453 |
+
def team_access(token: str):
|
| 1454 |
+
"""Accesso al sistema per collaboratori tramite invite link (no login)."""
|
| 1455 |
+
rec = knowledge_manager.store.get_guest_token(token)
|
| 1456 |
+
if not rec or rec["plan_id"] != "__team__":
|
| 1457 |
+
return render_template("error.html", message="Link di invito non valido."), 404
|
| 1458 |
+
if rec.get("expires_at") and rec["expires_at"] < datetime.now().isoformat():
|
| 1459 |
+
return render_template("error.html", message="Link di invito scaduto."), 403
|
| 1460 |
+
# Log accesso
|
| 1461 |
+
knowledge_manager.store.log_guest_access(
|
| 1462 |
+
token=token, plan_id="__team__",
|
| 1463 |
+
ip=request.headers.get("X-Forwarded-For", request.remote_addr or ""),
|
| 1464 |
+
user_agent=request.user_agent.string or "",
|
| 1465 |
+
)
|
| 1466 |
+
# Salva token in sessione per navigazione successiva
|
| 1467 |
+
session["guest_team_token"] = token
|
| 1468 |
+
session["guest_label"] = rec.get("label", "")
|
| 1469 |
+
return redirect(url_for("guest_dashboard"))
|
| 1470 |
+
|
| 1471 |
+
|
| 1472 |
+
@app.route("/guest-dashboard")
|
| 1473 |
+
def guest_dashboard():
|
| 1474 |
+
"""Dashboard read-only per collaboratori senza account."""
|
| 1475 |
+
token = session.get("guest_team_token")
|
| 1476 |
+
if not token:
|
| 1477 |
+
return redirect(url_for("login"))
|
| 1478 |
+
rec = knowledge_manager.store.get_guest_token(token)
|
| 1479 |
+
if not rec or rec["plan_id"] != "__team__":
|
| 1480 |
+
session.pop("guest_team_token", None)
|
| 1481 |
+
return redirect(url_for("login"))
|
| 1482 |
+
if rec.get("expires_at") and rec["expires_at"] < datetime.now().isoformat():
|
| 1483 |
+
return render_template("error.html", message="Sessione scaduta."), 403
|
| 1484 |
+
# Carica tutti i piani del super_admin (owner_id del token)
|
| 1485 |
+
owner_id = rec["owner_id"]
|
| 1486 |
+
plans, _ = knowledge_manager.store.list_plans(owner_id=owner_id, limit=100)
|
| 1487 |
+
return render_template(
|
| 1488 |
+
"guest_dashboard.html",
|
| 1489 |
+
plans=plans,
|
| 1490 |
+
guest_label=rec.get("label", ""),
|
| 1491 |
+
token=token,
|
| 1492 |
+
)
|
| 1493 |
+
|
| 1494 |
+
|
| 1495 |
@app.route("/preview/<token>")
|
| 1496 |
def guest_preview(token: str):
|
| 1497 |
"""Visualizza piano senza login tramite guest token."""
|
|
|
|
| 1514 |
@app.route("/view-public/<plan_id>")
|
| 1515 |
def view_plan_public(plan_id: str):
|
| 1516 |
"""Viewer pubblico per guest token — nessun login."""
|
| 1517 |
+
token = request.args.get("token", "") or session.get("guest_team_token", "")
|
| 1518 |
rec = knowledge_manager.store.get_guest_token(token) if token else None
|
| 1519 |
+
# Accetta sia token specifico per piano sia team token (__team__)
|
| 1520 |
+
if not rec or (rec["plan_id"] != plan_id and rec["plan_id"] != "__team__"):
|
| 1521 |
return render_template("error.html", message="Accesso non autorizzato."), 403
|
| 1522 |
if rec.get("expires_at") and rec["expires_at"] < datetime.now().isoformat():
|
| 1523 |
return render_template("error.html", message="Link scaduto."), 403
|
static/js/plan_viewer.js
CHANGED
|
@@ -338,41 +338,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
| 338 |
const formData = new FormData(shareForm);
|
| 339 |
|
| 340 |
const data = {
|
|
|
|
| 341 |
expires_days: parseInt(formData.get('expires_days')),
|
| 342 |
-
password: formData.get('password') || null,
|
| 343 |
-
allow_download: formData.get('allow_download') === 'on'
|
| 344 |
};
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
fetch(`/share/${planId}`, {
|
| 349 |
method: 'POST',
|
| 350 |
-
headers: {
|
| 351 |
-
'Content-Type': 'application/json'
|
| 352 |
-
},
|
| 353 |
body: JSON.stringify(data)
|
| 354 |
})
|
| 355 |
-
.then(
|
| 356 |
.then(result => {
|
| 357 |
-
if (result.
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
// Show result
|
| 361 |
-
document.getElementById('shareUrl').value = result.share_url;
|
| 362 |
document.getElementById('shareResult').style.display = 'block';
|
| 363 |
-
|
| 364 |
-
// Reload active shares
|
| 365 |
loadActiveShares();
|
| 366 |
-
|
| 367 |
-
// Scroll to result
|
| 368 |
document.getElementById('shareResult').scrollIntoView({ behavior: 'smooth' });
|
| 369 |
} else {
|
| 370 |
-
alert('Errore
|
| 371 |
}
|
| 372 |
})
|
| 373 |
-
.catch(
|
| 374 |
-
console.error('[SHARE] Error
|
| 375 |
-
alert('Errore durante la creazione del link
|
| 376 |
});
|
| 377 |
});
|
| 378 |
}
|
|
@@ -410,71 +398,40 @@ function loadActiveShares() {
|
|
| 410 |
|
| 411 |
sharesList.innerHTML = '<p style="color: #6c757d; font-size: 14px;">Caricamento...</p>';
|
| 412 |
|
| 413 |
-
fetch(`/api/
|
| 414 |
-
.then(
|
| 415 |
-
.then(
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
if (shares.length === 0) {
|
| 419 |
-
sharesList.innerHTML = '<p style="color: #6c757d; font-size: 14px;">Nessun link attivo. Crea il primo!</p>';
|
| 420 |
return;
|
| 421 |
}
|
| 422 |
-
|
| 423 |
let html = '';
|
| 424 |
-
|
| 425 |
-
const
|
| 426 |
-
const
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
<
|
| 431 |
-
|
| 432 |
-
<small>
|
| 433 |
-
Creato: ${createdAt.toLocaleDateString('it-IT')} |
|
| 434 |
-
Scade: ${expiresAt.toLocaleDateString('it-IT')} |
|
| 435 |
-
Visualizzazioni: ${share.view_count}
|
| 436 |
-
${share.password_hash ? ' | 🔐 Protetto' : ''}
|
| 437 |
-
${share.allow_download ? ' | 📥 Download' : ''}
|
| 438 |
-
</small>
|
| 439 |
-
</div>
|
| 440 |
-
<button class="btn-revoke" onclick="revokeShare('${share.share_token}')">
|
| 441 |
-
🗑️ Revoca
|
| 442 |
-
</button>
|
| 443 |
</div>
|
| 444 |
-
|
|
|
|
| 445 |
});
|
| 446 |
-
|
| 447 |
sharesList.innerHTML = html;
|
| 448 |
-
console.log('[SHARE] Loaded', shares.length, 'active shares');
|
| 449 |
})
|
| 450 |
-
.catch(
|
| 451 |
-
|
| 452 |
-
sharesList.innerHTML = '<p style="color: #dc2626; font-size: 14px;">Errore nel caricamento dei link.</p>';
|
| 453 |
});
|
| 454 |
}
|
| 455 |
|
| 456 |
-
function revokeShare(shareToken) {
|
| 457 |
-
if (!confirm('
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
console.log('[SHARE] Revoking share:', shareToken);
|
| 462 |
-
|
| 463 |
-
fetch(`/api/shares/${shareToken}/revoke`, {
|
| 464 |
-
method: 'POST'
|
| 465 |
-
})
|
| 466 |
-
.then(response => response.json())
|
| 467 |
.then(result => {
|
| 468 |
-
if (result.
|
| 469 |
-
|
| 470 |
-
loadActiveShares();
|
| 471 |
-
} else {
|
| 472 |
-
alert('Errore durante la revoca: ' + (result.error || 'Errore sconosciuto'));
|
| 473 |
-
}
|
| 474 |
-
})
|
| 475 |
-
.catch(error => {
|
| 476 |
-
console.error('[SHARE] Error revoking share:', error);
|
| 477 |
-
alert('Errore durante la revoca del link.');
|
| 478 |
});
|
| 479 |
}
|
| 480 |
|
|
|
|
| 338 |
const formData = new FormData(shareForm);
|
| 339 |
|
| 340 |
const data = {
|
| 341 |
+
label: formData.get('label') || 'Collaboratore',
|
| 342 |
expires_days: parseInt(formData.get('expires_days')),
|
|
|
|
|
|
|
| 343 |
};
|
| 344 |
|
| 345 |
+
fetch(`/api/plans/${planId}/guest-tokens`, {
|
|
|
|
|
|
|
| 346 |
method: 'POST',
|
| 347 |
+
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
|
| 348 |
body: JSON.stringify(data)
|
| 349 |
})
|
| 350 |
+
.then(r => r.json())
|
| 351 |
.then(result => {
|
| 352 |
+
if (result.link) {
|
| 353 |
+
document.getElementById('shareUrl').value = result.link;
|
|
|
|
|
|
|
|
|
|
| 354 |
document.getElementById('shareResult').style.display = 'block';
|
|
|
|
|
|
|
| 355 |
loadActiveShares();
|
|
|
|
|
|
|
| 356 |
document.getElementById('shareResult').scrollIntoView({ behavior: 'smooth' });
|
| 357 |
} else {
|
| 358 |
+
alert('Errore: ' + (result.error || 'Sconosciuto'));
|
| 359 |
}
|
| 360 |
})
|
| 361 |
+
.catch(err => {
|
| 362 |
+
console.error('[SHARE] Error:', err);
|
| 363 |
+
alert('Errore durante la creazione del link.');
|
| 364 |
});
|
| 365 |
});
|
| 366 |
}
|
|
|
|
| 398 |
|
| 399 |
sharesList.innerHTML = '<p style="color: #6c757d; font-size: 14px;">Caricamento...</p>';
|
| 400 |
|
| 401 |
+
fetch(`/api/plans/${planId}/guest-tokens`)
|
| 402 |
+
.then(r => r.json())
|
| 403 |
+
.then(tokens => {
|
| 404 |
+
if (!tokens.length) {
|
| 405 |
+
sharesList.innerHTML = '<p style="color:#6c757d;font-size:14px;">Nessun link attivo. Crea il primo!</p>';
|
|
|
|
|
|
|
| 406 |
return;
|
| 407 |
}
|
|
|
|
| 408 |
let html = '';
|
| 409 |
+
tokens.forEach(t => {
|
| 410 |
+
const created = new Date(t.created_at).toLocaleDateString('it-IT');
|
| 411 |
+
const expires = t.expires_at ? new Date(t.expires_at).toLocaleDateString('it-IT') : '∞';
|
| 412 |
+
html += `<div class="share-item">
|
| 413 |
+
<div class="share-item-info">
|
| 414 |
+
<strong>${t.label || 'Collaboratore'}</strong>
|
| 415 |
+
<small>Creato: ${created} | Scade: ${expires} | Accessi: ${t.access_count || 0}
|
| 416 |
+
${t.last_access ? ' | Ultimo: ' + new Date(t.last_access).toLocaleString('it-IT') : ''}</small>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
</div>
|
| 418 |
+
<button class="btn-revoke" onclick="revokeShare('${t.token}','${planId}')">🗑️ Revoca</button>
|
| 419 |
+
</div>`;
|
| 420 |
});
|
|
|
|
| 421 |
sharesList.innerHTML = html;
|
|
|
|
| 422 |
})
|
| 423 |
+
.catch(() => {
|
| 424 |
+
sharesList.innerHTML = '<p style="color:#dc2626;font-size:14px;">Errore caricamento link.</p>';
|
|
|
|
| 425 |
});
|
| 426 |
}
|
| 427 |
|
| 428 |
+
function revokeShare(shareToken, planId) {
|
| 429 |
+
if (!confirm('Revocare questo link? Non sarà più accessibile.')) return;
|
| 430 |
+
fetch(`/api/plans/${planId}/guest-tokens/${shareToken}`, { method: 'DELETE' })
|
| 431 |
+
.then(r => r.json())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
.then(result => {
|
| 433 |
+
if (result.ok) loadActiveShares();
|
| 434 |
+
else alert('Errore revoca link');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
});
|
| 436 |
}
|
| 437 |
|
templates/guest_dashboard.html
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="it">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Rooting Future • Area Collaboratori</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root { --primary: #10b981; --bg: #0f172a; --card: #1e293b; --border: #334155; --text: #f1f5f9; --muted: #94a3b8; }
|
| 10 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 11 |
+
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; }
|
| 12 |
+
|
| 13 |
+
.header { background: var(--card); border-bottom: 1px solid var(--border); padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between; }
|
| 14 |
+
.logo { font-size: 1.2rem; font-weight: 800; color: var(--primary); }
|
| 15 |
+
.guest-badge { background: rgba(16,185,129,.15); color: var(--primary); padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; font-weight: 700; }
|
| 16 |
+
|
| 17 |
+
.container { max-width: 1100px; margin: 0 auto; padding: 2rem 1rem; }
|
| 18 |
+
h1 { font-size: 1.6rem; font-weight: 800; margin-bottom: 0.4rem; }
|
| 19 |
+
.subtitle { color: var(--muted); margin-bottom: 2rem; font-size: 0.9rem; }
|
| 20 |
+
|
| 21 |
+
.plans-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.2rem; }
|
| 22 |
+
.plan-card { background: var(--card); border: 1px solid var(--border); border-radius: 16px; padding: 1.5rem; text-decoration: none; color: inherit; display: block; transition: all .2s; }
|
| 23 |
+
.plan-card:hover { border-color: var(--primary); transform: translateY(-3px); }
|
| 24 |
+
.plan-club { font-size: 1.1rem; font-weight: 800; margin-bottom: 0.3rem; }
|
| 25 |
+
.plan-cat { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 1rem; }
|
| 26 |
+
.plan-date { font-size: 0.75rem; color: var(--muted); }
|
| 27 |
+
.plan-btn { display: inline-block; margin-top: 1rem; background: var(--primary); color: #000; padding: 0.5rem 1.2rem; border-radius: 8px; font-size: 0.8rem; font-weight: 700; }
|
| 28 |
+
|
| 29 |
+
.empty { text-align: center; color: var(--muted); padding: 4rem 0; }
|
| 30 |
+
|
| 31 |
+
@media(max-width:600px) { .plans-grid { grid-template-columns: 1fr; } }
|
| 32 |
+
</style>
|
| 33 |
+
</head>
|
| 34 |
+
<body>
|
| 35 |
+
<div class="header">
|
| 36 |
+
<div class="logo">🌱 Rooting Future</div>
|
| 37 |
+
<div class="guest-badge">👤 {{ guest_label or 'Collaboratore' }}</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div class="container">
|
| 41 |
+
<h1>Piani Strategici</h1>
|
| 42 |
+
<p class="subtitle">Accesso in sola lettura. Per modifiche contatta l'amministratore.</p>
|
| 43 |
+
|
| 44 |
+
{% if plans %}
|
| 45 |
+
<div class="plans-grid">
|
| 46 |
+
{% for plan in plans %}
|
| 47 |
+
<a href="/view-public/{{ plan.id }}?token={{ token }}" class="plan-card">
|
| 48 |
+
<div class="plan-club">{{ plan.club_name or plan.id }}</div>
|
| 49 |
+
<div class="plan-cat">{{ plan.category or '—' }}</div>
|
| 50 |
+
<div class="plan-date">{{ plan.created_at[:10] if plan.created_at else '' }}</div>
|
| 51 |
+
<div class="plan-btn">Visualizza Piano →</div>
|
| 52 |
+
</a>
|
| 53 |
+
{% endfor %}
|
| 54 |
+
</div>
|
| 55 |
+
{% else %}
|
| 56 |
+
<div class="empty">
|
| 57 |
+
<div style="font-size:3rem;margin-bottom:1rem">📋</div>
|
| 58 |
+
<p>Nessun piano disponibile al momento.</p>
|
| 59 |
+
</div>
|
| 60 |
+
{% endif %}
|
| 61 |
+
</div>
|
| 62 |
+
</body>
|
| 63 |
+
</html>
|
templates/strategic_plan_viewer.html
CHANGED
|
@@ -310,6 +310,11 @@
|
|
| 310 |
</p>
|
| 311 |
|
| 312 |
<form id="shareForm">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
<div class="form-group">
|
| 314 |
<label>Scadenza Link</label>
|
| 315 |
<select name="expires_days" class="form-control">
|
|
@@ -320,19 +325,6 @@
|
|
| 320 |
</select>
|
| 321 |
</div>
|
| 322 |
|
| 323 |
-
<div class="form-group">
|
| 324 |
-
<label>Password (opzionale)</label>
|
| 325 |
-
<input type="password" name="password" class="form-control" placeholder="Lascia vuoto per nessuna password">
|
| 326 |
-
<small>Aggiungi una password per maggiore sicurezza</small>
|
| 327 |
-
</div>
|
| 328 |
-
|
| 329 |
-
<div class="form-group checkbox-group">
|
| 330 |
-
<label>
|
| 331 |
-
<input type="checkbox" name="allow_download">
|
| 332 |
-
Permetti download PDF/DOCX
|
| 333 |
-
</label>
|
| 334 |
-
</div>
|
| 335 |
-
|
| 336 |
<button type="submit" class="btn btn-primary btn-block">
|
| 337 |
✨ Genera Link di Condivisione
|
| 338 |
</button>
|
|
|
|
| 310 |
</p>
|
| 311 |
|
| 312 |
<form id="shareForm">
|
| 313 |
+
<div class="form-group">
|
| 314 |
+
<label>Nome collaboratore</label>
|
| 315 |
+
<input type="text" name="label" class="form-control" placeholder="es. Mario Rossi, Staff Tecnico…" required>
|
| 316 |
+
</div>
|
| 317 |
+
|
| 318 |
<div class="form-group">
|
| 319 |
<label>Scadenza Link</label>
|
| 320 |
<select name="expires_days" class="form-control">
|
|
|
|
| 325 |
</select>
|
| 326 |
</div>
|
| 327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
<button type="submit" class="btn btn-primary btn-block">
|
| 329 |
✨ Genera Link di Condivisione
|
| 330 |
</button>
|