Ajout du module gestion des actions et correction qq bugs
This commit is contained in:
+94
-7
@@ -6,7 +6,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
main = Blueprint('main', __name__)
|
main = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
# Route pour la page d'accueil
|
||||||
@main.route('/', methods=['GET', 'POST'])
|
@main.route('/', methods=['GET', 'POST'])
|
||||||
def index():
|
def index():
|
||||||
if 'user_id' in session:
|
if 'user_id' in session:
|
||||||
@@ -39,17 +39,20 @@ def index():
|
|||||||
|
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# Route pour la déconnexion
|
||||||
|
@main.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
# Route pour le menu principal
|
||||||
@main.route('/menu')
|
@main.route('/menu')
|
||||||
def menu():
|
def menu():
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
return redirect(url_for('main.index'))
|
return redirect(url_for('main.index'))
|
||||||
return render_template('menu.html')
|
return render_template('menu.html')
|
||||||
|
|
||||||
@main.route('/logout')
|
# Route pour la gestion des utilisateurs
|
||||||
def logout():
|
|
||||||
session.clear()
|
|
||||||
return redirect(url_for('main.index'))
|
|
||||||
|
|
||||||
@main.route('/gestion-utilisateurs', methods=['GET', 'POST'])
|
@main.route('/gestion-utilisateurs', methods=['GET', 'POST'])
|
||||||
def gestion_utilisateurs():
|
def gestion_utilisateurs():
|
||||||
if 'user_id' not in session:
|
if 'user_id' not in session:
|
||||||
@@ -136,4 +139,88 @@ def gestion_utilisateurs():
|
|||||||
'actif': row.actif if row.actif is not None else 1
|
'actif': row.actif if row.actif is not None else 1
|
||||||
})
|
})
|
||||||
|
|
||||||
return render_template('gestion_utilisateurs.html', users=users)
|
return render_template('gestion_utilisateurs.html', users=users)
|
||||||
|
|
||||||
|
# Route pour la gestion des actions
|
||||||
|
@main.route('/gestion-actions', methods=['GET', 'POST'])
|
||||||
|
def gestion_actions():
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('main.index'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
action_type = request.form.get('action')
|
||||||
|
# On récupère aussi l'id caché s'il s'agit d'une modification
|
||||||
|
action_id = request.form.get('id')
|
||||||
|
isin = request.form.get('isin')
|
||||||
|
ticker = request.form.get('ticker')
|
||||||
|
yahoo_code = request.form.get('yahoo_code')
|
||||||
|
company_name = request.form.get('company_name')
|
||||||
|
|
||||||
|
if action_type == 'creer':
|
||||||
|
if isin:
|
||||||
|
try:
|
||||||
|
# Vérifier si l'ISIN existe déjà
|
||||||
|
check_query = text("SELECT COUNT(*) FROM actions WHERE isin = :isin")
|
||||||
|
exists = db.session.execute(check_query, {"isin": isin}).scalar()
|
||||||
|
|
||||||
|
if exists > 0:
|
||||||
|
flash("L'action existe déjà, pas de création.", "warning")
|
||||||
|
else:
|
||||||
|
# L'id s'incrémente tout seul, on ne l'inclus pas dans l'INSERT
|
||||||
|
query = text("""
|
||||||
|
INSERT INTO actions (isin, ticker, yahoo_code, company_name, updated_at)
|
||||||
|
VALUES (:isin, :ticker, :yahoo_code, :company_name, NOW())
|
||||||
|
""")
|
||||||
|
db.session.execute(query, {
|
||||||
|
"isin": isin,
|
||||||
|
"ticker": ticker,
|
||||||
|
"yahoo_code": yahoo_code,
|
||||||
|
"company_name": company_name
|
||||||
|
})
|
||||||
|
db.session.commit()
|
||||||
|
flash("Action créée", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Erreur lors de la création de l'action.", "danger")
|
||||||
|
else:
|
||||||
|
flash("L'ISIN est obligatoire.", "danger")
|
||||||
|
|
||||||
|
elif action_type == 'modifier' and action_id:
|
||||||
|
try:
|
||||||
|
# On met à jour en se basant sur l'id technique unique
|
||||||
|
query = text("""
|
||||||
|
UPDATE actions
|
||||||
|
SET isin = :isin, ticker = :ticker, yahoo_code = :yahoo_code, company_name = :company_name, updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
""")
|
||||||
|
db.session.execute(query, {
|
||||||
|
"id": action_id,
|
||||||
|
"isin": isin,
|
||||||
|
"ticker": ticker,
|
||||||
|
"yahoo_code": yahoo_code,
|
||||||
|
"company_name": company_name
|
||||||
|
})
|
||||||
|
db.session.commit()
|
||||||
|
flash("Modification effectuée", "success")
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("Erreur lors de la modification (Vérifiez si l'ISIN n'est pas déjà utilisé par une autre ligne).", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('main.gestion_actions'))
|
||||||
|
|
||||||
|
# On sélectionne l'id en plus dans la requête
|
||||||
|
actions_query = text("SELECT id, isin, ticker, yahoo_code, company_name, updated_at FROM actions ORDER BY company_name ASC")
|
||||||
|
result = db.session.execute(actions_query)
|
||||||
|
|
||||||
|
actions_list = []
|
||||||
|
for row in result:
|
||||||
|
actions_list.append({
|
||||||
|
'id': row.id,
|
||||||
|
'isin': row.isin if row.isin else '',
|
||||||
|
'ticker': row.ticker if row.ticker else '',
|
||||||
|
'yahoo_code': row.yahoo_code if row.yahoo_code else '',
|
||||||
|
'company_name': row.company_name if row.company_name else 'Sans nom',
|
||||||
|
'updated_at': row.updated_at.strftime('%Y-%m-%d %H:%M:%S') if row.updated_at else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('gestion_actions.html', actions=actions_list)
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
{% extends "base.html" %} {% block title %}Gestion des actions - Bolsa{%
|
||||||
|
endblock %} {% block header_title %}Gestion actions{% endblock %} {% block
|
||||||
|
content %}
|
||||||
|
<div class="flex flex-col items-center justify-center py-6 relative">
|
||||||
|
<!-- Système de Toasts (Notifications flottantes centrées) -->
|
||||||
|
<div
|
||||||
|
id="toast-container"
|
||||||
|
class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 space-y-3 w-full max-w-md px-4 pointer-events-none"
|
||||||
|
>
|
||||||
|
{% set messages = get_flashed_messages(with_categories=true) %} {% if
|
||||||
|
messages %} {% for category, message in messages %}
|
||||||
|
<div
|
||||||
|
class="toast-message flex items-center px-4 py-3 rounded-xl shadow-2xl text-white text-sm font-medium transition-all duration-300 transform translate-y-0 opacity-100 {% if category == 'success' %} bg-emerald-600 border border-emerald-500 {% elif category == 'warning' %} bg-amber-600 border border-amber-500 {% else %} bg-rose-600 border border-rose-500 {% endif %}"
|
||||||
|
>
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %} {% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Box centrée principale -->
|
||||||
|
<div
|
||||||
|
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-6xl"
|
||||||
|
>
|
||||||
|
<!-- En-tête de section -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-extrabold text-white">
|
||||||
|
Administration des actions
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
Gérez le référentiel des actions et valeurs boursières.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.menu') }}"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-200 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Retour au menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille principale -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 py-4">
|
||||||
|
<!-- COLONNE GAUCHE : Recherche et Liste -->
|
||||||
|
<div
|
||||||
|
class="md:col-span-4 bg-gray-950 border border-gray-800 rounded-xl p-4 flex flex-col h-[560px]"
|
||||||
|
>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Rechercher (ISIN, Ticker, Nom)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
oninput="filtrerActions()"
|
||||||
|
placeholder="Filtrer..."
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3
|
||||||
|
class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2 px-1"
|
||||||
|
>
|
||||||
|
Actions enregistrées
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="overflow-y-auto flex-1 space-y-1 pr-1"
|
||||||
|
id="actions-list-container"
|
||||||
|
>
|
||||||
|
{% for a in actions %}
|
||||||
|
<div
|
||||||
|
onclick="selectionnerAction('{{ a.id }}', '{{ a.isin }}', '{{ a.ticker }}', '{{ a.yahoo_code }}', '{{ a.company_name | e }}', '{{ a.updated_at }}')"
|
||||||
|
class="action-item cursor-pointer px-3 py-2.5 rounded-lg {% if loop.first %}bg-blue-600 text-white{% else %}hover:bg-gray-800 text-gray-300{% endif %} font-medium text-sm transition flex flex-col justify-between"
|
||||||
|
data-id="{{ a.id }}"
|
||||||
|
data-isin="{{ a.isin }}"
|
||||||
|
data-ticker="{{ a.ticker | lower }}"
|
||||||
|
data-company="{{ a.company_name | lower }}"
|
||||||
|
data-search="{{ a.isin | lower }} {{ a.ticker | lower }} {{ a.company_name | lower }}"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<span class="truncate font-semibold pr-2"
|
||||||
|
>{{ a.company_name }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-xs {% if loop.first %}bg-blue-800{% else %}bg-gray-800 text-gray-400{% endif %} px-2 py-0.5 rounded whitespace-nowrap"
|
||||||
|
>{{ a.ticker or 'N/A' }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs opacity-70 font-mono mt-0.5"
|
||||||
|
>{{ a.isin }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COLONNE DROITE : Formulaire -->
|
||||||
|
<div
|
||||||
|
class="md:col-span-8 bg-gray-950 border border-gray-800 rounded-xl p-6 flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
id="action-form"
|
||||||
|
method="POST"
|
||||||
|
action=""
|
||||||
|
class="space-y-4 flex-1 flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<!-- ID technique caché pour les modifications -->
|
||||||
|
<input type="hidden" id="field-id" name="id" />
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3
|
||||||
|
class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2"
|
||||||
|
>
|
||||||
|
Détails de l'action
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- ISIN (Modifiable) -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>ISIN</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="field-isin"
|
||||||
|
name="isin"
|
||||||
|
required
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ticker -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Ticker</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="field-ticker"
|
||||||
|
name="ticker"
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<!-- Yahoo Code -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Yahoo Code</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="field-yahoo_code"
|
||||||
|
name="yahoo_code"
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Company Name -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Company Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="field-company_name"
|
||||||
|
name="company_name"
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Updated At -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Dernière mise à jour (Lecture seule)</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="field-updated_at"
|
||||||
|
name="updated_at"
|
||||||
|
readonly
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-gray-400 text-sm cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bas : Boutons -->
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-end space-x-3 pt-6 border-t border-gray-800 mt-6"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="preparerCreation()"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Nouveau / Vider
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="annulerAction()"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
value="creer"
|
||||||
|
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Créer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
value="modifier"
|
||||||
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic text-center"
|
||||||
|
>
|
||||||
|
Positions en bourse : Accès sécurisé. Veillez à fermer votre session après
|
||||||
|
chaque utilisation.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const toasts = document.querySelectorAll(".toast-message");
|
||||||
|
toasts.forEach((toast) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
toast.style.transform = "translateY(10px)";
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const premiereAction = document.querySelector(".action-item");
|
||||||
|
if (premiereAction) {
|
||||||
|
premiereAction.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectionnerAction(
|
||||||
|
id,
|
||||||
|
isin,
|
||||||
|
ticker,
|
||||||
|
yahoo_code,
|
||||||
|
company_name,
|
||||||
|
updated_at,
|
||||||
|
) {
|
||||||
|
document.getElementById("field-id").value = id;
|
||||||
|
document.getElementById("field-isin").value = isin;
|
||||||
|
document.getElementById("field-ticker").value = ticker;
|
||||||
|
document.getElementById("field-yahoo_code").value = yahoo_code;
|
||||||
|
document.getElementById("field-company_name").value = company_name;
|
||||||
|
document.getElementById("field-updated_at").value = updated_at;
|
||||||
|
|
||||||
|
document.querySelectorAll(".action-item").forEach((el) => {
|
||||||
|
el.classList.remove("bg-blue-600", "text-white");
|
||||||
|
el.classList.add("hover:bg-gray-800", "text-gray-300");
|
||||||
|
const badge = el.querySelector("span:nth-child(1) span:last-child");
|
||||||
|
if (badge) {
|
||||||
|
badge.classList.remove("bg-blue-800");
|
||||||
|
badge.classList.add("bg-gray-800", "text-gray-400");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedEl = document.querySelector(`[data-id="${id}"]`);
|
||||||
|
if (selectedEl) {
|
||||||
|
selectedEl.classList.remove("hover:bg-gray-800", "text-gray-300");
|
||||||
|
selectedEl.classList.add("bg-blue-600", "text-white");
|
||||||
|
const badge = selectedEl.querySelector(
|
||||||
|
"span:nth-child(1) span:last-child",
|
||||||
|
);
|
||||||
|
if (badge) {
|
||||||
|
badge.classList.remove("bg-gray-800", "text-gray-400");
|
||||||
|
badge.classList.add("bg-blue-800");
|
||||||
|
}
|
||||||
|
selectedEl.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparerCreation() {
|
||||||
|
document.getElementById("field-id").value = "";
|
||||||
|
document.getElementById("field-isin").value = "";
|
||||||
|
document.getElementById("field-ticker").value = "";
|
||||||
|
document.getElementById("field-yahoo_code").value = "";
|
||||||
|
document.getElementById("field-company_name").value = "";
|
||||||
|
document.getElementById("field-updated_at").value =
|
||||||
|
"Automatique à la création";
|
||||||
|
|
||||||
|
// Retirer la sélection visuelle active de la liste
|
||||||
|
document.querySelectorAll(".action-item").forEach((el) => {
|
||||||
|
el.classList.remove("bg-blue-600", "text-white");
|
||||||
|
el.classList.add("hover:bg-gray-800", "text-gray-300");
|
||||||
|
const badge = el.querySelector("span:nth-child(1) span:last-child");
|
||||||
|
if (badge) {
|
||||||
|
badge.classList.remove("bg-blue-800");
|
||||||
|
badge.classList.add("bg-gray-800", "text-gray-400");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtrerActions() {
|
||||||
|
const terme = document
|
||||||
|
.getElementById("search-input")
|
||||||
|
.value.toLowerCase()
|
||||||
|
.trim();
|
||||||
|
const items = document.querySelectorAll(".action-item");
|
||||||
|
let premierCorrespondant = null;
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const texteRecherche = item.getAttribute("data-search");
|
||||||
|
if (texteRecherche.includes(terme)) {
|
||||||
|
item.style.display = "flex";
|
||||||
|
if (!premierCorrespondant) {
|
||||||
|
premierCorrespondant = item;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (premierCorrespondant && terme !== "") {
|
||||||
|
premierCorrespondant.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function annulerAction() {
|
||||||
|
document.getElementById("search-input").value = "";
|
||||||
|
filtrerActions();
|
||||||
|
const premiereAction = document.querySelector(".action-item");
|
||||||
|
if (premiereAction) {
|
||||||
|
premiereAction.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
{% extends "base.html" %} {% block title %}Gestion des utilisateurs - Bolsa{%
|
{% extends "base.html" %} {% block title %}Gestion des utilisateurs - Bolsa{%
|
||||||
endblock %} {% block header_title %}Gestion utilisateurs{% endblock %} {% block
|
endblock %} {% block header_title %}Gestion utilisateurs{% endblock %} {% block
|
||||||
content %}
|
content %}
|
||||||
<div class="flex flex-col items-center justify-center py-6">
|
<div class="flex flex-col items-center justify-center py-6 relative">
|
||||||
|
<!-- Système de Toasts (Notifications flottantes centrées) -->
|
||||||
|
<div
|
||||||
|
id="toast-container"
|
||||||
|
class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50 space-y-3 w-full max-w-md px-4 pointer-events-none"
|
||||||
|
>
|
||||||
|
{% set messages = get_flashed_messages(with_categories=true) %} {% if
|
||||||
|
messages %} {% for category, message in messages %}
|
||||||
|
<div
|
||||||
|
class="toast-message pointer-events-auto flex items-center justify-center px-4 py-3 rounded-xl shadow-2xl text-white text-sm font-medium transition-all duration-300 transform translate-y-0 opacity-100 text-center {% if category == 'success' %} bg-emerald-600 border border-emerald-500 {% elif category == 'warning' %} bg-amber-600 border border-amber-500 {% else %} bg-rose-600 border border-rose-500 {% endif %}"
|
||||||
|
>
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %} {% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Box centrée principale -->
|
<!-- Box centrée principale -->
|
||||||
<div
|
<div
|
||||||
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-6xl"
|
class="bg-gray-900 border border-gray-800 p-8 rounded-2xl shadow-2xl w-full max-w-6xl"
|
||||||
@@ -15,7 +30,7 @@ content %}
|
|||||||
Administration des utilisateurs
|
Administration des utilisateurs
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-400 text-sm">
|
<p class="text-gray-400 text-sm">
|
||||||
Gérez les accès et les comptes autorisés de l'application.
|
Gérez les comptes d'accès à l'application.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -26,9 +41,9 @@ content %}
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grille principale : Gauche (Liste) / Droite (Formulaire) -->
|
<!-- Grille principale -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 py-4">
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 py-4">
|
||||||
<!-- COLONNE GAUCHE : Liste des utilisateurs réels de la BDD -->
|
<!-- COLONNE GAUCHE : Liste des utilisateurs -->
|
||||||
<div
|
<div
|
||||||
class="md:col-span-4 bg-gray-950 border border-gray-800 rounded-xl p-4 flex flex-col h-[520px]"
|
class="md:col-span-4 bg-gray-950 border border-gray-800 rounded-xl p-4 flex flex-col h-[520px]"
|
||||||
>
|
>
|
||||||
@@ -42,21 +57,23 @@ content %}
|
|||||||
<div class="overflow-y-auto flex-1 space-y-1 pr-1">
|
<div class="overflow-y-auto flex-1 space-y-1 pr-1">
|
||||||
{% for u in users %}
|
{% for u in users %}
|
||||||
<div
|
<div
|
||||||
onclick="selectionnerUtilisateur('{{ u.id }}', '{{ u.actif }}', '{{ u.prenom }}', '{{ u.nom }}', '{{ u.login }}', '{{ u.mail }}')"
|
onclick="selectionnerUtilisateur('{{ u.id }}', '{{ u.actif }}', '{{ u.prenom | e }}', '{{ u.nom | e }}', '{{ u.login | e }}', '{{ u.mail | e }}')"
|
||||||
class="user-item cursor-pointer px-3 py-2.5 rounded-lg {% if loop.first %}bg-blue-600 text-white{% else %}hover:bg-gray-800 text-gray-300{% endif %} font-medium text-sm transition flex justify-between items-center"
|
class="user-item cursor-pointer px-3 py-2.5 rounded-lg {% if loop.first %}bg-blue-600 text-white{% else %}hover:bg-gray-800 text-gray-300{% endif %} font-medium text-sm transition flex justify-between items-center"
|
||||||
data-id="{{ u.id }}"
|
data-id="{{ u.id }}"
|
||||||
>
|
>
|
||||||
<span>{{ u.prenom }} {{ u.nom }}</span>
|
<span class="truncate pr-2">{{ u.prenom }} {{ u.nom }}</span>
|
||||||
<span
|
<span
|
||||||
class="text-xs {% if loop.first %}bg-blue-800{% else %}bg-gray-800 text-gray-400{% endif %} px-2 py-0.5 rounded"
|
class="text-xs {% if loop.first %}bg-blue-800{% else %}bg-gray-800 text-gray-400{% endif %} px-2 py-0.5 rounded whitespace-nowrap"
|
||||||
>ID: {{ u.id }}</span
|
|
||||||
>
|
>
|
||||||
|
{% if u.actif == 1 or u.actif == True %}Actif{% else %}Inactif{%
|
||||||
|
endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- COLONNE DROITE : Formulaire des détails -->
|
<!-- COLONNE DROITE : Formulaire -->
|
||||||
<div
|
<div
|
||||||
class="md:col-span-8 bg-gray-950 border border-gray-800 rounded-xl p-6 flex flex-col justify-between"
|
class="md:col-span-8 bg-gray-950 border border-gray-800 rounded-xl p-6 flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
@@ -66,42 +83,16 @@ content %}
|
|||||||
action=""
|
action=""
|
||||||
class="space-y-4 flex-1 flex flex-col justify-between"
|
class="space-y-4 flex-1 flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
|
<!-- ID technique caché -->
|
||||||
|
<input type="hidden" id="field-id" name="id" />
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3
|
<h3
|
||||||
class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2"
|
class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-2"
|
||||||
>
|
>
|
||||||
Détails du compte
|
Détails de l'utilisateur
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<!-- ID (Lecture seule) -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-400 mb-1"
|
|
||||||
>ID (Lecture seule)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="field-id"
|
|
||||||
name="id"
|
|
||||||
readonly
|
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-gray-400 text-sm cursor-not-allowed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actif / Statut -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-400 mb-1"
|
|
||||||
>Actif (1 = Oui, 0 = Non)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="field-actif"
|
|
||||||
name="actif"
|
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<!-- Prénom -->
|
<!-- Prénom -->
|
||||||
<div>
|
<div>
|
||||||
@@ -112,6 +103,7 @@ content %}
|
|||||||
type="text"
|
type="text"
|
||||||
id="field-prenom"
|
id="field-prenom"
|
||||||
name="prenom"
|
name="prenom"
|
||||||
|
required
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,6 +117,7 @@ content %}
|
|||||||
type="text"
|
type="text"
|
||||||
id="field-nom"
|
id="field-nom"
|
||||||
name="nom"
|
name="nom"
|
||||||
|
required
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,6 +133,7 @@ content %}
|
|||||||
type="text"
|
type="text"
|
||||||
id="field-login"
|
id="field-login"
|
||||||
name="login"
|
name="login"
|
||||||
|
required
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,39 +141,60 @@ content %}
|
|||||||
<!-- Mail -->
|
<!-- Mail -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-400 mb-1"
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
>E-mail</label
|
>Email</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="field-mail"
|
id="field-mail"
|
||||||
name="mail"
|
name="mail"
|
||||||
|
required
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<!-- Mot de passe -->
|
<!-- Mot de passe -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-400 mb-1"
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
>Nouveau mot de passe (laisser vide pour ne pas
|
>Mot de passe (Laisser vide pour ne pas modifier)</label
|
||||||
changer)</label
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="field-pass"
|
id="field-pass"
|
||||||
name="pass"
|
name="pass"
|
||||||
placeholder="••••••••"
|
|
||||||
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Actif -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-400 mb-1"
|
||||||
|
>Statut</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="field-actif"
|
||||||
|
name="actif"
|
||||||
|
class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="1">Actif</option>
|
||||||
|
<option value="0">Inactif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bas : Trois boutons (Créer, Modifier, Annuler) -->
|
<!-- Bas : Boutons -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-end space-x-3 pt-6 border-t border-gray-800 mt-6"
|
class="flex items-center justify-end space-x-3 pt-6 border-t border-gray-800 mt-6"
|
||||||
>
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="preparerCreation()"
|
||||||
|
class="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm font-medium rounded-lg transition duration-200"
|
||||||
|
>
|
||||||
|
Nouveau / Vider
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick="annulerAction()"
|
onclick="annulerAction()"
|
||||||
@@ -208,7 +223,6 @@ content %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pied de page interne -->
|
|
||||||
<div
|
<div
|
||||||
class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic text-center"
|
class="border-t border-gray-800 pt-4 text-xs text-gray-400 italic text-center"
|
||||||
>
|
>
|
||||||
@@ -218,12 +232,28 @@ content %}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Script de gestion de la sélection et du premier utilisateur par défaut -->
|
|
||||||
<!-- Script de gestion de la sélection -->
|
|
||||||
<script>
|
<script>
|
||||||
|
// Disparition automatique des Toasts au bout de 4 secondes
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const toasts = document.querySelectorAll(".toast-message");
|
||||||
|
toasts.forEach((toast) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
toast.style.transform = "translateY(-10px)";
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
const premierUser = document.querySelector(".user-item");
|
||||||
|
if (premierUser) {
|
||||||
|
premierUser.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function selectionnerUtilisateur(id, actif, prenom, nom, login, mail) {
|
function selectionnerUtilisateur(id, actif, prenom, nom, login, mail) {
|
||||||
document.getElementById("field-id").value = id;
|
document.getElementById("field-id").value = id;
|
||||||
document.getElementById("field-actif").value = actif;
|
document.getElementById("field-actif").value =
|
||||||
|
actif == "True" || actif == "1" ? "1" : "0";
|
||||||
document.getElementById("field-prenom").value = prenom;
|
document.getElementById("field-prenom").value = prenom;
|
||||||
document.getElementById("field-nom").value = nom;
|
document.getElementById("field-nom").value = nom;
|
||||||
document.getElementById("field-login").value = login;
|
document.getElementById("field-login").value = login;
|
||||||
@@ -249,23 +279,37 @@ content %}
|
|||||||
badge.classList.remove("bg-gray-800", "text-gray-400");
|
badge.classList.remove("bg-gray-800", "text-gray-400");
|
||||||
badge.classList.add("bg-blue-800");
|
badge.classList.add("bg-blue-800");
|
||||||
}
|
}
|
||||||
|
selectedEl.scrollIntoView({ block: "nearest" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fonction pour vider le formulaire pour une création propre
|
||||||
|
function preparerCreation() {
|
||||||
|
document.getElementById("field-id").value = "";
|
||||||
|
document.getElementById("field-actif").value = "1";
|
||||||
|
document.getElementById("field-prenom").value = "";
|
||||||
|
document.getElementById("field-nom").value = "";
|
||||||
|
document.getElementById("field-login").value = "";
|
||||||
|
document.getElementById("field-mail").value = "";
|
||||||
|
document.getElementById("field-pass").value = "";
|
||||||
|
|
||||||
|
// Retirer la surbrillance de la liste
|
||||||
|
document.querySelectorAll(".user-item").forEach((el) => {
|
||||||
|
el.classList.remove("bg-blue-600", "text-white");
|
||||||
|
el.classList.add("hover:bg-gray-800", "text-gray-300");
|
||||||
|
const badge = el.querySelector("span:last-child");
|
||||||
|
if (badge) {
|
||||||
|
badge.classList.remove("bg-blue-800");
|
||||||
|
badge.classList.add("bg-gray-800", "text-gray-400");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function annulerAction() {
|
function annulerAction() {
|
||||||
// Sélectionne le tout premier élément de la liste s'il existe
|
|
||||||
const premierUser = document.querySelector(".user-item");
|
const premierUser = document.querySelector(".user-item");
|
||||||
if (premierUser) {
|
if (premierUser) {
|
||||||
premierUser.click();
|
premierUser.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sélectionner automatiquement le premier utilisateur au chargement de la page
|
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const premierUser = document.querySelector(".user-item");
|
|
||||||
if (premierUser) {
|
|
||||||
premierUser.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ header_title %}Tableau de bord{% endblock %} {% block content %}
|
|||||||
Accédez aux fonctions clés de l'application financière.
|
Accédez aux fonctions clés de l'application financière.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Grille principale : 2 colonnes pour les boutons -->
|
||||||
<!-- Grille des 8 gros boutons -->
|
<!-- Grille des 8 gros boutons -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-8">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="{{ url_for('main.gestion_actions') }}"
|
||||||
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
>
|
>
|
||||||
1. Actions
|
1. Gérer les actions
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
class="py-6 px-6 bg-gray-950 hover:bg-blue-600/30 border-2 border-gray-800 hover:border-blue-500 rounded-xl text-white text-lg font-bold tracking-wide transition-all duration-200 flex items-center justify-center shadow-lg hover:scale-[1.01]"
|
||||||
|
|||||||
Reference in New Issue
Block a user