init at work

This commit is contained in:
Mathias Schneider
2026-03-27 12:58:01 +01:00
parent 352c352056
commit 8d40597732
60 changed files with 16498 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
# FlexibilityTable Filter Flow
This note explains how your `selectedPeriodStartFrom` example interacts with the rest of the code.
## Goal of the feature
The table lets the user:
- choose a start period
- choose an end period
- click Refresh
- load matching backend data
- view and export the filtered result
## The important variables
From FlexibilityTable:
```ts
type PeriodFilterValue = string | null;
const periodStartOptions = getDownloadPeriodStartOptions();
const defaultPeriodStartFrom = periodStartOptions[0]?.value ?? null;
const defaultPeriodStartUntil = periodStartOptions[periodStartOptions.length - 1]?.value ?? null;
const selectedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
const selectedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
const appliedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
const appliedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
```
There are two groups of state here.
### selected values
These track what the user is currently editing in the UI.
```ts
selectedPeriodStartFrom
selectedPeriodStartUntil
```
### applied values
These track what was last confirmed with the Refresh button.
```ts
appliedPeriodStartFrom
appliedPeriodStartUntil
```
This separation is very important.
## Why both selected and applied exist
Without this split, every dropdown change could immediately trigger a backend request.
That would mean:
- too many requests
- surprising UI updates while the user is still choosing dates
- exports might not match the table state clearly
So the component uses a draft pattern.
Python analogy:
```python
form_input = {'start': '2026-03-01', 'end': '2026-03-31'}
applied_filters = {'start': '2026-02-01', 'end': '2026-02-28'}
```
The form input is what the user is editing.
The applied filters are what the system is currently using.
## Step 1: build options for the dropdown
```ts
const periodStartOptions = getDownloadPeriodStartOptions();
```
This comes from `jsonDownload.ts`.
That composable calculates valid backend week starts.
Each option looks roughly like this:
```ts
{
title: '2026-03-23',
value: '2026-03-22T23:00:00.000Z'
}
```
- `title` is for humans
- `value` is for the program
## Step 2: connect the dropdown to state
```vue
<v-select
v-model="selectedPeriodStartFrom"
:items="periodStartOptions"
item-title="title"
item-value="value"
label="Period start from"
/>
```
When the user chooses an option:
- Vuetify reads the chosen item
- Vue writes its `value` field into `selectedPeriodStartFrom`
- any code depending on `selectedPeriodStartFrom` can react
## Step 3: validate the selected range
```ts
const hasValidPeriodRange = computed(() => {
if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
return true;
}
return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
});
```
This computed reads the two selected values and checks if the range makes sense.
The Refresh button uses it:
```vue
<v-btn
:disabled="loading || !hasValidPeriodRange"
@click="applyDateFilters"
>
Refresh
</v-btn>
```
So if the user chooses an invalid range, the button becomes disabled.
## Step 4: user clicks Refresh
```ts
async function applyDateFilters() {
if (!hasValidPeriodRange.value) {
showMessage('Select a valid period range before refreshing.', 'error', true);
return;
}
appliedPeriodStartFrom.value = selectedPeriodStartFrom.value;
appliedPeriodStartUntil.value = selectedPeriodStartUntil.value;
await refreshTableDataFromBackend(
'Could not load data from backend.',
buildSelectedDownloadPayload(),
);
}
```
This does three things:
1. validate
2. copy selected values into applied values
3. fetch new backend data
## Step 5: build the request payload
```ts
function buildSelectedDownloadPayload(): DownloadPayload {
return buildDownloadPayload(
buildDateRange(selectedPeriodStartFrom.value, selectedPeriodStartUntil.value),
);
}
```
and:
```ts
function buildDateRange(periodStartFrom: PeriodFilterValue, periodStartUntil: PeriodFilterValue): DownloadDateRange {
return {
periodStartFrom: periodStartFrom ?? undefined,
periodStartUntil: periodStartUntil ?? undefined,
};
}
```
This turns UI values into backend filter input.
Important detail:
- `null` from the UI becomes `undefined` for the backend builder
- that means "no boundary selected"
## Step 6: call the composable
```ts
tableData.value = await downloadFlexData(payload);
```
This happens inside `refreshTableDataFromBackend()`.
The component does not itself know how to talk to the backend in detail. That logic lives in `useJsonDownload()`.
## Step 7: transform backend data into rows
Inside `jsonDownload.ts`, the composable:
- picks the correct endpoint
- sends the request
- receives backend offer data
- converts it into table rows and display rows
So the data flow is:
```text
Dropdown -> selectedPeriodStartFrom -> payload builder -> downloadFlexData -> tableData -> rendered table
```
## Step 8: render the table
The table rows come from:
```ts
const rows = computed<FlexDisplayRow[]>(() => tableData.value.displayRows ?? []);
```
Then more filtering happens:
```ts
const filteredRows = computed<FlexDisplayRow[]>(() =>
rows.value.filter((row) => {
const aggregator = row.senderId ?? row.aggregator;
const controlArea = row.receiverId ?? row.controlArea;
if (selectedAggregators.value.length && (!aggregator || !selectedAggregators.value.includes(aggregator))) {
return false;
}
if (selectedControlArea.value && controlArea !== selectedControlArea.value) {
return false;
}
return true;
}),
);
```
Notice something important:
- date filters affect what is fetched from the backend
- aggregator and control area filters affect what is shown locally in the frontend
That is a useful architectural distinction.
## Step 9: exports use applied filters
For JSON export:
```ts
const rawData = await downloadRawFlexData(buildAppliedDownloadPayload());
```
That uses applied values, not selected values.
Why?
Because if the user changed the dropdown but did not click Refresh yet, the table still reflects the old filters. Export should match what the user is actually seeing as the active state.
## Full mental model
Use this simple chain:
```text
1. Options are created
2. User picks a value
3. selectedPeriodStartFrom changes
4. Validation recomputes
5. User clicks Refresh
6. selected values become applied values
7. payload is built
8. backend data is fetched
9. tableData updates
10. table re-renders
```
## Tiny simplified example
```vue
<template>
<select v-model="selectedColor">
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<button @click="apply">Apply</button>
<p>Selected: {{ selectedColor }}</p>
<p>Applied: {{ appliedColor }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const selectedColor = ref('red');
const appliedColor = ref('red');
function apply() {
appliedColor.value = selectedColor.value;
}
</script>
```
This is the same pattern as your date filters, just simpler.
## Key beginner lesson
When reading Vue, do not only ask "what is this variable".
Ask:
- who writes it?
- who reads it?
- is it draft state or applied state?
- does it affect frontend-only filtering or backend requests?
That is how the whole interaction becomes understandable.
## Related Notes
- [[00-Vue for Python Developers]]
- [[01-Refs, Computed, and Reactivity]]
- [[02-Templates, v-model, and Events]]
- [[03-Components, Props, Lifecycle, and Composables]]