- 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>
153 lines
4.1 KiB
TypeScript
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;
|