Files
bakery-ia/frontend/src/contexts/BakeryContext.tsx

359 lines
10 KiB
TypeScript
Raw Normal View History

2025-08-31 22:14:05 +02:00
import React, { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
export interface Bakery {
id: string;
name: string;
logo?: string;
role: 'owner' | 'manager' | 'baker' | 'staff';
status: 'active' | 'inactive';
address?: string;
description?: string;
settings?: {
timezone: string;
currency: string;
language: string;
};
permissions?: string[];
}
interface BakeryState {
bakeries: Bakery[];
currentBakery: Bakery | null;
isLoading: boolean;
error: string | null;
}
type BakeryAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_BAKERIES'; payload: Bakery[] }
| { type: 'SET_CURRENT_BAKERY'; payload: Bakery }
| { type: 'ADD_BAKERY'; payload: Bakery }
| { type: 'UPDATE_BAKERY'; payload: { id: string; updates: Partial<Bakery> } }
| { type: 'REMOVE_BAKERY'; payload: string };
const initialState: BakeryState = {
bakeries: [],
currentBakery: null,
isLoading: false,
error: null,
};
// Mock bakeries data
const mockBakeries: Bakery[] = [
{
id: '1',
name: 'Panadería San Miguel',
logo: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=100&h=100&fit=crop&crop=center',
role: 'owner',
status: 'active',
address: 'Calle Mayor 123, Madrid, 28013',
description: 'Panadería tradicional familiar con más de 30 años de experiencia.',
settings: {
timezone: 'Europe/Madrid',
currency: 'EUR',
language: 'es',
},
permissions: ['*'],
},
{
id: '2',
name: 'Panadería La Artesana',
logo: 'https://images.unsplash.com/photo-1509440159596-0249088772ff?w=100&h=100&fit=crop&crop=center',
role: 'manager',
status: 'active',
address: 'Avenida de la Paz 45, Barcelona, 08001',
description: 'Panadería artesanal especializada en panes tradicionales catalanes.',
settings: {
timezone: 'Europe/Madrid',
currency: 'EUR',
language: 'ca',
},
permissions: ['inventory:*', 'production:*', 'sales:*', 'reports:read'],
},
{
id: '3',
name: 'Pan y Masa',
logo: 'https://images.unsplash.com/photo-1524704654690-b56c05c78a00?w=100&h=100&fit=crop&crop=center',
role: 'baker',
status: 'active',
address: 'Plaza Central 8, Valencia, 46001',
description: 'Panadería moderna con enfoque en productos orgánicos.',
settings: {
timezone: 'Europe/Madrid',
currency: 'EUR',
language: 'es',
},
permissions: ['production:*', 'inventory:read', 'inventory:update'],
},
{
id: '4',
name: 'Horno Dorado',
role: 'staff',
status: 'inactive',
address: 'Calle del Sol 67, Sevilla, 41001',
description: 'Panadería tradicional andaluza.',
settings: {
timezone: 'Europe/Madrid',
currency: 'EUR',
language: 'es',
},
permissions: ['inventory:read', 'sales:read'],
},
];
function bakeryReducer(state: BakeryState, action: BakeryAction): BakeryState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, isLoading: false };
case 'SET_BAKERIES':
return {
...state,
bakeries: action.payload,
currentBakery: state.currentBakery || action.payload[0] || null,
isLoading: false,
error: null
};
case 'SET_CURRENT_BAKERY':
// Save to localStorage for persistence
localStorage.setItem('selectedBakery', action.payload.id);
return { ...state, currentBakery: action.payload };
case 'ADD_BAKERY':
return {
...state,
bakeries: [...state.bakeries, action.payload]
};
case 'UPDATE_BAKERY':
return {
...state,
bakeries: state.bakeries.map(bakery =>
bakery.id === action.payload.id
? { ...bakery, ...action.payload.updates }
: bakery
),
currentBakery: state.currentBakery?.id === action.payload.id
? { ...state.currentBakery, ...action.payload.updates }
: state.currentBakery
};
case 'REMOVE_BAKERY':
const remainingBakeries = state.bakeries.filter(b => b.id !== action.payload);
return {
...state,
bakeries: remainingBakeries,
currentBakery: state.currentBakery?.id === action.payload
? remainingBakeries[0] || null
: state.currentBakery
};
default:
return state;
}
}
interface BakeryContextType extends BakeryState {
selectBakery: (bakery: Bakery) => void;
addBakery: (bakery: Omit<Bakery, 'id'>) => Promise<void>;
updateBakery: (id: string, updates: Partial<Bakery>) => Promise<void>;
removeBakery: (id: string) => Promise<void>;
refreshBakeries: () => Promise<void>;
hasPermission: (permission: string) => boolean;
canAccess: (resource: string, action: string) => boolean;
}
const BakeryContext = createContext<BakeryContextType | undefined>(undefined);
interface BakeryProviderProps {
children: ReactNode;
}
export function BakeryProvider({ children }: BakeryProviderProps) {
const [state, dispatch] = useReducer(bakeryReducer, initialState);
// Load bakeries on mount
useEffect(() => {
loadBakeries();
}, []);
// Load saved bakery selection
useEffect(() => {
if (state.bakeries.length > 0 && !state.currentBakery) {
const savedBakeryId = localStorage.getItem('selectedBakery');
const savedBakery = savedBakeryId
? state.bakeries.find(b => b.id === savedBakeryId)
: null;
if (savedBakery) {
dispatch({ type: 'SET_CURRENT_BAKERY', payload: savedBakery });
} else if (state.bakeries[0]) {
dispatch({ type: 'SET_CURRENT_BAKERY', payload: state.bakeries[0] });
}
}
}, [state.bakeries, state.currentBakery]);
const loadBakeries = async () => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 1000));
// In a real app, this would be an API call
dispatch({ type: 'SET_BAKERIES', payload: mockBakeries });
} catch (error) {
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Error loading bakeries'
});
}
};
const selectBakery = (bakery: Bakery) => {
dispatch({ type: 'SET_CURRENT_BAKERY', payload: bakery });
};
const addBakery = async (bakeryData: Omit<Bakery, 'id'>) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const newBakery: Bakery = {
...bakeryData,
id: Date.now().toString(), // In real app, this would come from the API
};
dispatch({ type: 'ADD_BAKERY', payload: newBakery });
dispatch({ type: 'SET_LOADING', payload: false });
} catch (error) {
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Error adding bakery'
});
}
};
const updateBakery = async (id: string, updates: Partial<Bakery>) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: 'UPDATE_BAKERY', payload: { id, updates } });
dispatch({ type: 'SET_LOADING', payload: false });
} catch (error) {
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Error updating bakery'
});
}
};
const removeBakery = async (id: string) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
dispatch({ type: 'REMOVE_BAKERY', payload: id });
dispatch({ type: 'SET_LOADING', payload: false });
} catch (error) {
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Error removing bakery'
});
}
};
const refreshBakeries = async () => {
await loadBakeries();
};
const hasPermission = (permission: string): boolean => {
if (!state.currentBakery) return false;
const permissions = state.currentBakery.permissions || [];
// Admin/owner has all permissions
if (permissions.includes('*')) return true;
return permissions.includes(permission);
};
const canAccess = (resource: string, action: string): boolean => {
if (!state.currentBakery) return false;
// Check specific permission
if (hasPermission(`${resource}:${action}`)) return true;
// Check wildcard permissions
if (hasPermission(`${resource}:*`)) return true;
// Role-based access fallback
switch (state.currentBakery.role) {
case 'owner':
return true;
case 'manager':
return ['inventory', 'production', 'sales', 'reports'].includes(resource);
case 'baker':
return ['production', 'inventory'].includes(resource) &&
['read', 'update'].includes(action);
case 'staff':
return ['inventory', 'sales'].includes(resource) &&
action === 'read';
default:
return false;
}
};
const value: BakeryContextType = {
...state,
selectBakery,
addBakery,
updateBakery,
removeBakery,
refreshBakeries,
hasPermission,
canAccess,
};
return (
<BakeryContext.Provider value={value}>
{children}
</BakeryContext.Provider>
);
}
export function useBakery(): BakeryContextType {
const context = useContext(BakeryContext);
if (context === undefined) {
throw new Error('useBakery must be used within a BakeryProvider');
}
return context;
}
// Convenience hooks
export const useCurrentBakery = () => {
const { currentBakery } = useBakery();
return currentBakery;
};
export const useBakeries = () => {
const { bakeries } = useBakery();
return bakeries;
};
export const useBakeryPermissions = () => {
const { hasPermission, canAccess } = useBakery();
return { hasPermission, canAccess };
};