|
|
@@ -44,7 +44,6 @@
|
|
|
if (!auth || !auth.token) {
|
|
|
await goto('/logout');
|
|
|
} else {
|
|
|
-
|
|
|
const defaultCurrency = localStorage.getItem('defaultCurrency');
|
|
|
currency = defaultCurrency || 'USD';
|
|
|
|
|
|
@@ -65,7 +64,11 @@
|
|
|
|
|
|
async function fetchPortfolio() {
|
|
|
try {
|
|
|
- const response = await getRequest(`${import.meta.env.VITE_STOCKS_HOST}/api/portfolios?currency=${currency}`, {}, authToken);
|
|
|
+ const response = await getRequest(
|
|
|
+ `${import.meta.env.VITE_STOCKS_HOST}/api/portfolios?currency=${currency}`,
|
|
|
+ {},
|
|
|
+ authToken
|
|
|
+ );
|
|
|
|
|
|
if (response.ok) {
|
|
|
await update(response.json());
|
|
|
@@ -243,9 +246,7 @@
|
|
|
}
|
|
|
|
|
|
function formatCode(code) {
|
|
|
- return code.includes(':')
|
|
|
- ? code.split(':')[1]
|
|
|
- : code;
|
|
|
+ return code.includes(':') ? code.split(':')[1] : code;
|
|
|
}
|
|
|
|
|
|
function getFlag(code) {
|
|
|
@@ -299,8 +300,14 @@
|
|
|
|
|
|
{#if isLoading}
|
|
|
<div in:fade class="flex justify-center items-center py-10">
|
|
|
- <svg class="animate-spin h-8 w-8 text-blue-500 dark:text-blue-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
+ <svg
|
|
|
+ class="animate-spin h-8 w-8 text-blue-500 dark:text-blue-300"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ fill="none"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ >
|
|
|
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
|
|
|
+ ></circle>
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
|
|
</svg>
|
|
|
</div>
|
|
|
@@ -359,10 +366,18 @@
|
|
|
/>
|
|
|
|
|
|
{#if showDeleteConfirm}
|
|
|
- <div class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg z-50 text-center">
|
|
|
+ <div
|
|
|
+ class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 p-6 rounded-xl shadow-lg z-50 text-center"
|
|
|
+ >
|
|
|
<div class="flex justify-center gap-4">
|
|
|
- <button class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg" on:click={confirmDeleteAction}>Confirm deletion</button>
|
|
|
- <button class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-lg" on:click={cancelDelete}>Cancel</button>
|
|
|
+ <button
|
|
|
+ class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg"
|
|
|
+ on:click={confirmDeleteAction}>Confirm deletion</button
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded-lg"
|
|
|
+ on:click={cancelDelete}>Cancel</button
|
|
|
+ >
|
|
|
</div>
|
|
|
</div>
|
|
|
{/if}
|
|
|
@@ -370,16 +385,18 @@
|
|
|
<div in:fade class="overflow-x-auto mt-6 rounded-xl shadow">
|
|
|
<table class="min-w-full bg-white dark:bg-gray-900 text-sm">
|
|
|
<thead class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200">
|
|
|
- <tr>
|
|
|
- <th class="px-4 py-3 text-left font-semibold">Total Value</th>
|
|
|
- <th class="px-4 py-3 text-left font-semibold">Total Assets</th>
|
|
|
- </tr>
|
|
|
+ <tr>
|
|
|
+ <th class="px-4 py-3 text-left font-semibold">Total Value</th>
|
|
|
+ <th class="px-4 py-3 text-left font-semibold">Total Assets</th>
|
|
|
+ </tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
- <tr class="border-t border-gray-200 dark:border-gray-700">
|
|
|
- <td class="px-4 py-3 font-semibold text-gray-800 dark:text-gray-100">{formatCurrency(totalValue)}</td>
|
|
|
- <td class="px-4 py-3 font-semibold text-gray-800 dark:text-gray-100">{totalAssets}</td>
|
|
|
- </tr>
|
|
|
+ <tr class="border-t border-gray-200 dark:border-gray-700">
|
|
|
+ <td class="px-4 py-3 font-semibold text-gray-800 dark:text-gray-100"
|
|
|
+ >{formatCurrency(totalValue)}</td
|
|
|
+ >
|
|
|
+ <td class="px-4 py-3 font-semibold text-gray-800 dark:text-gray-100">{totalAssets}</td>
|
|
|
+ </tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
@@ -387,60 +404,100 @@
|
|
|
<div in:fade class="overflow-x-auto mt-6 rounded-xl shadow">
|
|
|
<table class="min-w-full bg-white dark:bg-gray-900 text-sm">
|
|
|
<thead class="bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200">
|
|
|
- <tr>
|
|
|
- <th class="px-4 py-3">Code</th>
|
|
|
- <th class="px-4 py-3">Name</th>
|
|
|
- <th class="px-4 py-3">Qty</th>
|
|
|
- <th class="px-4 py-3">Price</th>
|
|
|
- <th class="px-4 py-3">Total</th>
|
|
|
- <th class="px-4 py-3">% of Portfolio</th>
|
|
|
- <th class="px-4 py-3"></th>
|
|
|
- </tr>
|
|
|
+ <tr>
|
|
|
+ <th class="px-4 py-3">Code</th>
|
|
|
+ <th class="px-4 py-3">Name</th>
|
|
|
+ <th class="px-4 py-3">Qty</th>
|
|
|
+ <th class="px-4 py-3">Price</th>
|
|
|
+ <th class="px-4 py-3">Total</th>
|
|
|
+ <th class="px-4 py-3">% of Portfolio</th>
|
|
|
+ <th class="px-4 py-3"></th>
|
|
|
+ </tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
- {#each result as stock}
|
|
|
- <tr class="odd:bg-gray-50 even:bg-gray-100 dark:odd:bg-gray-800 dark:even:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 transition">
|
|
|
- <td class="px-4 py-2 font-mono font-semibold text-blue-700 dark:text-blue-300 cursor-pointer" title={stock.code} on:click={() => openStock(stock.code)}>
|
|
|
- <div class="inline-flex items-center gap-2">
|
|
|
- <img src={`https://flagcdn.com/w40/${getFlag(stock.code)}.png`} alt="{getFlag(stock.code)} flag" class="w-5 h-auto" />
|
|
|
- {formatCode(stock.code)}
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- <td class="px-4 py-2 text-gray-700 dark:text-gray-300">{stock.name}</td>
|
|
|
- <td class="px-4 py-2">
|
|
|
- <form id="updateQuantity" on:submit|preventDefault={updateStockQuantity} class="flex items-center">
|
|
|
- <input type="hidden" name="code" value={stock.code} />
|
|
|
- <input type="number" name="quantity" min="0" value={stock.quantity}
|
|
|
- class="w-16 px-2 py-1 text-right border rounded-lg text-sm text-gray-800 dark:text-gray-100 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-300" on:input={handleInputChange} />
|
|
|
- </form>
|
|
|
- </td>
|
|
|
- <td class="px-4 py-2 font-semibold text-green-600 dark:text-green-400">{formatCurrency(stock.price)}</td>
|
|
|
- <td class="px-4 py-2 font-semibold text-gray-800 dark:text-gray-200">{formatCurrency(stock.total)}</td>
|
|
|
- <td class="px-4 py-2 text-blue-600 dark:text-blue-400">{calculatePercentage(stock.total, totalValue)}%</td>
|
|
|
- <td class="px-4 py-2 text-right">
|
|
|
- <button on:click={() => confirmDelete(stock.code)} aria-label="Delete" title="remove"
|
|
|
- class="bg-red-600 hover:bg-red-700 text-white rounded-full p-2 shadow focus:outline-none focus:ring-2 focus:ring-red-400">
|
|
|
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
|
|
- <polyline points="3 6 5 6 21 6"></polyline>
|
|
|
- <path d="M19 6l-1 14H6L5 6"></path>
|
|
|
- <path d="M10 11v6"></path>
|
|
|
- <path d="M14 11v6"></path>
|
|
|
- <path d="M9 6V4h6v2"></path>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- {/each}
|
|
|
+ {#each result as stock}
|
|
|
+ <tr
|
|
|
+ class="odd:bg-gray-50 even:bg-gray-100 dark:odd:bg-gray-800 dark:even:bg-gray-700 hover:bg-blue-50 dark:hover:bg-blue-900 transition"
|
|
|
+ >
|
|
|
+ <td
|
|
|
+ class="px-4 py-2 font-mono font-semibold text-blue-700 dark:text-blue-300 cursor-pointer"
|
|
|
+ title={stock.code}
|
|
|
+ on:click={() => openStock(stock.code)}
|
|
|
+ >
|
|
|
+ <div class="inline-flex items-center gap-2">
|
|
|
+ <img
|
|
|
+ src={`https://flagcdn.com/w40/${getFlag(stock.code)}.png`}
|
|
|
+ alt="{getFlag(stock.code)} flag"
|
|
|
+ class="w-5 h-auto"
|
|
|
+ />
|
|
|
+ {formatCode(stock.code)}
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-2 text-gray-700 dark:text-gray-300">{stock.name}</td>
|
|
|
+ <td class="px-4 py-2">
|
|
|
+ <form
|
|
|
+ id="updateQuantity"
|
|
|
+ on:submit|preventDefault={updateStockQuantity}
|
|
|
+ class="flex items-center"
|
|
|
+ >
|
|
|
+ <input type="hidden" name="code" value={stock.code} />
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ name="quantity"
|
|
|
+ min="0"
|
|
|
+ value={stock.quantity}
|
|
|
+ class="w-16 px-2 py-1 text-right border rounded-lg text-sm text-gray-800 dark:text-gray-100 bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-300"
|
|
|
+ on:input={handleInputChange}
|
|
|
+ />
|
|
|
+ </form>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-2 font-semibold text-green-600 dark:text-green-400"
|
|
|
+ >{formatCurrency(stock.price)}</td
|
|
|
+ >
|
|
|
+ <td class="px-4 py-2 font-semibold text-gray-800 dark:text-gray-200"
|
|
|
+ >{formatCurrency(stock.total)}</td
|
|
|
+ >
|
|
|
+ <td class="px-4 py-2 text-blue-600 dark:text-blue-400"
|
|
|
+ >{calculatePercentage(stock.total, totalValue)}%</td
|
|
|
+ >
|
|
|
+ <td class="px-4 py-2 text-right">
|
|
|
+ <button
|
|
|
+ on:click={() => confirmDelete(stock.code)}
|
|
|
+ aria-label="Delete"
|
|
|
+ title="remove"
|
|
|
+ class="bg-red-600 hover:bg-red-700 text-white rounded-full p-2 shadow focus:outline-none focus:ring-2 focus:ring-red-400"
|
|
|
+ >
|
|
|
+ <svg
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ stroke-linecap="round"
|
|
|
+ stroke-linejoin="round"
|
|
|
+ class="w-4 h-4"
|
|
|
+ >
|
|
|
+ <polyline points="3 6 5 6 21 6"></polyline>
|
|
|
+ <path d="M19 6l-1 14H6L5 6"></path>
|
|
|
+ <path d="M10 11v6"></path>
|
|
|
+ <path d="M14 11v6"></path>
|
|
|
+ <path d="M9 6V4h6v2"></path>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ {/each}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
|
|
|
{#if hasChanges}
|
|
|
- <button class="fixed bottom-5 right-5 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg z-50" on:click={applyChanges}>
|
|
|
+ <button
|
|
|
+ class="fixed bottom-5 right-5 bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg shadow-lg z-50"
|
|
|
+ on:click={applyChanges}
|
|
|
+ >
|
|
|
Apply Changes
|
|
|
</button>
|
|
|
{/if}
|
|
|
{:else}
|
|
|
<div class="text-gray-500 dark:text-gray-300 text-center py-6">No portfolio data available.</div>
|
|
|
{/if}
|
|
|
-
|