mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: bulk actions — live progress, grant subscription, error details
- Real-time progress: SSE streaming with animated progress bar, live success/error counters, scrollable log of last 10 results with auto-scroll, percentage display - New action "Выдать подписку": tariff selector + days input, warning about users with existing subscriptions being skipped - Error details: collapsible section with per-user error list, colored summary line (green successes / red errors) - Fix: modal cannot be closed during loading (backdrop + Escape) - Fix: row checkbox aria-labels now user-specific - i18n: full progress/grant/error translations (ru + en)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import apiClient from './client';
|
||||
import { tokenStorage } from '../utils/token';
|
||||
|
||||
export type BulkActionType =
|
||||
| 'extend'
|
||||
@@ -7,7 +8,8 @@ export type BulkActionType =
|
||||
| 'change_tariff'
|
||||
| 'add_traffic'
|
||||
| 'add_balance'
|
||||
| 'assign_promo_group';
|
||||
| 'assign_promo_group'
|
||||
| 'grant_subscription';
|
||||
|
||||
export interface BulkActionRequest {
|
||||
action: BulkActionType;
|
||||
@@ -23,20 +25,114 @@ export interface BulkActionParams {
|
||||
promo_group_id?: number | null;
|
||||
}
|
||||
|
||||
export interface BulkActionErrorItem {
|
||||
user_id: number;
|
||||
username?: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface BulkActionResult {
|
||||
success: boolean;
|
||||
total: number;
|
||||
success_count: number;
|
||||
error_count: number;
|
||||
errors: Array<{
|
||||
user_id: number;
|
||||
error: string;
|
||||
}>;
|
||||
errors: BulkActionErrorItem[];
|
||||
}
|
||||
|
||||
export interface BulkProgressEvent {
|
||||
type: 'progress';
|
||||
current: number;
|
||||
total: number;
|
||||
user_id: number;
|
||||
username?: string;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface BulkCompleteEvent {
|
||||
type: 'complete';
|
||||
success: boolean;
|
||||
total: number;
|
||||
success_count: number;
|
||||
error_count: number;
|
||||
errors: BulkActionErrorItem[];
|
||||
}
|
||||
|
||||
export type BulkSSEEvent = BulkProgressEvent | BulkCompleteEvent;
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
export const adminBulkActionsApi = {
|
||||
execute: async (data: BulkActionRequest): Promise<BulkActionResult> => {
|
||||
const response = await apiClient.post('/cabinet/admin/bulk/execute', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
executeWithStream: async (
|
||||
data: BulkActionRequest,
|
||||
onEvent: (event: BulkSSEEvent) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> => {
|
||||
const token = tokenStorage.getAccessToken();
|
||||
const response = await fetch(`${API_BASE_URL}/cabinet/admin/bulk/execute?stream=true`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (contentType.includes('text/event-stream') && response.body) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('data: ')) {
|
||||
try {
|
||||
const event = JSON.parse(trimmed.slice(6)) as BulkSSEEvent;
|
||||
onEvent(event);
|
||||
} catch {
|
||||
// skip malformed SSE lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process remaining buffer
|
||||
if (buffer.trim().startsWith('data: ')) {
|
||||
try {
|
||||
const event = JSON.parse(buffer.trim().slice(6)) as BulkSSEEvent;
|
||||
onEvent(event);
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: non-streaming JSON response
|
||||
const result = (await response.json()) as BulkActionResult;
|
||||
onEvent({
|
||||
type: 'complete',
|
||||
...result,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user