379 lines
No EOL
18 KiB
Svelte
379 lines
No EOL
18 KiB
Svelte
<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 PointEditor from "$lib/components/PointEditor.svelte";
|
||
import ToastContainer from "$lib/components/Toast.svelte";
|
||
import { addToast } from "$lib/components/Toast.svelte";
|
||
|
||
// TODO: Implement these imports
|
||
import { SavedPointsStore, SavedFlightProfilesStore, SavedScenarioStore } from "$lib/stores";
|
||
import { getSavedPoints, deletePoint } from "$lib/api/points";
|
||
import { getSavedFlightProfiles, deleteFlightProfile } from "$lib/api/profiles";
|
||
import { getSavedScenarios, deleteScenario } from "$lib/api/scenarios";
|
||
import type { SavedPoint, SavedFlightProfile, SavedScenario } 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($SavedScenarioStore, { rowsPerPage: 5 }));
|
||
let templatesSearch = $derived(templatesTable.createSearch(["name"]));
|
||
|
||
let editPoint: SavedPoint | null = $state(null);
|
||
|
||
onMount(async () => {
|
||
// Mock data for demonstration. Replace with API calls.
|
||
const pts = await getSavedPoints();
|
||
$SavedPointsStore = pts;
|
||
SavedPointsStore.set($SavedPointsStore);
|
||
|
||
$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} },
|
||
];
|
||
|
||
|
||
/*
|
||
// 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' }));
|
||
},
|
||
});
|
||
}
|
||
|
||
function handleEditPoint(point: SavedPoint) {
|
||
editPoint = point;
|
||
}
|
||
</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 mb-4">
|
||
<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>
|
||
<div class="position-relative mb-2">
|
||
<Input
|
||
type="text"
|
||
class="form-control-sm pe-5"
|
||
placeholder="Поиск по названию..."
|
||
bind:value={pointsSearch.value}
|
||
oninput={() => pointsSearch.set()}
|
||
/>
|
||
<Button
|
||
size="sm"
|
||
color="white"
|
||
class="position-absolute top-50 end-0 translate-middle-y me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white);"
|
||
onclick={() => {
|
||
pointsSearch.value = "";
|
||
pointsSearch.set();
|
||
}}
|
||
disabled={!pointsSearch.value}
|
||
>
|
||
<Icon name="x" style="font-size: 16px;" />
|
||
</Button>
|
||
</div>
|
||
<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="primary" size="sm" onclick={() => handleEditPoint(row)}>
|
||
<Icon name="pencil" />
|
||
</Button>
|
||
<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>
|
||
|
||
<PointEditor
|
||
point={editPoint}
|
||
isOpen={editPoint !== null}
|
||
onClose={() => { editPoint = null; pointsTable.setRows($SavedPointsStore) }}
|
||
editor={true}
|
||
closeOnSave={true}
|
||
closeOnDelete={true}
|
||
/>
|
||
|
||
<ToastContainer /> |