Bladeren bron

update portfolio page

Daniel Bohry 9 maanden geleden
bovenliggende
commit
c9661839bc
1 gewijzigde bestanden met toevoegingen van 463 en 74 verwijderingen
  1. 463 74
      src/routes/portfolio/+page.svelte

+ 463 - 74
src/routes/portfolio/+page.svelte

@@ -1,14 +1,42 @@
 <script>
+    import {authentication} from "../store.js";
+    import {onMount} from "svelte";
+    import {fade} from 'svelte/transition';
+
+    let portfolioId = undefined;
     let result = [];
+    let totalValue = 0;
+    let totalAssets = 0;
+    let authToken;
+    let isLoading = true;
+    let showModal = false;
+    let searchStockResult = [];
+
+    onMount(() => {
+        const unsubscribe = authentication.subscribe(value => {
+            if (value?.token) {
+                authToken = value.token;
+                fetchPortfolio();
+            }
+        });
 
-    async function getPortfolio(code) {
+        return () => unsubscribe();
+    });
+
+    async function fetchPortfolio() {
         try {
-            const response = await fetch(import.meta.env.VITE_STOCKS_HOST + '/api/portfolios/' + code, {
-                method: 'GET'
-            });
+            const response = await fetch(
+                `${import.meta.env.VITE_STOCKS_HOST}/api/portfolios`,
+                {
+                    method: 'GET',
+                    headers: {
+                        Authorization: 'Bearer ' + authToken
+                    }
+                }
+            );
 
             if (response.ok) {
-                return await response.json();
+                await update(response.json())
             } else {
                 const error = await response.json();
                 console.error('Failed to find portfolio info:', error);
@@ -17,24 +45,126 @@
         } catch (err) {
             console.error('Failed to find portfolio info', err);
             alert('Failed to find portfolio info');
+        } finally {
+            isLoading = false;
         }
     }
 
-    async function search(id) {
-        const portfolio = await getPortfolio("066b47a9-46be-487f-bcc4-f35835d0ca02");
+    async function update(response) {
+        const portfolio = await response;
+        if (portfolio?.length > 0) {
+            result = portfolio[0].stocks;
+            totalValue = portfolio[0].totalValue;
+            totalAssets = portfolio[0].totalAssets;
+            portfolioId = portfolio[0].id;
+        }
+    }
 
-        console.log(portfolio)
+    async function updatePortfolio(stocks) {
+        try {
+            const response = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios/${portfolioId}`, {
+                method: 'PUT',
+                headers: {
+                    Authorization: 'Bearer ' + authToken,
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({stocks})
+            });
 
-        if (portfolio !== undefined && portfolio.stocks.length !== 0) {
-            for (const stock of portfolio.stocks) {
-                const stockInfo = stock
-                if (stockInfo) {
-                    result = [...result, stockInfo];
-                }
+            if (response.status === 400) {
+                alert("Bad request. Invalid code.");
+                return;
             }
+
+            await fetchPortfolio();
+        } catch (err) {
+            console.error('Update failed', err);
+        }
+    }
+
+    async function searchStock(code) {
+        try {
+            const res = await fetch(`${import.meta.env.VITE_STOCKS_HOST}/api/stocks?q=${code}`);
+            return await res.json();
+        } catch (err) {
+            console.error("Search error:", err);
+            return [];
         }
     }
 
+    async function handleSubmit(e) {
+        e.preventDefault();
+        const code = new FormData(e.target).get("stock_code").toUpperCase();
+
+        const data = await searchStock(code);
+        if (!data || data.length === 0) {
+            alert("Stock not found.");
+            return;
+        }
+
+        searchStockResult = data;
+        const alreadyInPortfolio = result.some(s => s.code === data[0]?.code);
+
+        if (data.length === 1 && !alreadyInPortfolio) {
+            await addSelectedStock(data[0]);
+            closeOrOpenModal();
+        }
+    }
+
+    async function addSelectedStock(newStock) {
+        const exists = result.some(stock => stock.code === newStock.code);
+        if (exists) return;
+
+        result = [
+            ...result,
+            {
+                code: newStock.code,
+                name: newStock.name,
+                quantity: 0,
+                price: newStock.price,
+                total: 0,
+                totalPercent: 0
+            }
+        ];
+
+        closeOrOpenModal();
+
+        await updatePortfolio(result);
+    }
+
+    function remove(code) {
+        result = result.filter(stock => stock.code !== code);
+        updatePortfolio(result);
+    }
+
+    function formatCurrency(value) {
+        return value.toLocaleString('en-US', {
+            style: 'currency',
+            currency: 'USD'
+        });
+    }
+
+    function calculatePercentage(part, total) {
+        return total ? Math.floor((part / total) * 10000) / 100 : 0;
+    }
+
+    function updateStockQuantity(e) {
+        e.preventDefault();
+        const form = new FormData(e.target);
+        const code = form.get("code");
+        const quantity = parseInt(form.get("quantity")) || 0;
+
+        result = result.map(stock =>
+            stock.code === code ? {...stock, quantity} : stock
+        );
+
+        updatePortfolio(result);
+    }
+
+    function closeOrOpenModal() {
+        searchStockResult = [];
+        showModal = !showModal;
+    }
 </script>
 
 <svelte:head>
@@ -42,99 +172,358 @@
     <meta name="description" content="About"/>
 </svelte:head>
 
-<div>
-    <div class="card">
-        <form on:submit={search}>
-            <input type="text" name="stock" placeholder="Enter a portfolio id" class="input-field"/>
-            <button type="submit" class="search-button">Add</button>
-        </form>
-    </div>
+{#if isLoading}
+    <div in:fade>Loading...</div>
+{:else if result.length !== 0}
 
-    {#if result.length !== 0}
-        <div class="result">
-            {#each result as stock}
-                <div class="card card2">
-                    <div class="stock-info">
-                        <div class="stock-code">{stock.code}</div>
-                        <div class="stock-name">{stock.name}</div>
+    <button class="btn btn-primary" data-toggle="modal" data-target="#exampleModal" on:click={closeOrOpenModal}>Add
+    </button>
+
+    {#if showModal}
+        <div class="modal-container">
+            <div class="modal-content">
+                <form on:submit|preventDefault={handleSubmit}>
+                    <div class="row">
+                        <div class="col">
+                            <input type="text" class="form-control" placeholder="stock code or name"
+                                   name="stock_code"
+                                   oninput="this.value = this.value.toUpperCase()"
+                                   autocomplete="off" autofocus>
+                        </div>
+                        <div class="col">
+                            <input type="reset" value="cancel" class="btn btn-danger" on:click={closeOrOpenModal}/>
+                            <input type="submit" value="search" class="btn btn-primary"/>
+                        </div>
                     </div>
-                    <div class="stock-details">
-                        <div class="stock-currency">{stock.currency || "USD"}</div>
-                        <div class="stock-price">{stock.price}</div>
+                </form>
+
+                {#if searchStockResult.length > 0}
+                    <div class="modal-result">
+                        <div class="card" style="width: 100%;">
+                            <ul class="list-group list-group-flush">
+                                {#each searchStockResult as result}
+                                    <li class="list-group-item d-flex justify-content-between align-items-center"
+                                        on:click={addSelectedStock(result)}>
+                                        ({result.code}) {result.name}
+                                        <button class="btn btn-primary btn-sm">+</button>
+                                    </li>
+                                {/each}
+                            </ul>
+                        </div>
                     </div>
-                </div>
-            {/each}
+                {/if}
+            </div>
         </div>
     {/if}
-</div>
+
+    <div in:fade class="table-container">
+        <table class="stock-table">
+            <thead>
+            <tr>
+                <th>Total Value</th>
+                <th>Total Assets</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr>
+                <td class="code">{formatCurrency(totalValue)}</td>
+                <td class="code">{totalAssets}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+
+    <div in:fade class="table-container">
+        <table class="stock-table">
+            <thead>
+            <tr>
+                <th>Code</th>
+                <th>Name</th>
+                <th>Qty</th>
+                <th>Price</th>
+                <th>Total</th>
+                <th>% of Portfolio</th>
+                <th scope="col"></th>
+            </tr>
+            </thead>
+            <tbody>
+            {#each result as stock}
+                <tr>
+                    <td class="code">{stock.code}</td>
+                    <td class="name">{stock.name}</td>
+                    <td class="qty-edit">
+                        <form id="updateQuantity" on:submit|preventDefault={updateStockQuantity}>
+                            <input type="hidden" name="code" value="{stock.code}"/>
+                            <input type="number" class="qty-input" name="quantity" value="{stock.quantity}"/>
+                        </form>
+                    </td>
+                    <td class="price">{formatCurrency(stock.price)}</td>
+                    <td class="total">{formatCurrency(stock.total)}</td>
+                    <td class="percent">{calculatePercentage(stock.total, totalValue)}%</td>
+                    <td>
+                        <button class="remove-btn" on:click={() => remove(stock.code)} title="remove"></button>
+                    </td>
+                </tr>
+            {/each}
+            </tbody>
+        </table>
+    </div>
+{:else}
+    <div>No portfolio data available.</div>
+{/if}
 
 <style>
-    .result {
-        margin-top: 1rem;
-        display: flex;
-        flex-wrap: wrap;
-        gap: 2rem;
+    .table-container {
+        margin-top: 2rem;
+        overflow-x: auto;
+        border-radius: 12px;
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+    }
+
+    .stock-table {
+        width: 100%;
+        border-collapse: collapse;
+        font-family: system-ui, sans-serif;
+        background-color: #fff;
+        border-radius: 12px;
+        overflow: hidden;
+        min-width: 600px;
     }
 
-    .card {
-        flex: 1 1 300px;
+    th, td {
+        padding: 1rem 1.25rem;
+        text-align: left;
+        white-space: nowrap;
     }
 
-    .card2 {
+    thead {
+        background-color: #f7f7f7;
+        border-bottom: 2px solid #e0e0e0;
+    }
+
+    th {
+        font-weight: 600;
+        font-size: 0.95rem;
+        color: #333;
+    }
+
+    tbody tr:nth-child(odd) {
         background-color: #fafafa;
-        border: 1px solid #eee;
-        border-radius: 12px;
-        max-width: 267px;
     }
 
-    .card:hover {
-        transform: translateY(-10px);
+    tbody tr:nth-child(even) {
+        background-color: #f0f4f8;
+    }
+
+    tbody tr:hover {
+        background-color: #e1ecf4;
     }
 
-    .stock-info {
+    td {
+        font-size: 0.95rem;
+        color: #555;
+        border-bottom: 1px solid #eee;
+    }
+
+    .code {
+        font-weight: 600;
+        color: #2c3e50;
+    }
+
+    .name {
+        color: #7f8c8d;
+    }
+
+    .qty-edit {
         display: flex;
-        justify-content: space-between;
         align-items: center;
-        margin-bottom: 10px;
+        gap: 0.5rem;
     }
 
-    .stock-code {
-        font-size: 1.5rem;
-        font-weight: bold;
+    .qty-input {
+        width: 60px;
+        padding: 0.4rem 0.5rem;
+        font-size: 0.9rem;
+        border: 1px solid #ccc;
+        border-radius: 6px;
+        text-align: right;
+        background-color: #fff;
         color: #333;
+        transition: border-color 0.2s ease;
     }
 
-    .stock-name {
-        font-size: 1.1rem;
-        font-weight: normal;
-        color: #555;
-        text-align: right;
+    .qty-input:focus {
+        outline: none;
+        border-color: #2980b9;
+    }
+
+    .remove-btn {
+        height: 15px;
+        width: 15px;
+        background-color: #e74c3c;
+        border-radius: 50%;
+        display: inline-block;
+        border: 0;
+    }
+
+    .price {
+        color: #27ae60;
+        font-weight: bold;
+    }
+
+    .total {
+        font-weight: 500;
+        color: #34495e;
+    }
+
+    .percent {
+        color: #2980b9;
+    }
+
+    /* Responsive design */
+    @media (max-width: 768px) {
+        .modal-content {
+            width: 90%;
+            padding: 1.5rem;
+        }
+
+        input[type="text"],
+        input[type="number"] {
+            font-size: 0.9rem;
+        }
+
+        .modal-result {
+            max-height: 200px;
+        }
+
+        .list-group-item {
+            font-size: 0.9rem;
+        }
+    }
+
+    @media (max-width: 768px) {
+        .stock-table {
+            font-size: 0.875rem;
+        }
+
+        th, td {
+            padding: 0.75rem 1rem;
+        }
     }
 
-    .stock-info:after {
-        content: '';
-        display: block;
+    /* Modal */
+    .modal-container {
+        position: fixed;
+        top: 20%;
+        left: 0;
+        right: 0;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        z-index: 9999;
+        animation: fadeIn 0.3s ease;
+    }
+
+    /* Modal content styling */
+    .modal-content {
+        background-color: #fff;
+        color: #333;
+        width: 450px;
+        height: 130px;
+        max-width: 500px;
+        border-radius: 10px;
+        padding: 2rem;
+        box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
+        transition: transform 0.3s ease-in-out;
+    }
+
+    /* Form input fields styling */
+    input[type="text"],
+    input[type="number"],
+    input[type="reset"],
+    input[type="submit"] {
+        font-size: 1rem;
+        padding: 10px;
+        margin: 2px;
+        border-radius: 8px;
+        border: 1px solid #ddd;
         width: 100%;
-        height: 1px;
-        background-color: #ddd;
-        margin-top: 10px;
+        transition: border-color 0.3s ease;
+    }
+
+    /* Button specific styles */
+    input[type="submit"] {
+        background-color: #3498db;
+        color: white;
+        cursor: pointer;
+        border: none;
+    }
+
+    input[type="submit"]:hover {
+        background-color: #2980b9;
+    }
+
+    input[type="reset"] {
+        background-color: #e74c3c;
+        color: white;
+        border: none;
     }
 
-    .stock-details {
+    input[type="reset"]:hover {
+        background-color: #c0392b;
+    }
+
+    input[type="text"]:focus,
+    input[type="number"]:focus {
+        outline: none;
+        border-color: #2980b9;
+    }
+
+    /* Results section styling */
+    .modal-result {
+        max-width: 80%;
+        max-height: 250px;
+        transition: opacity 0.3s ease;
+    }
+
+    .list-group-item {
         display: flex;
         justify-content: space-between;
         align-items: center;
-        margin-top: 15px;
+        padding: 10px;
+        border: 1px solid #ddd;
+        margin-bottom: 0.5rem;
+        border-radius: 8px;
+        background-color: #f9f9f9;
+        transition: background-color 0.3s ease;
     }
 
-    .stock-currency {
-        font-size: 1rem;
-        color: #888;
+    .list-group-item:hover {
+        background-color: #f1f1f1;
     }
 
-    .stock-price {
-        font-size: 1.5rem;
-        font-weight: bold;
-        color: #27ae60;
+    .list-group-item .btn-primary {
+        background-color: #2980b9;
+        color: white;
+        border-radius: 50%;
+        height: 30px;
+        width: 30px;
+        padding: 0;
+        border: none;
+    }
+
+    .list-group-item .btn-primary:hover {
+        background-color: #3498db;
+    }
+
+    /* Modal transition */
+    @keyframes fadeIn {
+        from {
+            opacity: 0;
+        }
+        to {
+            opacity: 1;
+        }
     }
-</style>
+
+</style>