diff --git a/css/actions.css b/css/actions.css new file mode 100644 index 0000000..85ed44f --- /dev/null +++ b/css/actions.css @@ -0,0 +1,492 @@ +/* Actions page specific styles - Dark Mode financial UI */ + +body.actions-page { + --actions-header-height: 76px; + --actions-titlebar-height: 80px; + --actions-form-height: 220px; + --actions-footer-height: 64px; + --actions-gap: 10px; + background: #070a12; + color: #edf2ff; + font-family: 'Inter', 'Segoe UI', Arial, sans-serif; + min-height: 100vh; + display: flex; + flex-direction: column; + margin: 0; +} + +body.actions-page header.header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1003; + width: 100%; +} + +.actions-header { + position: fixed; + top: var(--actions-header-height); + left: 0; + right: 0; + z-index: 1002; + width: 100%; + padding: 20px; + background: rgba(7, 10, 18, 0.98); + border-bottom: 1px solid rgba(84, 138, 255, 0.14); +} + +body.actions-page main.main-content { + flex: 1 0 auto; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: stretch; + min-height: 0; + padding-top: calc(var(--actions-header-height) + var(--actions-titlebar-height) + var(--actions-form-height) + var(--actions-gap)); + padding-bottom: calc(var(--actions-footer-height) + 20px); +} + +body.actions-page .footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1002; + width: 100%; +} + +#formulaire-creation { + position: fixed; + top: calc(var(--actions-header-height) + var(--actions-titlebar-height)); + left: 0; + right: 0; + z-index: 1001; + width: 100%; + padding: 20px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.32); + background: rgba(7, 10, 18, 0.98); +} + +#formulaire-creation .actions-panel { + margin: 0 auto; + width: min(1200px, 100%); +} + +.table-container { + width: min(1200px, 100%); + margin: 0 auto; + padding: 0 20px; +} + +.actions-table-wrapper { + height: calc(100vh - var(--actions-header-height) - var(--actions-form-height) - var(--actions-footer-height) - 40px); + overflow-y: auto; + overflow-x: auto; +} + +.actions-shell { + width: 100%; + flex: 1 1 auto; + min-height: 0; + padding: 0 20px 28px; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.actions-container { + width: min(1200px, 100%); + margin: 0 auto; + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + gap: 24px; +} + +.actions-card { + display: flex; + flex-direction: column; + min-height: 0; +} + +.actions-table-wrapper { + overflow-x: auto; + min-height: 0; +} + +.actions-card:last-child { + display: flex; + flex-direction: column; + min-height: 0; +} + +.actions-table-wrapper { + overflow-x: auto; + overflow-y: auto; + flex: 1 1 auto; + min-height: 0; +} + +.actions-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: nowrap; +} + +.actions-header .actions-title { + margin: 0; + font-size: clamp(1.8rem, 2.5vw, 2.8rem); + letter-spacing: -0.04em; + white-space: nowrap; +} + +.actions-header .action-button { + width: auto; + min-width: 170px; + max-width: 260px; + flex-shrink: 0; +} + +.actions-subtitle { + color: #94a7d6; + margin: 4px 0 0; +} + +.actions-card, +.actions-panel { + background: rgba(12, 18, 34, 0.96); + border: 1px solid rgba(84, 138, 255, 0.18); + border-radius: 24px; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.22); + padding: 28px; +} + +.actions-panel { + display: grid; + grid-template-columns: 1fr; + gap: 18px; + transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease; +} + +.actions-panel.edit-mode { + border-color: rgba(120, 211, 141, 0.35); + box-shadow: 0 28px 90px rgba(46, 160, 95, 0.18); + background: rgba(18, 42, 22, 0.95); +} + +.actions-panel label { + display: flex; + flex-direction: column; + gap: 10px; + color: #d3e1ff; + font-size: 0.95rem; +} + +.actions-panel input, +.actions-panel select { + background: rgba(15, 22, 41, 0.98); + border: 1px solid rgba(96, 156, 255, 0.18); + border-radius: 14px; + color: #edf2ff; + padding: 14px 16px; + font-size: 1rem; + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.actions-panel input:focus, +.actions-panel select:focus { + border-color: rgba(89, 151, 255, 0.65); + box-shadow: 0 0 0 4px rgba(64, 126, 255, 0.14); +} + +.actions-panel-footer { + margin-top: 12px !important; + display: grid; + grid-auto-flow: column; + justify-content: end; + gap: 12px; + grid-auto-columns: minmax(140px, max-content); + align-items: center; +} + +.btn-action, +.actions-panel-footer button { + width: 90%; + max-width: 190px; + min-width: 120px; + box-sizing: border-box; + height: 46px; + padding: 0 18px; + border-radius: 999px; + white-space: nowrap; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.95rem; + transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease, opacity 0.2s ease; +} + +.actions-panel-footer button:hover, +.btn-action:hover { + transform: translateY(-1px); +} + +.actions-panel-footer button:disabled, +.btn-action:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-action:focus-visible { + outline: 3px solid rgba(94, 154, 255, 0.45); + outline-offset: 2px; +} + +.btn-primary { + border: 1px solid rgba(94, 154, 255, 0.35); + background: linear-gradient(135deg, rgba(72, 124, 255, 0.18), rgba(26, 50, 103, 0.95)); + color: #f7fbff; +} + +.btn-action.update-button, +.btn-primary.update-button { + border-color: rgba(96, 204, 129, 0.35); + background: linear-gradient(135deg, rgba(86, 221, 146, 0.18), rgba(50, 90, 72, 0.95)); +} + +.btn-cancel { + background: rgba(56, 66, 86, 0.95); + border: 1px solid rgba(99, 110, 133, 0.32); + color: #d7e6ff; +} + +.cancel-edit-button { + border-color: rgba(99, 110, 133, 0.32); +} + +.actions-table-wrapper { + overflow-x: auto; + max-height: 100%; +} + +.actions-table { + width: 100%; + border-collapse: collapse; + min-width: 720px; +} + +.actions-table th, +.actions-table td { + padding: 12px 14px; + border-bottom: 1px solid rgba(93, 142, 218, 0.12); +} + +.actions-table th { + text-align: left; + font-size: 0.95rem; + color: #c7d4f3; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.actions-table td { + color: #e2ecff; + font-size: 0.96rem; +} + +.actions-table th:nth-child(5), +.actions-table td:nth-child(5) { + min-width: 180px; + max-width: 220px; + white-space: nowrap; +} + +.actions-table td:nth-child(5) { + overflow: hidden; + text-overflow: ellipsis; +} + +.actions-table td .readonly-value { + display: inline-block; + width: 100%; +} + +.actions-table td .edit-field { + display: none; + width: 100%; + box-sizing: border-box; +} + +.actions-table tr.editing .readonly-value { + display: none; +} + +.actions-table tr.editing .edit-field { + display: inline-block; +} + +.modal-backdrop { + display: grid; +} + +.actions-table tbody tr:hover { + background: rgba(102, 145, 255, 0.08); +} + +.edit-action-button, +.action-button, +.cancel-edit-button, +.actions-submit { + width: 100%; + min-height: 41px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + cursor: pointer; + transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease, opacity 0.2s ease; +} + +.edit-action-button { + height: 38px; + min-height: 38px; + padding: 0 16px; + border: 1px solid rgba(94, 154, 255, 0.35); + background: rgba(22, 45, 96, 0.95); + color: #f7fbff; + font-size: 0.95rem; + font-weight: 700; +} + +.action-button, +.cancel-edit-button { + padding: 8px 14px; + border: 1px solid rgba(100, 159, 255, 0.22); + background: rgba(24, 34, 62, 0.98); + color: #d7e6ff; + font-size: 0.9rem; + text-decoration: none; +} + +.edit-action-button:hover, +.action-button:hover, +.cancel-edit-button:hover, +.actions-submit:hover { + transform: translateY(-1px); + background: rgba(72, 122, 255, 0.16); +} + +.action-form-row { + display: grid; + grid-template-columns: 25% 12.5% 12.5% 50%; + gap: 16px; + align-items: flex-end; +} + +.action-form-row label { + margin-bottom: 0; +} + +.field-isin { + grid-column: span 1; +} + +.field-ticker, +.field-yahoo { + grid-column: span 1; +} + +.field-company { + grid-column: span 1; +} + +@media (max-width: 1200px) { + .action-form-row { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 768px) { + .action-form-row { + grid-template-columns: 1fr; + } +} + +.form-error, +#action-form-feedback { + color: #ff7c93; + font-size: 0.92rem; + margin-top: 8px; +} + +.modal-backdrop { + position: fixed; + inset: 0; + display: grid; + place-items: center; + background: rgba(4, 8, 20, 0.8); + backdrop-filter: blur(6px); + z-index: 10000; +} + +.modal-panel { + width: min(520px, calc(100% - 40px)); + background: rgba(10, 14, 28, 0.98); + border: 1px solid rgba(92, 156, 255, 0.22); + border-radius: 24px; + padding: 28px; + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45); +} + +.modal-panel h2 { + margin: 0 0 16px; + font-size: 1.4rem; + color: #eaf4ff; +} + +.modal-copy { + margin: 0 0 24px; + color: #b8c9ff; + line-height: 1.7; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + flex-wrap: wrap; +} + +.modal-panel .action-button, +.modal-panel .actions-submit { + min-width: 120px; +} + +@media (max-width: 900px) { + .actions-panel { + grid-template-columns: 1fr; + } + + .action-form-row { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .actions-card, + .actions-panel { + padding: 20px; + } + + .actions-table th, + .actions-table td { + padding: 12px 10px; + } +} diff --git a/css/style.css b/css/style.css index 5031d9e..5b92e77 100755 --- a/css/style.css +++ b/css/style.css @@ -168,6 +168,7 @@ main { justify-content: center; background: rgba(18, 24, 42, 0.98); border-top: 1px solid rgba(255, 255, 255, 0.06); + flex-shrink: 0; } .footer-logout { diff --git a/js/actions.js b/js/actions.js new file mode 100644 index 0000000..7f0f5c6 --- /dev/null +++ b/js/actions.js @@ -0,0 +1,134 @@ +document.addEventListener('DOMContentLoaded', () => { + const actionForm = document.getElementById('action-form'); + const actionIsinInput = document.querySelector('input[name="action_isin"]'); + const isinInput = document.getElementById('action-isin'); + const tickerInput = document.getElementById('action-ticker'); + const yahooInput = document.getElementById('action-yahoo_code'); + const companyInput = document.getElementById('action-company_name'); + const feedbackMessage = document.getElementById('action-form-feedback'); + const createButton = document.querySelector('.create-button'); + const updateButton = document.querySelector('.update-button'); + const cancelEditButton = document.querySelector('.cancel-edit-button'); + const actionsPanel = document.querySelector('.actions-panel'); + const editActionButtons = document.querySelectorAll('.edit-action-button'); + + const getExistingActions = () => Array.from(document.querySelectorAll('.action-row')).map((row) => ({ + isin: row.dataset.isin || '', + ticker: row.dataset.ticker || '', + yahoo_code: row.dataset.yahoo_code || '', + company_name: row.dataset.company_name || '', + })); + + const setFeedback = (message) => { + if (!feedbackMessage) { + return; + } + feedbackMessage.textContent = message; + feedbackMessage.style.display = message ? 'block' : 'none'; + }; + + const validateDuplicateFields = () => { + const existingActions = getExistingActions(); + const currentActionIsin = actionIsinInput.value.trim(); + const currentIsin = isinInput.value.trim(); + const currentTicker = tickerInput.value.trim(); + const currentYahoo = yahooInput.value.trim(); + + const duplicateFields = []; + + const isDuplicate = (fieldName, value) => { + if (!value) { + return false; + } + return existingActions.some((entry) => { + if (!entry[fieldName]) { + return false; + } + if (currentActionIsin && entry.isin === currentActionIsin) { + return false; + } + return entry[fieldName].localeCompare(value, undefined, { sensitivity: 'base' }) === 0; + }); + }; + + if (isDuplicate('isin', currentIsin)) { + duplicateFields.push('ISIN'); + } + if (isDuplicate('ticker', currentTicker)) { + duplicateFields.push('symbole'); + } + if (isDuplicate('yahoo_code', currentYahoo)) { + duplicateFields.push('code Yahoo'); + } + + if (duplicateFields.length > 0) { + const joined = duplicateFields.join(', '); + setFeedback(`Doublon détecté pour le(s) champ(s) : ${joined}. Veuillez modifier la saisie.`); + createButton.disabled = true; + updateButton.disabled = true; + return false; + } + + setFeedback(''); + createButton.disabled = false; + updateButton.disabled = false; + return true; + }; + + const resetFormMode = () => { + actionForm.reset(); + actionIsinInput.value = ''; + createButton.removeAttribute('disabled'); + createButton.hidden = false; + updateButton.hidden = true; + cancelEditButton.hidden = true; + isinInput.removeAttribute('readonly'); + isinInput.removeAttribute('disabled'); + actionsPanel.classList.remove('edit-mode'); + setFeedback(''); + }; + + const setEditMode = (row) => { + const isin = row.dataset.isin || ''; + const ticker = row.dataset.ticker || ''; + const yahooCode = row.dataset.yahoo_code || ''; + const companyName = row.dataset.company_name || ''; + + actionIsinInput.value = isin; + isinInput.value = isin; + tickerInput.value = ticker; + yahooInput.value = yahooCode; + companyInput.value = companyName; + + createButton.setAttribute('disabled', 'disabled'); + createButton.hidden = true; + updateButton.hidden = false; + cancelEditButton.hidden = false; + isinInput.setAttribute('readonly', 'readonly'); + actionsPanel.classList.add('edit-mode'); + validateDuplicateFields(); + }; + + const handleInputChange = () => { + validateDuplicateFields(); + }; + + editActionButtons.forEach((button) => { + button.addEventListener('click', () => { + const row = button.closest('tr'); + if (!row) { + return; + } + setEditMode(row); + isinInput.focus(); + }); + }); + + [isinInput, tickerInput, yahooInput].forEach((input) => { + input.addEventListener('input', handleInputChange); + }); + + cancelEditButton.addEventListener('click', resetFormMode); + + resetFormMode(); +}); diff --git a/php/actions.php b/php/actions.php new file mode 100644 index 0000000..0564d1e --- /dev/null +++ b/php/actions.php @@ -0,0 +1,159 @@ + trim($_POST['isin'] ?? ''), + 'ticker' => trim($_POST['ticker'] ?? ''), + 'yahoo_code' => trim($_POST['yahoo_code'] ?? ''), + 'company_name' => trim($_POST['company_name'] ?? ''), + ]; + + if ($newAction['isin'] === '' || $newAction['ticker'] === '' || $newAction['company_name'] === '') { + $creationError = 'Veuillez remplir tous les champs obligatoires.'; + } else { + if ($actionsManager->createAction($newAction)) { + $creationMessage = 'Nouvelle action ajoutée avec succès.'; + } else { + $creationError = 'Impossible d’ajouter l’action, veuillez réessayer.'; + } + } +} + +// Traitement du formulaire de mise à jour d'une action. +$updateMessage = null; +$updateError = null; +if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action_update'], $_POST['action_isin'])) { + $actionIsin = trim($_POST['action_isin']); + $updateData = [ + 'ticker' => trim($_POST['ticker'] ?? ''), + 'yahoo_code' => trim($_POST['yahoo_code'] ?? ''), + 'company_name' => trim($_POST['company_name'] ?? ''), + ]; + + if ($actionIsin === '' || $updateData['ticker'] === '' || $updateData['company_name'] === '') { + $updateError = 'Tous les champs requis doivent être remplis pour mettre à jour cette action.'; + } else { + if ($actionsManager->updateAction($actionIsin, $updateData)) { + $updateMessage = 'Action mise à jour avec succès.'; + } else { + $updateError = 'Impossible de mettre à jour l’action. Vérifiez les données et réessayez.'; + } + } +} + +$actions = $actionsManager->listActions(); + +require_once __DIR__ . '/header.php'; +?> + +
+
+
+

Actions financières

+ Retour au menu +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ +
+ + + + +
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
ISINSymboleCode YahooSociétéMise à jourActions
+ +
+
+
+
+
+ + + + diff --git a/php/db.php b/php/db.php index 5d6c7ee..d07101d 100644 --- a/php/db.php +++ b/php/db.php @@ -364,3 +364,103 @@ class LoginManager return $statement->execute(); } } + +/** + * Class ActionsManager + * + * Opérations CRUD sur la table `actions`. + */ +class ActionsManager +{ + private PDO $connection; + + public function __construct(Database $database) + { + $this->connection = $database->getConnection(); + } + + /** + * Retourne toutes les actions. + * + * @return array + */ + public function listActions(): array + { + $statement = $this->connection->query( + 'SELECT isin, ticker, yahoo_code, company_name, updated_at + FROM actions + ORDER BY updated_at DESC' + ); + + return $statement->fetchAll(); + } + + /** + * Retourne une action par son ISIN. + * + * @param string $isin + * @return array|null + */ + public function getActionByIsin(string $isin): ?array + { + $statement = $this->connection->prepare( + 'SELECT isin, ticker, yahoo_code, company_name, updated_at + FROM actions + WHERE isin = :isin' + ); + + $statement->bindValue(':isin', $isin, PDO::PARAM_STR); + $statement->execute(); + + $result = $statement->fetch(); + + return $result === false ? null : $result; + } + + /** + * Crée une nouvelle action. + * + * @param array $data + * @return bool + */ + public function createAction(array $data): bool + { + $statement = $this->connection->prepare( + 'INSERT INTO actions (isin, ticker, yahoo_code, company_name, updated_at) + VALUES (:isin, :ticker, :yahoo_code, :company_name, NOW())' + ); + + $statement->bindValue(':isin', $data['isin'], PDO::PARAM_STR); + $statement->bindValue(':ticker', $data['ticker'], PDO::PARAM_STR); + $statement->bindValue(':yahoo_code', $data['yahoo_code'], PDO::PARAM_STR); + $statement->bindValue(':company_name', $data['company_name'], PDO::PARAM_STR); + + return $statement->execute(); + } + + /** + * Met à jour une action. + * + * @param string $isin + * @param array $data + * @return bool + */ + public function updateAction(string $isin, array $data): bool + { + $statement = $this->connection->prepare( + 'UPDATE actions + SET ticker = :ticker, + yahoo_code = :yahoo_code, + company_name = :company_name, + updated_at = NOW() + WHERE isin = :isin' + ); + + $statement->bindValue(':ticker', $data['ticker'], PDO::PARAM_STR); + $statement->bindValue(':yahoo_code', $data['yahoo_code'], PDO::PARAM_STR); + $statement->bindValue(':company_name', $data['company_name'], PDO::PARAM_STR); + $statement->bindValue(':isin', $isin, PDO::PARAM_STR); + + return $statement->execute(); + } +} diff --git a/php/menu.php b/php/menu.php index f5ba70b..756b8e1 100755 --- a/php/menu.php +++ b/php/menu.php @@ -31,7 +31,7 @@ require_once __DIR__ . '/header.php'; @@ -39,7 +39,7 @@ require_once __DIR__ . '/header.php'; @@ -47,7 +47,7 @@ require_once __DIR__ . '/header.php'; @@ -55,7 +55,7 @@ require_once __DIR__ . '/header.php'; @@ -63,7 +63,7 @@ require_once __DIR__ . '/header.php'; @@ -71,7 +71,7 @@ require_once __DIR__ . '/header.php';