import { computed, onMounted, ref } from 'vue' import { useAwardsStore } from '../../stores/awards' import type { AdminTrackingFlagRule, AdminTrackingMetricRule, AdminTrackingRulesResponse, AdminTrackingSource, } from '../../types/awards' function cloneMetricRules(rules: AdminTrackingMetricRule[]) { return rules.map((rule) => ({ ...rule, autoSupportedWindowKeys: [...(rule.autoSupportedWindowKeys ?? [])], ignoredCategories: [...(rule.ignoredCategories ?? [])], })) } function cloneFlagRules(rules: AdminTrackingFlagRule[]) { return rules.map((rule) => ({ ...rule })) } function normalizeMetricRule(rule: AdminTrackingMetricRule): AdminTrackingMetricRule { const autoSupportedWindowKeys = Array.isArray(rule.autoSupportedWindowKeys) ? [...new Set(rule.autoSupportedWindowKeys.map((item) => String(item).trim()).filter(Boolean))] : [] return { ...rule, enabled: Boolean(rule.enabled), requiredForAutoClassification: Boolean(rule.requiredForAutoClassification), showInReview: Boolean(rule.showInReview), showInAdminSummary: Boolean(rule.showInAdminSummary), manualOverrideAllowed: Boolean(rule.manualOverrideAllowed), windowKey: String(rule.windowKey || '30d'), autoSupportedWindowKeys, providerFieldKey: rule.providerFieldKey?.trim() || null, topCount: rule.topCount == null ? null : Number(rule.topCount), minPrimaryCategorySharePercent: rule.minPrimaryCategorySharePercent == null ? null : Number(rule.minPrimaryCategorySharePercent), minPrimaryCategoryHours: rule.minPrimaryCategoryHours == null ? null : Number(rule.minPrimaryCategoryHours), maxDistinctCategoriesBeforeFlag: rule.maxDistinctCategoriesBeforeFlag == null ? null : Number(rule.maxDistinctCategoriesBeforeFlag), ignoredCategories: Array.isArray(rule.ignoredCategories) ? [...new Set(rule.ignoredCategories.map((item) => String(item).trim()).filter(Boolean))] : [], matchAwardCategoryAgainstTopCategories: Boolean(rule.matchAwardCategoryAgainstTopCategories), flagIfAwardCategoryNotInTopX: Boolean(rule.flagIfAwardCategoryNotInTopX), flagIfCategorySpreadTooWide: Boolean(rule.flagIfCategorySpreadTooWide), flagIfNoCategoryContextAvailable: Boolean(rule.flagIfNoCategoryContextAvailable), minValue: rule.minValue == null ? null : Number(rule.minValue), maxValue: rule.maxValue == null ? null : Number(rule.maxValue), } } function normalizeFlagRule(rule: AdminTrackingFlagRule): AdminTrackingFlagRule { return { ...rule, enabled: Boolean(rule.enabled), autoTriggerEnabled: Boolean(rule.autoTriggerEnabled), requiresManualReview: Boolean(rule.requiresManualReview), blocksApproval: Boolean(rule.blocksApproval), adminNoteRequiredOnOverride: Boolean(rule.adminNoteRequiredOnOverride), severity: rule.severity === 'high' || rule.severity === 'low' ? rule.severity : 'medium', } } function createEmptyTrackingRules(): AdminTrackingRulesResponse { return { source: { providerKey: 'twitchtracker', providerLabel: 'TwitchTracker Basic API', baseUrl: 'https://twitchtracker.com/api', notesSummary: '', showManualReviewNotesInReview: true, }, importantMetrics: [], optionalMetrics: [], flags: [], manualReviewNotes: '', } } function createSourceSnapshot(source: AdminTrackingSource) { return JSON.stringify(source) } function parseSourceSnapshot(snapshot: string): AdminTrackingSource { if (!snapshot) { return createEmptyTrackingRules().source } try { const parsed = JSON.parse(snapshot) as AdminTrackingSource return { providerKey: parsed.providerKey || 'twitchtracker', providerLabel: parsed.providerLabel || 'TwitchTracker Basic API', baseUrl: parsed.baseUrl || 'https://twitchtracker.com/api', notesSummary: parsed.notesSummary || '', showManualReviewNotesInReview: parsed.showManualReviewNotesInReview !== false, } } catch { return createEmptyTrackingRules().source } } function createRulesSnapshot( importantMetrics: AdminTrackingMetricRule[], optionalMetrics: AdminTrackingMetricRule[], flags: AdminTrackingFlagRule[], ) { return JSON.stringify({ importantMetrics: importantMetrics.map(normalizeMetricRule), optionalMetrics: optionalMetrics.map(normalizeMetricRule), flags: flags.map(normalizeFlagRule), }) } export function useAdminTrackingRules() { const store = useAwardsStore() const trackingLoading = ref(false) const rulesSaving = ref(false) const sourceSaving = ref(false) const notesSaving = ref(false) const trackingError = ref('') const trackingSuccess = ref('') const trackingSource = ref(createEmptyTrackingRules().source) const importantMetrics = ref([]) const optionalMetrics = ref([]) const trackingFlags = ref([]) const manualReviewNotes = ref('') const sourceSnapshot = ref('') const rulesSnapshot = ref('') const notesSnapshot = ref('') const trackingSummary = computed(() => ({ importantActive: importantMetrics.value.filter((item) => item.enabled).length, optionalActive: optionalMetrics.value.filter((item) => item.enabled).length, flagsActive: trackingFlags.value.filter((item) => item.enabled).length, manualReviewFlags: trackingFlags.value.filter((item) => item.enabled && item.requiresManualReview).length, })) const hasUnsavedTrackingSourceChanges = computed(() => createSourceSnapshot(trackingSource.value) !== sourceSnapshot.value) const hasUnsavedTrackingRuleChanges = computed(() => createRulesSnapshot(importantMetrics.value, optionalMetrics.value, trackingFlags.value) !== rulesSnapshot.value, ) const hasUnsavedTrackingNotesChanges = computed(() => manualReviewNotes.value !== notesSnapshot.value) function applyResponse(response: AdminTrackingRulesResponse) { trackingSource.value = { providerKey: response.source?.providerKey || 'twitchtracker', providerLabel: response.source?.providerLabel || 'TwitchTracker Basic API', baseUrl: response.source?.baseUrl || 'https://twitchtracker.com/api', notesSummary: response.source?.notesSummary || '', showManualReviewNotesInReview: response.source?.showManualReviewNotesInReview !== false, } importantMetrics.value = cloneMetricRules(response.importantMetrics) optionalMetrics.value = cloneMetricRules(response.optionalMetrics) trackingFlags.value = cloneFlagRules(response.flags) manualReviewNotes.value = response.manualReviewNotes ?? '' sourceSnapshot.value = createSourceSnapshot(trackingSource.value) rulesSnapshot.value = createRulesSnapshot(importantMetrics.value, optionalMetrics.value, trackingFlags.value) notesSnapshot.value = manualReviewNotes.value } async function loadTrackingRules() { trackingLoading.value = true trackingError.value = '' trackingSuccess.value = '' try { applyResponse(await store.loadAdminTrackingRules()) } catch (error) { trackingError.value = error instanceof Error ? error.message : 'Tracking Rules konnten nicht geladen werden.' applyResponse(createEmptyTrackingRules()) } finally { trackingLoading.value = false } } function updateImportantMetric(ruleKey: string, patch: Partial) { importantMetrics.value = importantMetrics.value.map((rule) => rule.key === ruleKey ? normalizeMetricRule({ ...rule, ...patch }) : rule, ) } function updateOptionalMetric(ruleKey: string, patch: Partial) { optionalMetrics.value = optionalMetrics.value.map((rule) => rule.key === ruleKey ? normalizeMetricRule({ ...rule, ...patch }) : rule, ) } function updateTrackingFlag(ruleKey: string, patch: Partial) { trackingFlags.value = trackingFlags.value.map((rule) => rule.key === ruleKey ? normalizeFlagRule({ ...rule, ...patch }) : rule, ) } async function saveTrackingSource() { sourceSaving.value = true trackingError.value = '' trackingSuccess.value = '' try { const response = await store.updateAdminTrackingSource({ source: { ...trackingSource.value }, }) applyResponse(response) trackingSuccess.value = 'Tracking Source wurde gespeichert.' } catch (error) { trackingError.value = error instanceof Error ? error.message : 'Tracking Source konnte nicht gespeichert werden.' } finally { sourceSaving.value = false } } async function saveTrackingNotes() { notesSaving.value = true trackingError.value = '' trackingSuccess.value = '' try { const response = await store.updateAdminTrackingReviewNotes({ manualReviewNotes: manualReviewNotes.value, showManualReviewNotesInReview: trackingSource.value.showManualReviewNotesInReview, }) applyResponse(response) trackingSuccess.value = 'Manual Review Notes wurden gespeichert.' } catch (error) { trackingError.value = error instanceof Error ? error.message : 'Manual Review Notes konnten nicht gespeichert werden.' } finally { notesSaving.value = false } } async function saveTrackingRules() { rulesSaving.value = true trackingError.value = '' trackingSuccess.value = '' try { const response = await store.updateAdminTrackingRules({ source: parseSourceSnapshot(sourceSnapshot.value), importantMetrics: importantMetrics.value.map(normalizeMetricRule), optionalMetrics: optionalMetrics.value.map(normalizeMetricRule), flags: trackingFlags.value.map(normalizeFlagRule), manualReviewNotes: notesSnapshot.value, }) applyResponse(response) trackingSuccess.value = 'Tracking Rules wurden gespeichert.' } catch (error) { trackingError.value = error instanceof Error ? error.message : 'Tracking Rules konnten nicht gespeichert werden.' } finally { rulesSaving.value = false } } onMounted(loadTrackingRules) return { trackingLoading, rulesSaving, sourceSaving, notesSaving, trackingError, trackingSuccess, trackingSource, importantMetrics, optionalMetrics, trackingFlags, manualReviewNotes, trackingSummary, hasUnsavedTrackingSourceChanges, hasUnsavedTrackingRuleChanges, hasUnsavedTrackingNotesChanges, loadTrackingRules, updateImportantMetric, updateOptionalMetric, updateTrackingFlag, saveTrackingSource, saveTrackingNotes, saveTrackingRules, } }