feat: add auth, mock data and zustand stores
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
import { User } from '../types';
|
||||||
|
import { MOCK_USERS } from './mock-data';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
login: (username: string, password: string) => boolean;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
user: null,
|
||||||
|
login: (username, password) => {
|
||||||
|
if (!username || !password) return false;
|
||||||
|
if (password !== username) return false;
|
||||||
|
const foundUser = MOCK_USERS.find(
|
||||||
|
(u) => u.username.toLowerCase() === username.toLowerCase()
|
||||||
|
);
|
||||||
|
if (!foundUser) return false;
|
||||||
|
set({ user: foundUser });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
logout: () => set({ user: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'patchpilot:auth',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
partialize: (state) => ({ user: state.user }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import type { Manual, Tool, User } from '../types';
|
||||||
|
|
||||||
|
export const MOCK_USERS: User[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
username: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
trainings: ['drill', '3d-print', 'laser', 'cnc', 'soldering'],
|
||||||
|
email: 'admin@repaircafe.dev',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
username: 'guest',
|
||||||
|
role: 'guest',
|
||||||
|
trainings: [],
|
||||||
|
email: 'guest@repaircafe.dev',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
username: 'staff',
|
||||||
|
role: 'staff',
|
||||||
|
trainings: ['drill', 'soldering'],
|
||||||
|
email: 'staff@repaircafe.dev',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_TOOLS: Tool[] = [
|
||||||
|
{
|
||||||
|
id: 't1',
|
||||||
|
name: 'Bosch Schlagbohrmaschine',
|
||||||
|
description:
|
||||||
|
'Leistungsstarke Schlagbohrmaschine 850 W für Beton, Stein und Mauerwerk. Inklusive Bohrer-Set bis 13 mm.',
|
||||||
|
location: 'Werkstatt A · Regal 2',
|
||||||
|
category: 'power',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: 'drill',
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 3,
|
||||||
|
emoji: '🪛',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't2',
|
||||||
|
name: '3D-Drucker Prusa i3 MK3S',
|
||||||
|
description:
|
||||||
|
'Präziser FDM-Drucker für PLA, PETG und ABS. Druckraum 250×210×210 mm. PLA-Filament ist verfügbar.',
|
||||||
|
location: 'Labor 1 · Schrank B',
|
||||||
|
category: 'electronics',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: '3d-print',
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 1,
|
||||||
|
emoji: '🖨️',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't3',
|
||||||
|
name: 'Schlosserhammer 500 g',
|
||||||
|
description:
|
||||||
|
'Klassischer Schlosserhammer mit Hickory-Stiel. Für allgemeine Heimwerker-Arbeiten geeignet.',
|
||||||
|
location: 'Werkstatt A · Werkzeugwand',
|
||||||
|
category: 'hand',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 7,
|
||||||
|
emoji: '🔨',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't4',
|
||||||
|
name: 'Lötstation Weller WE 1010',
|
||||||
|
description:
|
||||||
|
'Digitale Lötstation 70 W mit Temperaturregelung 100-450 °C. Inkl. Lötspitzen-Set und Entlötlitze.',
|
||||||
|
location: 'Labor 2 · Tisch 3',
|
||||||
|
category: 'electronics',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: 'soldering',
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 2,
|
||||||
|
emoji: '🔌',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't5',
|
||||||
|
name: 'Akku-Schrauber Makita 18V',
|
||||||
|
description:
|
||||||
|
'Kompakter Akku-Schrauber mit 18 V Lithium-Ionen-Akku. Inklusive Bit-Set und Ersatzakku.',
|
||||||
|
location: 'Werkstatt A · Schrank 1',
|
||||||
|
category: 'power',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 5,
|
||||||
|
emoji: '🔧',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't6',
|
||||||
|
name: 'Laser-Schneider K40',
|
||||||
|
description:
|
||||||
|
'CO2-Laserschneider 40 W für Holz, Acryl und Pappe. Arbeitsfläche 300×200 mm. Achtung: Materialliste beachten!',
|
||||||
|
location: 'Labor 1 · Standalone',
|
||||||
|
category: 'workshop',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: 'laser',
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 1,
|
||||||
|
emoji: '⚡',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't7',
|
||||||
|
name: 'Digital-Multimeter Fluke 117',
|
||||||
|
description:
|
||||||
|
'Profi-Multimeter mit AutoVolt, True-RMS und kontaktloser Spannungsprüfung. Für Elektronik-Diagnose.',
|
||||||
|
location: 'Labor 2 · Werkzeugkoffer',
|
||||||
|
category: 'measurement',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 3,
|
||||||
|
emoji: '🔋',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't8',
|
||||||
|
name: 'Nähmaschine Bernina B215',
|
||||||
|
description:
|
||||||
|
'Robuste Nähmaschine für Anfänger und Fortgeschrittene. 18 Nähstiche, automatisches Knopfloch.',
|
||||||
|
location: 'Textil-Ecke · Tisch 1',
|
||||||
|
category: 'sewing',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 2,
|
||||||
|
emoji: '🧵',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't9',
|
||||||
|
name: 'Fahrrad-Montageständer Park Tool',
|
||||||
|
description:
|
||||||
|
'Professioneller Werkstatt-Montageständer für alle Fahrrad-Reparaturen. Tragkraft bis 36 kg.',
|
||||||
|
location: 'Fahrrad-Werkstatt · Eingang',
|
||||||
|
category: 'bike',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 5,
|
||||||
|
emoji: '🚲',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't10',
|
||||||
|
name: 'Akku-Heckenschere Bosch',
|
||||||
|
description:
|
||||||
|
'Akku-Heckenschere AHS 50-20 LI mit 50 cm Schwertlänge. Für Hecken bis 20 mm Astdurchmesser.',
|
||||||
|
location: 'Garten-Schuppen',
|
||||||
|
category: 'garden',
|
||||||
|
trainingRequired: false,
|
||||||
|
homeAllowed: true,
|
||||||
|
maxRentalDays: 4,
|
||||||
|
emoji: '🌿',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't11',
|
||||||
|
name: 'Oszilloskop Rigol DS1054Z',
|
||||||
|
description:
|
||||||
|
'4-Kanal-Oszilloskop 50 MHz, 1 GSa/s Abtastrate. Für anspruchsvolle Elektronik-Messungen.',
|
||||||
|
location: 'Labor 2 · Tisch 2',
|
||||||
|
category: 'measurement',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: 'soldering',
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 2,
|
||||||
|
emoji: '📊',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 't12',
|
||||||
|
name: 'CNC-Fräse Stepcraft D.420',
|
||||||
|
description:
|
||||||
|
'Tisch-CNC-Fräse mit Arbeitsbereich 420×300×140 mm. Für Holz, Aluminium und Kunststoff.',
|
||||||
|
location: 'Labor 1 · CNC-Bereich',
|
||||||
|
category: 'workshop',
|
||||||
|
trainingRequired: true,
|
||||||
|
requiredTrainingId: 'cnc',
|
||||||
|
homeAllowed: false,
|
||||||
|
maxRentalDays: 1,
|
||||||
|
emoji: '⚙️',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_MANUALS: Manual[] = [
|
||||||
|
{
|
||||||
|
toolId: 't1',
|
||||||
|
title: 'Bohrmaschine richtig nutzen',
|
||||||
|
content:
|
||||||
|
'Spannfutter gegen den Uhrzeigersinn lösen, passenden Bohrer einsetzen, fest zudrehen. Bei Beton den Schlag-Modus aktivieren. Augenschutz tragen!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolId: 't2',
|
||||||
|
title: 'Prusa i3 vorbereiten',
|
||||||
|
content:
|
||||||
|
'Druckbett mit Isopropanol reinigen, Filament laden, Erstschicht-Kalibrierung durchführen. Erste 10 Minuten am Drucker bleiben.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolId: 't4',
|
||||||
|
title: 'Löten — Grundlagen',
|
||||||
|
content:
|
||||||
|
'Lötkolben auf 320-360 °C aufheizen. Erst Pad und Bauteil erhitzen, dann Lötzinn zuführen. Niemals direkt auf den Lötkolben löten — nur indirekt über Pad/Pin.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolId: 't6',
|
||||||
|
title: 'Laser-Cutter Sicherheit',
|
||||||
|
content:
|
||||||
|
'NIEMALS verbotene Materialien (PVC, Vinyl, Polycarbonat) schneiden — Chlorgas! Absaugung VOR Start prüfen. Während des Schnitts nie unbeaufsichtigt lassen.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolId: 't8',
|
||||||
|
title: 'Nähmaschine einfädeln',
|
||||||
|
content:
|
||||||
|
'Oberfaden vom Garnrollenhalter durch alle markierten Führungen. Spule unten einsetzen — Faden gegen den Uhrzeigersinn. Probelauf an Stoffrest.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
toolId: 't11',
|
||||||
|
title: 'Oszilloskop-Trigger',
|
||||||
|
content:
|
||||||
|
'Trigger immer auf das stabilste Signal setzen. AutoSetup gibt einen guten Startpunkt. Pegel manuell anpassen, bis das Bild "steht".',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_TRAININGS = [
|
||||||
|
{ id: 'drill', name: 'Bohrmaschinen-Einweisung', duration: '30 min' },
|
||||||
|
{ id: '3d-print', name: '3D-Druck Grundlagen', duration: '90 min' },
|
||||||
|
{ id: 'laser', name: 'Laser-Cutter Sicherheit', duration: '60 min' },
|
||||||
|
{ id: 'cnc', name: 'CNC-Fräse Einführung', duration: '120 min' },
|
||||||
|
{ id: 'soldering', name: 'Löten für Anfänger', duration: '45 min' },
|
||||||
|
];
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
import { Reservation } from '../types';
|
||||||
|
|
||||||
|
interface ReservationState {
|
||||||
|
reservations: Reservation[];
|
||||||
|
addReservation: (reservation: Omit<Reservation, 'id' | 'status'>) => string;
|
||||||
|
updateStatus: (id: string, status: Reservation['status']) => void;
|
||||||
|
removeReservation: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return Math.random().toString(36).slice(2, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isoOffset = (offsetDays: number) =>
|
||||||
|
new Date(Date.now() + offsetDays * 86_400_000).toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const INITIAL_RESERVATIONS: Reservation[] = [
|
||||||
|
{
|
||||||
|
id: 'res1',
|
||||||
|
userId: '1',
|
||||||
|
toolId: 't1',
|
||||||
|
startDate: isoOffset(0),
|
||||||
|
endDate: isoOffset(2),
|
||||||
|
status: 'approved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'res2',
|
||||||
|
userId: '2',
|
||||||
|
toolId: 't2',
|
||||||
|
startDate: isoOffset(1),
|
||||||
|
endDate: isoOffset(3),
|
||||||
|
status: 'requested',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'res3',
|
||||||
|
userId: '3',
|
||||||
|
toolId: 't4',
|
||||||
|
startDate: isoOffset(-1),
|
||||||
|
endDate: isoOffset(1),
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'res4',
|
||||||
|
userId: '1',
|
||||||
|
toolId: 't6',
|
||||||
|
startDate: isoOffset(5),
|
||||||
|
endDate: isoOffset(6),
|
||||||
|
status: 'requested',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useReservationStore = create<ReservationState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
reservations: INITIAL_RESERVATIONS,
|
||||||
|
addReservation: (res) => {
|
||||||
|
const id = randomId();
|
||||||
|
const newReservation: Reservation = {
|
||||||
|
...res,
|
||||||
|
id,
|
||||||
|
status: 'requested',
|
||||||
|
};
|
||||||
|
set((state) => ({ reservations: [...state.reservations, newReservation] }));
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
updateStatus: (id, status) =>
|
||||||
|
set((state) => ({
|
||||||
|
reservations: state.reservations.map((r) =>
|
||||||
|
r.id === id ? { ...r, status } : r
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
removeReservation: (id) =>
|
||||||
|
set((state) => ({
|
||||||
|
reservations: state.reservations.filter((r) => r.id !== id),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'patchpilot:reservations',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
import type { Manual, Tool } from '../types';
|
||||||
|
import { MOCK_TOOLS, MOCK_MANUALS } from './mock-data';
|
||||||
|
|
||||||
|
interface ToolsState {
|
||||||
|
tools: Tool[];
|
||||||
|
manuals: Manual[];
|
||||||
|
addTool: (tool: Omit<Tool, 'id'>) => string;
|
||||||
|
updateTool: (id: string, patch: Partial<Tool>) => void;
|
||||||
|
removeTool: (id: string) => void;
|
||||||
|
addManual: (manual: Manual) => void;
|
||||||
|
removeManual: (toolId: string, idx: number) => void;
|
||||||
|
resetToMockData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId() {
|
||||||
|
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return Math.random().toString(36).slice(2, 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToolsStore = create<ToolsState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
tools: MOCK_TOOLS,
|
||||||
|
manuals: MOCK_MANUALS,
|
||||||
|
addTool: (tool) => {
|
||||||
|
const id = `t${randomId()}`;
|
||||||
|
set((state) => ({ tools: [...state.tools, { ...tool, id }] }));
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
updateTool: (id, patch) =>
|
||||||
|
set((state) => ({
|
||||||
|
tools: state.tools.map((t) => (t.id === id ? { ...t, ...patch } : t)),
|
||||||
|
})),
|
||||||
|
removeTool: (id) =>
|
||||||
|
set((state) => ({ tools: state.tools.filter((t) => t.id !== id) })),
|
||||||
|
addManual: (manual) =>
|
||||||
|
set((state) => ({ manuals: [...state.manuals, manual] })),
|
||||||
|
removeManual: (toolId, idx) =>
|
||||||
|
set((state) => ({
|
||||||
|
manuals: state.manuals.filter(
|
||||||
|
(m, i) => !(m.toolId === toolId && i === idx)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
resetToMockData: () => set({ tools: MOCK_TOOLS, manuals: MOCK_MANUALS }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'patchpilot:tools',
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
version: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user