Add profile and template pages (scaffolding)

This commit is contained in:
ThePetrovich 2025-07-03 18:39:04 +08:00
parent 41668498ea
commit cb67c5d93d
18 changed files with 1067 additions and 158 deletions

View file

@ -0,0 +1,345 @@
<script lang="ts">
import { onMount } from "svelte";
import { TableHandler } from "@vincjo/datatables";
import {
Card,
CardHeader,
CardBody,
Button,
Input,
Icon,
Pagination,
PaginationItem,
PaginationLink,
} from "@sveltestrap/sveltestrap";
import Navbar from "$lib/components/Navbar.svelte";
import Footer from "$lib/components/Footer.svelte";
import ConfirmationPrompt from "$lib/components/ConfirmationPrompt.svelte";
import { addToast } from "$lib/components/Toast.svelte";
// TODO: Implement these imports
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioTemplatesStore } from "$lib/stores";
import { getSavedPoints, deletePoint } from "$lib/api/points";
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
import { getSavedScenarioTemplates, deleteScenarioTemplate } from "$lib/api/templates";
import type { SavedPoint, SavedFlightProfile, SavedScenarioTemplate } from "$lib/types";
// Table handlers
let pointsTable = $derived(new TableHandler($SavedPointsStore, { rowsPerPage: 5 }));
let pointsSearch = $derived(pointsTable.createSearch(["name"]));
let profilesTable = $derived(new TableHandler($SavedFlightProfilesStore, { rowsPerPage: 5 }));
let profilesSearch = $derived(profilesTable.createSearch(["name"]));
let templatesTable = $derived(new TableHandler($SavedScenarioTemplatesStore, { rowsPerPage: 5 }));
let templatesSearch = $derived(templatesTable.createSearch(["name"]));
onMount(async () => {
// Mock data for demonstration. Replace with API calls.
$SavedPointsStore = [
{ id: 1, name: "Baikonur Cosmodrome", lat: 45.96, lon: 63.3, alt: 90 },
{ id: 2, name: "Kennedy Space Center", lat: 28.57, lon: -80.64, alt: 3 },
];
$SavedFlightProfilesStore = [
{ id: 1, name: "Standard Weather Balloon", rate_profile_data: {ascent_rate: 5, descent_rate: 8, burst_altitude: 30000} },
{ id: 2, name: "High Altitude Probe", rate_profile_data: {ascent_rate: 6, descent_rate: 10, burst_altitude: 40000} },
];
$SavedScenarioTemplatesStore = [
{ id: 1, name: "Summer Launch from Baikonur", template_data: {description: "Standard summer conditions test."} },
{ id: 2, name: "Winter Launch from KSC", template_data: {description: "High wind scenario."} },
];
/*
// TODO: Uncomment when API is ready
const [points, profiles, templates] = await Promise.all([
getSavedPoints(),
getSavedFlightProfiles(),
getSavedScenarioTemplates()
]);
$SavedPointsStore = points;
$SavedFlightProfilesStore = profiles;
$SavedScenarioTemplatesStore = templates;
*/
});
// --- Confirmation Prompt Logic ---
type ConfirmConfig = {
title: string;
body: string;
confirmText: string;
confirmVariant?: string;
onConfirm: () => void;
};
let showConfirm = $state(false);
let confirmConfig = $state<ConfirmConfig>({
title: "",
body: "",
confirmText: "",
onConfirm: () => {},
});
function openConfirmation(config: Partial<ConfirmConfig>) {
confirmConfig = { ...confirmConfig, ...config } as ConfirmConfig;
showConfirm = true;
}
function handleConfirm() {
if (confirmConfig.onConfirm) {
confirmConfig.onConfirm();
}
showConfirm = false;
}
// --- Delete Handlers ---
function handleDelete<T extends { id: number; name: string }>(
item: T,
deleteFn: (id: number) => Promise<any>,
store: any,
itemName: string,
) {
openConfirmation({
title: `Подтвердите удаление`,
body: `Вы уверены, что хотите удалить ${itemName} "${item.name}"?`,
confirmText: "Удалить",
confirmVariant: "danger",
onConfirm: () => {
// deleteFn(item.id).then(() => { // TODO: Uncomment when API is ready
store.update((items: T[]) => items.filter((i) => i.id !== item.id));
addToast({
header: `${itemName} удален`,
body: `${itemName} "${item.name}" успешно удален.`,
color: "success",
});
// }).catch(error => addToast({ header: 'Ошибка', body: `Не удалось удалить ${itemName}: ${error.message}`, color: 'danger' }));
},
});
}
</script>
<main class="force-page-height">
<Navbar />
<div style="height: var(--navbar-height);"></div>
<!-- Spacer for fixed navbar -->
<div class="container my-4">
<div class="row">
<!-- Side Navigation -->
<div class="col-md-3 col-lg-2">
<nav class="nav nav-pills flex-column">
<a class="nav-link" href="/user/account">Учетная запись</a>
<a class="nav-link active" href="/user/templates">Сохраненные сценарии</a>
<a class="nav-link" href="#/">История прогнозов</a>
<a class="nav-link" href="#/">История слежения</a>
</nav>
</div>
<!-- Main Content -->
<div class="col-md-9 col-lg-10">
<!-- Saved Points -->
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0">Точки запуска</h5>
</CardHeader>
<CardBody>
<Input
type="text"
class="form-control-sm mb-2"
placeholder="Поиск по названию..."
bind:value={pointsSearch.value}
oninput={() => pointsSearch.set()}
/>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Название</th>
<th>Широта</th>
<th>Долгота</th>
<th>Высота</th>
<th class="fit"></th>
</tr>
</thead>
<tbody>
{#each pointsTable.rows as row}
<tr>
<td>{row.name}</td>
<td>{row.lat.toFixed(4)} °</td>
<td>{row.lon.toFixed(4)} °</td>
<td>{row.alt} м</td>
<td class="fit">
<Button
color="danger"
size="sm"
onclick={() =>
handleDelete(row, deletePoint, SavedPointsStore, "Точка")}
>
<Icon name="trash" />
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Pagination aria-label="Points page navigation" size="sm">
<PaginationItem>
<PaginationLink previous onclick={() => pointsTable.setPage("previous")} />
</PaginationItem>
{#each pointsTable.pagesWithEllipsis as page}
<PaginationItem active={pointsTable.currentPage === page}>
<PaginationLink onclick={() => pointsTable.setPage(page)}>{page}</PaginationLink>
</PaginationItem>
{/each}
<PaginationItem>
<PaginationLink next onclick={() => pointsTable.setPage("next")} />
</PaginationItem>
</Pagination>
</CardBody>
</Card>
<!-- Saved Flight Profiles -->
<Card class="mb-4">
<CardHeader>
<h5 class="mb-0">Профили полета</h5>
</CardHeader>
<CardBody>
<Input
type="text"
class="form-control-sm mb-2"
placeholder="Поиск по названию..."
bind:value={profilesSearch.value}
oninput={() => profilesSearch.set()}
/>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Название</th>
<th>Скороподъемность</th>
<th>Скорость снижения</th>
<th>Высота разрыва</th>
<th class="fit"></th>
</tr>
</thead>
<tbody>
{#each profilesTable.rows as row}
<tr>
<td>{row.name}</td>
<td>{row.rate_profile_data.ascent_rate} м/с</td>
<td>{row.rate_profile_data.descent_rate} м/с</td>
<td>{row.rate_profile_data.burst_altitude} м</td>
<td class="fit">
<Button
color="danger"
size="sm"
onclick={() =>
handleDelete(
row,
deleteFlightProfile,
SavedFlightProfilesStore,
"Профиль",
)}
>
<Icon name="trash" />
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Pagination aria-label="Profiles page navigation" size="sm">
<PaginationItem>
<PaginationLink previous onclick={() => profilesTable.setPage("previous")} />
</PaginationItem>
{#each profilesTable.pagesWithEllipsis as page}
<PaginationItem active={profilesTable.currentPage === page}>
<PaginationLink onclick={() => profilesTable.setPage(page)}>{page}</PaginationLink>
</PaginationItem>
{/each}
<PaginationItem>
<PaginationLink next onclick={() => profilesTable.setPage("next")} />
</PaginationItem>
</Pagination>
</CardBody>
</Card>
<!-- Saved Scenario Templates -->
<Card>
<CardHeader>
<h5 class="mb-0">Сценарии</h5>
</CardHeader>
<CardBody>
<Input
type="text"
class="form-control-sm mb-2"
placeholder="Поиск по названию..."
bind:value={templatesSearch.value}
oninput={() => templatesSearch.set()}
/>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Название</th>
<th>Описание</th>
<th class="fit"></th>
</tr>
</thead>
<tbody>
{#each templatesTable.rows as row}
<tr>
<td>{row.name}</td>
<td>{row.template_data.description}</td>
<td class="fit">
<Button
color="danger"
size="sm"
onclick={() =>
handleDelete(
row,
deleteScenarioTemplate,
SavedScenarioTemplatesStore,
"Шаблон",
)}
>
<Icon name="trash" />
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Pagination aria-label="Templates page navigation" size="sm">
<PaginationItem>
<PaginationLink previous onclick={() => templatesTable.setPage("previous")} />
</PaginationItem>
{#each templatesTable.pagesWithEllipsis as page}
<PaginationItem active={templatesTable.currentPage === page}>
<PaginationLink onclick={() => templatesTable.setPage(page)}>{page}</PaginationLink>
</PaginationItem>
{/each}
<PaginationItem>
<PaginationLink next onclick={() => templatesTable.setPage("next")} />
</PaginationItem>
</Pagination>
</CardBody>
</Card>
</div>
</div>
</div>
<Footer />
</main>
<ConfirmationPrompt
bind:isOpen={showConfirm}
title={confirmConfig.title}
confirmText={confirmConfig.confirmText}
confirmVariant={confirmConfig.confirmVariant || "danger"}
cancelText="Отмена"
onconfirm={handleConfirm}
oncancel={() => (showConfirm = false)}
>
<p>{confirmConfig.body}</p>
</ConfirmationPrompt>