140 lines
3.6 KiB
Svelte
140 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { FormGroup, Label, Input } from '@sveltestrap/sveltestrap';
|
|
import type { SavedPoint } from '$domain';
|
|
import { pointsApi } from '$api';
|
|
import { addToast, ListEditor } from '$ui';
|
|
import type { ListEditorApi, ListEditorConfig } from '$ui';
|
|
import { t } from '$i18n';
|
|
import { pointsStore } from './pointsStore';
|
|
|
|
interface Props {
|
|
isOpen?: boolean;
|
|
onClose?: () => void;
|
|
onSave?: (p: SavedPoint) => void;
|
|
onSelectPoint?: (p: SavedPoint | null) => void;
|
|
point?: SavedPoint | null;
|
|
}
|
|
|
|
let {
|
|
isOpen = $bindable(false),
|
|
onClose = () => {},
|
|
onSave = (_: SavedPoint) => {},
|
|
onSelectPoint = (_: SavedPoint | null) => {},
|
|
point = null,
|
|
}: Props = $props();
|
|
|
|
let editorRef: ListEditor<SavedPoint> | null = $state(null);
|
|
|
|
onMount(async () => {
|
|
if ($pointsStore.length === 0) {
|
|
try {
|
|
pointsStore.set(await pointsApi.list());
|
|
} catch (err: unknown) {
|
|
addToast({
|
|
header: $t('common.error'),
|
|
body: (err as Error).message,
|
|
color: 'danger',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (point && editorRef) editorRef.open(point);
|
|
});
|
|
|
|
const config: ListEditorConfig = {
|
|
showTable: true,
|
|
closeOnSave: false,
|
|
closeOnDelete: false,
|
|
searchBy: ['name'],
|
|
labels: {
|
|
item: 'Point',
|
|
itemGenitive: 'point',
|
|
items: 'Points',
|
|
add: 'Add',
|
|
edit: 'Edit',
|
|
save: 'Save',
|
|
update: 'Update',
|
|
delete: 'Delete',
|
|
cancel: 'Cancel',
|
|
close: 'Close',
|
|
searchPlaceholder: 'Search by name...',
|
|
},
|
|
};
|
|
|
|
const api: ListEditorApi<SavedPoint> = {
|
|
save: (p) => pointsApi.create(p),
|
|
update: (p) => pointsApi.update(p),
|
|
delete: (p) => pointsApi.delete(p.id),
|
|
};
|
|
|
|
const factory = (): SavedPoint => ({ id: 0, name: '', lat: 0, lon: 0, alt: 0 });
|
|
|
|
let items = $state<SavedPoint[]>($pointsStore);
|
|
|
|
$effect(() => {
|
|
items = $pointsStore;
|
|
});
|
|
|
|
$effect(() => {
|
|
pointsStore.set(items);
|
|
});
|
|
|
|
export function open(p: SavedPoint | null = null, showTable: boolean = config.showTable ?? true) {
|
|
editorRef?.open(p, showTable);
|
|
}
|
|
</script>
|
|
|
|
<ListEditor
|
|
bind:this={editorRef}
|
|
bind:isOpen
|
|
bind:items
|
|
{api}
|
|
{config}
|
|
itemFactory={factory}
|
|
onClose={() => onClose()}
|
|
onSave={(p) => onSave(p)}
|
|
onSelect={(p) => onSelectPoint(p)}>
|
|
{#snippet tableHeader()}
|
|
<tr>
|
|
<th>{$t('points.name')}</th>
|
|
<th>{$t('points.lat')}</th>
|
|
<th>{$t('points.lon')}</th>
|
|
<th>{$t('points.alt')}</th>
|
|
<th class="fit"></th>
|
|
</tr>
|
|
{/snippet}
|
|
|
|
{#snippet tableRow({ row })}
|
|
<td>{row.name}</td>
|
|
<td>{row.lat.toFixed(5)} °</td>
|
|
<td>{row.lon.toFixed(5)} °</td>
|
|
<td>{row.alt} м</td>
|
|
{/snippet}
|
|
|
|
{#snippet formFields({ item })}
|
|
<div class="mb-2">
|
|
<Label class="small">{$t('points.name')}</Label>
|
|
<Input class="form-control-sm" type="text" bind:value={item.name} required />
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<FormGroup class="flex-grow-1">
|
|
<Label class="small">{$t('points.lat')}</Label>
|
|
<Input class="form-control-sm" type="number" step="any" bind:value={item.lat} required />
|
|
<span class="form-text">{$t('points.degrees')}</span>
|
|
</FormGroup>
|
|
<FormGroup class="flex-grow-1">
|
|
<Label class="small">{$t('points.lon')}</Label>
|
|
<Input class="form-control-sm" type="number" step="any" bind:value={item.lon} required />
|
|
<span class="form-text">{$t('points.degrees')}</span>
|
|
</FormGroup>
|
|
<FormGroup class="flex-grow-1">
|
|
<Label class="small">{$t('points.alt')}</Label>
|
|
<Input class="form-control-sm" type="number" step="any" bind:value={item.alt} required />
|
|
<span class="form-text">{$t('points.metersAsl')}</span>
|
|
</FormGroup>
|
|
</div>
|
|
{/snippet}
|
|
</ListEditor>
|