phase B (2/2): CSV upload endpoint + drag-drop UI
Completes Phase B. The full alternative-onboarding flow is now end-to-end: drop a T212 pie CSV → parser → InstrumentMap resolver → PortfolioSnapshot + Position rows, all without ever asking the user for broker credentials. - persist_pie() in app/services/csv_import.py: takes a ParsedPie, resolves each Slice via InstrumentMap, writes Portfolio + Snapshot + Position rows. Unmapped slices are still persisted using their CSV values and surfaced in the response for the UI to warn about. - POST /api/portfolios/upload: multipart endpoint accepting CSV file + optional portfolio_name + currency. 2 MiB cap. Returns import summary. - /upload page with drag-drop dropzone, file input fallback, and inline result panel showing invested/value/result + unmapped-slice warnings. - New "Import" link in the header nav. Verified end-to-end against the real T212 export: all 13 positions land with correct T212 tickers (incl. FPp_EQ for the Paris TotalEnergies listing the heuristic resolver picks), zero unmapped slices, totals reconcile to the penny. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
16e9f5f0cc
commit
8a155ef157
6 changed files with 439 additions and 12 deletions
|
|
@ -591,6 +591,113 @@ table.dense tr.row-stale td { color: var(--dim); }
|
|||
.log-meta__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0; margin-top: 6px; }
|
||||
.log-meta__row--dim { color: var(--dim); font-size: 10.5px; }
|
||||
|
||||
/* --- Upload page (drag-drop CSV) ------------------------------------- */
|
||||
|
||||
.dz {
|
||||
border: 2px dashed var(--border);
|
||||
background: var(--surface-2);
|
||||
padding: 36px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.dz:hover, .dz--over {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 6%, var(--surface-2));
|
||||
}
|
||||
.dz__icon {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 28px;
|
||||
color: var(--accent);
|
||||
letter-spacing: -2px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.dz__label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.dz__hint { color: var(--muted); font-size: 11.5px; margin-top: 4px; }
|
||||
.dz__hint a { color: var(--accent); }
|
||||
.dz__filename { margin-top: 10px; color: var(--accent); font-size: 12px; font-family: var(--font-mono); min-height: 1em; }
|
||||
|
||||
.form-row { display: grid; grid-template-columns: 180px 1fr; align-items: center; gap: 12px; padding: 6px 0; }
|
||||
.form-row label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
.form-row input[type="text"], .form-row select {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
outline: none;
|
||||
}
|
||||
.form-row input[type="text"]:focus, .form-row select:focus { border-color: var(--accent); }
|
||||
|
||||
#submit-btn {
|
||||
margin-top: 14px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--accent);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
padding: 8px 18px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
#submit-btn:hover:not(:disabled) { background: var(--accent); color: var(--bg); }
|
||||
#submit-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 4%, transparent);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
}
|
||||
.result--err { border-left-color: var(--negative); background: color-mix(in srgb, var(--negative) 5%, transparent); }
|
||||
.result__head {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--accent);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.result--err .result__head { color: var(--negative); }
|
||||
.result__tag {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
font-size: 9px;
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.result__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px 18px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.result__grid .k {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9.5px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.result__grid .v { font-size: 17px; color: var(--text); font-variant-numeric: tabular-nums; margin-top: 2px; }
|
||||
.result__grid .v.pos { color: var(--positive); }
|
||||
.result__grid .v.neg { color: var(--negative); }
|
||||
.result__row { color: var(--muted); font-size: 12px; margin-top: 6px; }
|
||||
.result__warn { color: var(--alert); font-size: 12px; margin-top: 4px; }
|
||||
.result__warn code { background: rgba(0,0,0,0.15); padding: 1px 4px; font-family: var(--font-mono); }
|
||||
|
||||
/* --- Chat sidebar ----------------------------------------------------- */
|
||||
|
||||
.chat-header {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue