ppt-tool/frontend/store/slices/clientSlice.ts
Vadym Samoilenko c97841f6d1 Phase 6: Export & Polish — brand export, client dashboard, retention, analytics
- Brand-enforced export pipeline (PPTX/PDF with auto brand fonts/colors/logo)
- Client library dashboard with two-level navigation (client grid → detail tabs)
- Data retention service with ARQ cron jobs (daily cleanup + weekly purge)
- Brand-adaptive UI theme via CSS custom properties (dynamic per client)
- Analytics dashboard with overview, usage, quality, and performance metrics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:41:58 +00:00

153 lines
4.1 KiB
TypeScript

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { getHeader } from "@/app/(presentation-generator)/services/api/header";
export interface Client {
id: string;
name: string;
logo_url?: string;
}
export interface MasterDeck {
id: string;
name: string;
client_id: string;
thumbnail_url?: string;
layout_count: number;
parse_status: string;
}
export interface ClientPresentation {
id: string;
title: string;
status: string;
created_at: string;
updated_at: string;
owner_id?: string;
slides: any[];
}
interface ClientState {
clients: Client[];
selectedClientId: string | null;
masterDecks: MasterDeck[];
presentations: ClientPresentation[];
isLoadingClients: boolean;
isLoadingDecks: boolean;
isLoadingPresentations: boolean;
}
const initialState: ClientState = {
clients: [],
selectedClientId: null,
masterDecks: [],
presentations: [],
isLoadingClients: false,
isLoadingDecks: false,
isLoadingPresentations: false,
};
export const fetchClients = createAsyncThunk(
"client/fetchClients",
async (_, { rejectWithValue }) => {
try {
const response = await fetch("/api/v1/admin/clients", {
headers: getHeader(),
});
if (!response.ok) return rejectWithValue("Failed to fetch clients");
const data = await response.json();
return data.items ?? data;
} catch {
return rejectWithValue("Network error");
}
}
);
export const fetchMasterDecks = createAsyncThunk(
"client/fetchMasterDecks",
async (clientId: string, { rejectWithValue }) => {
try {
const response = await fetch(
`/api/v1/admin/master-decks?client_id=${clientId}`,
{ headers: getHeader() }
);
if (!response.ok) return rejectWithValue("Failed to fetch master decks");
const data = await response.json();
return data.items ?? data;
} catch {
return rejectWithValue("Network error");
}
}
);
export const fetchClientPresentations = createAsyncThunk(
"client/fetchClientPresentations",
async (clientId: string, { rejectWithValue }) => {
try {
const response = await fetch(
`/api/v1/ppt/presentation/all?client_id=${clientId}`,
{ headers: getHeader() }
);
if (!response.ok) {
if (response.status === 404) return [];
return rejectWithValue("Failed to fetch presentations");
}
return await response.json();
} catch {
return rejectWithValue("Network error");
}
}
);
const clientSlice = createSlice({
name: "client",
initialState,
reducers: {
setSelectedClient: (state, action: PayloadAction<string | null>) => {
state.selectedClientId = action.payload;
state.masterDecks = [];
state.presentations = [];
},
},
extraReducers: (builder) => {
builder
// Clients
.addCase(fetchClients.pending, (state) => {
state.isLoadingClients = true;
})
.addCase(fetchClients.fulfilled, (state, action) => {
state.clients = action.payload;
state.isLoadingClients = false;
})
.addCase(fetchClients.rejected, (state) => {
state.clients = [];
state.isLoadingClients = false;
})
// Master Decks
.addCase(fetchMasterDecks.pending, (state) => {
state.isLoadingDecks = true;
})
.addCase(fetchMasterDecks.fulfilled, (state, action) => {
state.masterDecks = action.payload;
state.isLoadingDecks = false;
})
.addCase(fetchMasterDecks.rejected, (state) => {
state.masterDecks = [];
state.isLoadingDecks = false;
})
// Presentations
.addCase(fetchClientPresentations.pending, (state) => {
state.isLoadingPresentations = true;
})
.addCase(fetchClientPresentations.fulfilled, (state, action) => {
state.presentations = action.payload;
state.isLoadingPresentations = false;
})
.addCase(fetchClientPresentations.rejected, (state) => {
state.presentations = [];
state.isLoadingPresentations = false;
});
},
});
export const { setSelectedClient } = clientSlice.actions;
export default clientSlice.reducer;