@mytec: iter1.1.1 ready for testing

This commit is contained in:
2026-01-30 23:15:51 +02:00
parent 0fb19476cd
commit b7d008fe26
8 changed files with 422 additions and 11 deletions

View File

@@ -0,0 +1,108 @@
import { create } from 'zustand';
import type { Site, CoverageSettings } from '@/types/index.ts';
export interface ProjectSnapshot {
sites: Site[];
settings: CoverageSettings;
timestamp: number;
action: string;
}
interface HistoryState {
past: ProjectSnapshot[];
future: ProjectSnapshot[];
maxHistory: number;
// Actions
push: (snapshot: ProjectSnapshot) => void;
undo: () => ProjectSnapshot | null;
redo: () => ProjectSnapshot | null;
clear: () => void;
// Computed (not real zustand selectors — use getState())
canUndo: boolean;
canRedo: boolean;
}
export const useHistoryStore = create<HistoryState>((set, get) => ({
past: [],
future: [],
maxHistory: 50,
canUndo: false,
canRedo: false,
push: (snapshot: ProjectSnapshot) => {
set((state) => {
const past = [...state.past, snapshot];
// Trim oldest if exceeding max
if (past.length > state.maxHistory) {
past.shift();
}
return {
past,
future: [], // new action clears redo stack
canUndo: true,
canRedo: false,
};
});
},
undo: () => {
const { past } = get();
if (past.length === 0) return null;
const snapshot = past[past.length - 1];
set((state) => {
const newPast = state.past.slice(0, -1);
return {
past: newPast,
// We don't push to future here — the caller does that with the current state
canUndo: newPast.length > 0,
};
});
return snapshot;
},
redo: () => {
const { future } = get();
if (future.length === 0) return null;
const snapshot = future[future.length - 1];
set((state) => {
const newFuture = state.future.slice(0, -1);
return {
future: newFuture,
canRedo: newFuture.length > 0,
};
});
return snapshot;
},
clear: () => {
set({ past: [], future: [], canUndo: false, canRedo: false });
},
}));
/**
* Push the current state to the redo stack.
* Called by the undo handler before restoring a previous snapshot.
*/
export function pushToFuture(snapshot: ProjectSnapshot) {
useHistoryStore.setState((state) => ({
future: [...state.future, snapshot],
canRedo: true,
}));
}
/**
* Push the current state to the undo stack before restoring from redo.
*/
export function pushToPast(snapshot: ProjectSnapshot) {
useHistoryStore.setState((state) => {
const past = [...state.past, snapshot];
if (past.length > state.maxHistory) {
past.shift();
}
return { past, canUndo: true };
});
}

View File

@@ -3,6 +3,21 @@ import { v4 as uuidv4 } from 'uuid';
import type { Site, SiteFormData } from '@/types/index.ts';
import { db } from '@/db/schema.ts';
import { useCoverageStore } from '@/store/coverage.ts';
import { useHistoryStore } from '@/store/history.ts';
/**
* Capture a snapshot of current state and push it to the undo stack.
* Call this BEFORE mutating state.
*/
function pushSnapshot(action: string, sites: Site[]) {
const settings = useCoverageStore.getState().settings;
useHistoryStore.getState().push({
sites: structuredClone(sites),
settings: { ...settings },
timestamp: Date.now(),
action,
});
}
const SITE_COLORS = [
'#ef4444', '#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6',
@@ -65,6 +80,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
},
addSite: async (data: SiteFormData) => {
pushSnapshot('add site', get().sites);
const id = uuidv4();
const now = new Date();
const colorIndex = get().sites.length % SITE_COLORS.length;
@@ -90,6 +106,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
const sites = get().sites;
const existing = sites.find((s) => s.id === id);
if (!existing) return;
pushSnapshot('update site', sites);
const updated: Site = {
...existing,
@@ -108,6 +125,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
},
deleteSite: async (id: string) => {
pushSnapshot('delete site', get().sites);
await db.sites.delete(id);
set((state) => ({
sites: state.sites.filter((s) => s.id !== id),
@@ -127,6 +145,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
cloneSiteAsSectors: async (siteId: string, sectorCount: 2 | 3) => {
const source = get().sites.find((s) => s.id === siteId);
if (!source) return;
pushSnapshot('clone as sectors', get().sites);
const spacing = 360 / sectorCount;
const addSite = get().addSite;
@@ -159,6 +178,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
cloneSector: async (siteId: string) => {
const source = get().sites.find((s) => s.id === siteId);
if (!source) return;
pushSnapshot('clone sector', get().sites);
// Count existing sectors at this location to determine naming
const colocated = get().sites.filter(
@@ -196,6 +216,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
// Import sites from parsed data
importSites: async (sitesData: SiteFormData[]) => {
pushSnapshot('import sites', get().sites);
const addSite = get().addSite;
let count = 0;
for (const data of sitesData) {
@@ -229,6 +250,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchUpdateHeight: async (adjustment: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch update height', sites);
const selectedSet = new Set(selectedSiteIds);
const now = new Date();
@@ -257,6 +279,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchSetHeight: async (height: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch set height', sites);
const selectedSet = new Set(selectedSiteIds);
const clampedHeight = Math.max(1, Math.min(100, height));
const now = new Date();
@@ -286,6 +309,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchAdjustAzimuth: async (delta: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch adjust azimuth', sites);
const selectedSet = new Set(selectedSiteIds);
const now = new Date();
@@ -315,6 +339,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchSetAzimuth: async (azimuth: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch set azimuth', sites);
const selectedSet = new Set(selectedSiteIds);
const clamped = ((azimuth % 360) + 360) % 360;
const now = new Date();
@@ -344,6 +369,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchAdjustPower: async (delta: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch adjust power', sites);
const selectedSet = new Set(selectedSiteIds);
const now = new Date();
@@ -372,6 +398,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchSetPower: async (power: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch set power', sites);
const selectedSet = new Set(selectedSiteIds);
const clamped = Math.max(10, Math.min(50, power));
const now = new Date();
@@ -401,6 +428,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchAdjustTilt: async (delta: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch adjust tilt', sites);
const selectedSet = new Set(selectedSiteIds);
const now = new Date();
@@ -430,6 +458,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchSetTilt: async (tilt: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch set tilt', sites);
const selectedSet = new Set(selectedSiteIds);
const clamped = Math.max(-90, Math.min(90, tilt));
const now = new Date();
@@ -459,6 +488,7 @@ export const useSitesStore = create<SitesState>((set, get) => ({
batchSetFrequency: async (frequency: number) => {
const { sites, selectedSiteIds } = get();
pushSnapshot('batch set frequency', sites);
const selectedSet = new Set(selectedSiteIds);
const clamped = Math.max(100, Math.min(6000, frequency));
const now = new Date();