[GAPS] feat(frontend): Implement 4 missing Zustand stores (tenant, subscription, notification, feature-flag)
This commit is contained in:
parent
0f9899570b
commit
07064e3346
114
apps/frontend/src/stores/feature-flag.store.ts
Normal file
114
apps/frontend/src/stores/feature-flag.store.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export interface FeatureFlag {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
value?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FeatureFlagState {
|
||||||
|
flags: Record<string, FeatureFlag>;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastFetched: number | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchFlags: () => Promise<void>;
|
||||||
|
isEnabled: (flagKey: string) => boolean;
|
||||||
|
getFlagValue: <T = unknown>(flagKey: string, defaultValue?: T) => T;
|
||||||
|
refreshFlags: () => Promise<void>;
|
||||||
|
clearFlags: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
export const useFeatureFlagStore = create<FeatureFlagState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
flags: {},
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastFetched: null,
|
||||||
|
|
||||||
|
fetchFlags: async () => {
|
||||||
|
const { lastFetched } = get();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Use cached flags if still valid
|
||||||
|
if (lastFetched && now - lastFetched < CACHE_DURATION_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/feature-flags`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch feature flags');
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagsArray: FeatureFlag[] = await response.json();
|
||||||
|
const flags: Record<string, FeatureFlag> = {};
|
||||||
|
|
||||||
|
flagsArray.forEach((flag) => {
|
||||||
|
flags[flag.key] = flag;
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ flags, isLoading: false, lastFetched: now });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isEnabled: (flagKey) => {
|
||||||
|
const { flags } = get();
|
||||||
|
const flag = flags[flagKey];
|
||||||
|
return flag?.enabled ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
getFlagValue: <T = unknown>(flagKey: string, defaultValue?: T): T => {
|
||||||
|
const { flags } = get();
|
||||||
|
const flag = flags[flagKey];
|
||||||
|
|
||||||
|
if (!flag || !flag.enabled) {
|
||||||
|
return defaultValue as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (flag.value ?? defaultValue) as T;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFlags: async () => {
|
||||||
|
// Force refresh by clearing lastFetched
|
||||||
|
set({ lastFetched: null });
|
||||||
|
await get().fetchFlags();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFlags: () => set({ flags: {}, lastFetched: null, error: null }),
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'feature-flag-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
flags: state.flags,
|
||||||
|
lastFetched: state.lastFetched,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@ -1,2 +1,6 @@
|
|||||||
export * from './auth.store';
|
export * from './auth.store';
|
||||||
export * from './ui.store';
|
export * from './ui.store';
|
||||||
|
export * from './tenant.store';
|
||||||
|
export * from './subscription.store';
|
||||||
|
export * from './notification.store';
|
||||||
|
export * from './feature-flag.store';
|
||||||
|
|||||||
235
apps/frontend/src/stores/notification.store.ts
Normal file
235
apps/frontend/src/stores/notification.store.ts
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: string;
|
||||||
|
type: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
read: boolean;
|
||||||
|
created_at: string;
|
||||||
|
action_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationPreferences {
|
||||||
|
email_enabled: boolean;
|
||||||
|
push_enabled: boolean;
|
||||||
|
in_app_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationState {
|
||||||
|
notifications: Notification[];
|
||||||
|
unreadCount: number;
|
||||||
|
preferences: NotificationPreferences;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchNotifications: () => Promise<void>;
|
||||||
|
markAsRead: (notificationId: string) => Promise<void>;
|
||||||
|
markAllAsRead: () => Promise<void>;
|
||||||
|
deleteNotification: (notificationId: string) => Promise<void>;
|
||||||
|
clearAllNotifications: () => Promise<void>;
|
||||||
|
updatePreferences: (prefs: Partial<NotificationPreferences>) => Promise<void>;
|
||||||
|
addNotification: (notification: Notification) => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNotificationStore = create<NotificationState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
notifications: [],
|
||||||
|
unreadCount: 0,
|
||||||
|
preferences: {
|
||||||
|
email_enabled: true,
|
||||||
|
push_enabled: true,
|
||||||
|
in_app_enabled: true,
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
fetchNotifications: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch notifications');
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifications = await response.json();
|
||||||
|
const unreadCount = notifications.filter((n: Notification) => !n.read).length;
|
||||||
|
set({ notifications, unreadCount, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
markAsRead: async (notificationId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications/${notificationId}/read`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to mark notification as read');
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
notifications: state.notifications.map((n) =>
|
||||||
|
n.id === notificationId ? { ...n, read: true } : n
|
||||||
|
),
|
||||||
|
unreadCount: Math.max(0, state.unreadCount - 1),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
markAllAsRead: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications/read-all`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to mark all notifications as read');
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
notifications: state.notifications.map((n) => ({ ...n, read: true })),
|
||||||
|
unreadCount: 0,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteNotification: async (notificationId) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications/${notificationId}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => {
|
||||||
|
const notification = state.notifications.find((n) => n.id === notificationId);
|
||||||
|
const wasUnread = notification && !notification.read;
|
||||||
|
return {
|
||||||
|
notifications: state.notifications.filter((n) => n.id !== notificationId),
|
||||||
|
unreadCount: wasUnread ? state.unreadCount - 1 : state.unreadCount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAllNotifications: async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to clear notifications');
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ notifications: [], unreadCount: 0 });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePreferences: async (prefs) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/notifications/preferences`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(prefs),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update preferences');
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
preferences: { ...state.preferences, ...prefs },
|
||||||
|
isLoading: false,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addNotification: (notification) => {
|
||||||
|
set((state) => ({
|
||||||
|
notifications: [notification, ...state.notifications],
|
||||||
|
unreadCount: notification.read ? state.unreadCount : state.unreadCount + 1,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'notification-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
preferences: state.preferences,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
209
apps/frontend/src/stores/subscription.store.ts
Normal file
209
apps/frontend/src/stores/subscription.store.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export interface SubscriptionPlan {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
billing_period: 'monthly' | 'yearly';
|
||||||
|
features: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subscription {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
plan: SubscriptionPlan;
|
||||||
|
status: 'active' | 'canceled' | 'past_due' | 'trialing';
|
||||||
|
current_period_start: string;
|
||||||
|
current_period_end: string;
|
||||||
|
cancel_at_period_end: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionState {
|
||||||
|
subscription: Subscription | null;
|
||||||
|
availablePlans: SubscriptionPlan[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setSubscription: (subscription: Subscription) => void;
|
||||||
|
fetchSubscription: () => Promise<void>;
|
||||||
|
fetchPlans: () => Promise<void>;
|
||||||
|
changePlan: (planId: string) => Promise<void>;
|
||||||
|
cancelSubscription: () => Promise<void>;
|
||||||
|
reactivateSubscription: () => Promise<void>;
|
||||||
|
clearSubscription: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubscriptionStore = create<SubscriptionState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
subscription: null,
|
||||||
|
availablePlans: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
setSubscription: (subscription) => set({ subscription }),
|
||||||
|
|
||||||
|
fetchSubscription: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/subscriptions/current`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = await response.json();
|
||||||
|
set({ subscription, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPlans: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/plans`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch plans');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plans = await response.json();
|
||||||
|
set({ availablePlans: plans, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changePlan: async (planId) => {
|
||||||
|
const { subscription } = get();
|
||||||
|
if (!subscription) return;
|
||||||
|
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/subscriptions/${subscription.id}/change-plan`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ plan_id: planId }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to change plan');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSubscription = await response.json();
|
||||||
|
set({ subscription: updatedSubscription, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelSubscription: async () => {
|
||||||
|
const { subscription } = get();
|
||||||
|
if (!subscription) return;
|
||||||
|
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/subscriptions/${subscription.id}/cancel`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to cancel subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSubscription = await response.json();
|
||||||
|
set({ subscription: updatedSubscription, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reactivateSubscription: async () => {
|
||||||
|
const { subscription } = get();
|
||||||
|
if (!subscription) return;
|
||||||
|
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/subscriptions/${subscription.id}/reactivate`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to reactivate subscription');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSubscription = await response.json();
|
||||||
|
set({ subscription: updatedSubscription, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSubscription: () => set({ subscription: null, error: null }),
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'subscription-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
subscription: state.subscription,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
134
apps/frontend/src/stores/tenant.store.ts
Normal file
134
apps/frontend/src/stores/tenant.store.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export interface Tenant {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
plan: string;
|
||||||
|
settings: Record<string, unknown>;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantState {
|
||||||
|
tenant: Tenant | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setTenant: (tenant: Tenant) => void;
|
||||||
|
fetchTenant: () => Promise<void>;
|
||||||
|
updateTenant: (data: Partial<Tenant>) => Promise<void>;
|
||||||
|
switchTenant: (tenantId: string) => Promise<void>;
|
||||||
|
clearTenant: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTenantStore = create<TenantState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
tenant: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
setTenant: (tenant) => set({ tenant }),
|
||||||
|
|
||||||
|
fetchTenant: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/tenants/current`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch tenant');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = await response.json();
|
||||||
|
set({ tenant, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateTenant: async (data) => {
|
||||||
|
const { tenant } = get();
|
||||||
|
if (!tenant) return;
|
||||||
|
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/tenants/${tenant.id}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update tenant');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTenant = await response.json();
|
||||||
|
set({ tenant: updatedTenant, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
switchTenant: async (tenantId) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${import.meta.env.VITE_API_URL || '/api/v1'}/tenants/${tenantId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to switch tenant');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = await response.json();
|
||||||
|
set({ tenant, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearTenant: () => set({ tenant: null, error: null }),
|
||||||
|
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'tenant-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
tenant: state.tenant,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@ -216,7 +216,7 @@ shared:
|
|||||||
- WhatsAppTestMessage.tsx
|
- WhatsAppTestMessage.tsx
|
||||||
|
|
||||||
stores:
|
stores:
|
||||||
nota_auditoria: "Solo 2 stores implementados de 5 documentados - Auditoria 2026-01-24"
|
nota_auditoria: "6 stores implementados - Auditoria 2026-01-25"
|
||||||
implementados:
|
implementados:
|
||||||
- nombre: "authStore"
|
- nombre: "authStore"
|
||||||
archivo: "auth.store.ts"
|
archivo: "auth.store.ts"
|
||||||
@ -243,30 +243,63 @@ shared:
|
|||||||
usa_persist: true
|
usa_persist: true
|
||||||
storage_key: "ui-storage"
|
storage_key: "ui-storage"
|
||||||
nota: "NO DOCUMENTADO previamente - agregado en auditoria"
|
nota: "NO DOCUMENTADO previamente - agregado en auditoria"
|
||||||
no_implementados:
|
|
||||||
- nombre: "tenantStore"
|
- nombre: "tenantStore"
|
||||||
estado: "no_implementado"
|
archivo: "tenant.store.ts"
|
||||||
actions_planificadas:
|
estado: "completado"
|
||||||
|
actions_implementadas:
|
||||||
|
- setTenant
|
||||||
- fetchTenant
|
- fetchTenant
|
||||||
- updateTenant
|
- updateTenant
|
||||||
- switchTenant
|
- switchTenant
|
||||||
|
- clearTenant
|
||||||
|
- clearError
|
||||||
|
usa_persist: true
|
||||||
|
storage_key: "tenant-storage"
|
||||||
|
nota: "IMPLEMENTADO 2026-01-25 - Correccion de gaps cross-project"
|
||||||
- nombre: "subscriptionStore"
|
- nombre: "subscriptionStore"
|
||||||
estado: "no_implementado"
|
archivo: "subscription.store.ts"
|
||||||
actions_planificadas:
|
estado: "completado"
|
||||||
|
actions_implementadas:
|
||||||
|
- setSubscription
|
||||||
- fetchSubscription
|
- fetchSubscription
|
||||||
- updatePlan
|
- fetchPlans
|
||||||
|
- changePlan
|
||||||
- cancelSubscription
|
- cancelSubscription
|
||||||
|
- reactivateSubscription
|
||||||
|
- clearSubscription
|
||||||
|
- clearError
|
||||||
|
usa_persist: true
|
||||||
|
storage_key: "subscription-storage"
|
||||||
|
nota: "IMPLEMENTADO 2026-01-25 - Correccion de gaps cross-project"
|
||||||
- nombre: "notificationStore"
|
- nombre: "notificationStore"
|
||||||
estado: "no_implementado"
|
archivo: "notification.store.ts"
|
||||||
actions_planificadas:
|
estado: "completado"
|
||||||
|
actions_implementadas:
|
||||||
- fetchNotifications
|
- fetchNotifications
|
||||||
- markAsRead
|
- markAsRead
|
||||||
- subscribe
|
- markAllAsRead
|
||||||
|
- deleteNotification
|
||||||
|
- clearAllNotifications
|
||||||
|
- updatePreferences
|
||||||
|
- addNotification
|
||||||
|
- clearError
|
||||||
|
usa_persist: true
|
||||||
|
storage_key: "notification-storage"
|
||||||
|
nota: "IMPLEMENTADO 2026-01-25 - Correccion de gaps cross-project"
|
||||||
- nombre: "featureFlagStore"
|
- nombre: "featureFlagStore"
|
||||||
estado: "no_implementado"
|
archivo: "feature-flag.store.ts"
|
||||||
actions_planificadas:
|
estado: "completado"
|
||||||
|
actions_implementadas:
|
||||||
- fetchFlags
|
- fetchFlags
|
||||||
- evaluateFlag
|
- isEnabled
|
||||||
|
- getFlagValue
|
||||||
|
- refreshFlags
|
||||||
|
- clearFlags
|
||||||
|
- clearError
|
||||||
|
usa_persist: true
|
||||||
|
storage_key: "feature-flag-storage"
|
||||||
|
nota: "IMPLEMENTADO 2026-01-25 - Correccion de gaps cross-project"
|
||||||
|
no_implementados: []
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- nombre: "api (axios instance)"
|
- nombre: "api (axios instance)"
|
||||||
@ -420,12 +453,12 @@ shared:
|
|||||||
- useMediaQuery
|
- useMediaQuery
|
||||||
|
|
||||||
resumen:
|
resumen:
|
||||||
nota_auditoria: "CORRECCION 2026-01-24: Sales y Commissions ahora incluidos"
|
nota_auditoria: "CORRECCION 2026-01-25: Todos los stores implementados"
|
||||||
total_pages: 38
|
total_pages: 38
|
||||||
total_components_implementados: 40
|
total_components_implementados: 40
|
||||||
total_components_documentados_no_impl: 60
|
total_components_documentados_no_impl: 60
|
||||||
total_stores_implementados: 2
|
total_stores_implementados: 6
|
||||||
total_stores_no_implementados: 4
|
total_stores_no_implementados: 0
|
||||||
total_hooks_implementados: 64
|
total_hooks_implementados: 64
|
||||||
total_hooks_documentados_no_impl: 0
|
total_hooks_documentados_no_impl: 0
|
||||||
total_api_services: 24
|
total_api_services: 24
|
||||||
@ -437,17 +470,16 @@ planificado:
|
|||||||
pages_objetivo: 38
|
pages_objetivo: 38
|
||||||
components_actuales: 40
|
components_actuales: 40
|
||||||
components_objetivo: 100
|
components_objetivo: 100
|
||||||
stores_actuales: 2
|
stores_actuales: 6
|
||||||
stores_objetivo: 6
|
stores_objetivo: 6
|
||||||
hooks_actuales: 64
|
hooks_actuales: 64
|
||||||
hooks_objetivo: 64
|
hooks_objetivo: 64
|
||||||
nota: "CORRECCION: Sales y Commissions SI implementados en frontend"
|
nota: "COMPLETADO: Todos los 6 stores Zustand implementados"
|
||||||
|
|
||||||
gaps_identificados:
|
gaps_identificados:
|
||||||
criticos: []
|
criticos: []
|
||||||
altos:
|
altos:
|
||||||
- "Componentes UI base: No existen wrappers (se usa headlessui directo)"
|
- "Componentes UI base: No existen wrappers (se usa headlessui directo)"
|
||||||
- "4 stores Zustand adicionales pendientes"
|
|
||||||
medios:
|
medios:
|
||||||
- "Componentes Forms no implementados como wrappers"
|
- "Componentes Forms no implementados como wrappers"
|
||||||
resueltos_2026_01_24:
|
resueltos_2026_01_24:
|
||||||
@ -457,6 +489,7 @@ gaps_identificados:
|
|||||||
- "authStore completado (refreshTokens y updateProfile implementados)"
|
- "authStore completado (refreshTokens y updateProfile implementados)"
|
||||||
- "Rutas Sales/Commissions agregadas al router (antes existian paginas pero no rutas)"
|
- "Rutas Sales/Commissions agregadas al router (antes existian paginas pero no rutas)"
|
||||||
- "Documentacion FRONTEND-ROUTING.md creada"
|
- "Documentacion FRONTEND-ROUTING.md creada"
|
||||||
|
- "4 Zustand stores implementados: tenantStore, subscriptionStore, notificationStore, featureFlagStore"
|
||||||
|
|
||||||
dependencias_npm:
|
dependencias_npm:
|
||||||
core:
|
core:
|
||||||
@ -485,6 +518,10 @@ dependencias_npm:
|
|||||||
ultima_actualizacion: "2026-01-25"
|
ultima_actualizacion: "2026-01-25"
|
||||||
actualizado_por: "Claude Opus 4.5 (Alineacion Doc-Codigo)"
|
actualizado_por: "Claude Opus 4.5 (Alineacion Doc-Codigo)"
|
||||||
historial_cambios:
|
historial_cambios:
|
||||||
|
- fecha: "2026-01-25"
|
||||||
|
tipo: "implementacion"
|
||||||
|
descripcion: "4 Zustand stores implementados (tenant, subscription, notification, feature-flag). Correccion de gaps cross-project. index.ts actualizado."
|
||||||
|
agente: "Claude Opus 4.5 (Correccion Gaps Cross-Project)"
|
||||||
- fecha: "2026-01-25"
|
- fecha: "2026-01-25"
|
||||||
tipo: "alineacion"
|
tipo: "alineacion"
|
||||||
descripcion: "Rutas Sales/Commissions agregadas al router. authStore completado (refreshTokens, updateProfile). Documentacion FRONTEND-ROUTING.md creada."
|
descripcion: "Rutas Sales/Commissions agregadas al router. authStore completado (refreshTokens, updateProfile). Documentacion FRONTEND-ROUTING.md creada."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user