Files
cannamanage/cannamanage-frontend/src/services/documents.ts
T
Patrick Plate dad798a904
Deploy to TrueNAS / deploy (push) Failing after 33s
feat: Sprint 14 — Marketing & Monetization
- Landing page with hero, feature grid, trust signals
- Split-layout login redesign (admin + portal)
- Pricing page with storage tiers (5GB/50GB/unlimited)
- StorageQuotaService backend (V36 migration, 402 on exceeded)
- Frontend storage integration + 402 error handling
- StorageController uses TenantContext for tenant isolation
- onTierChange() hook for subscription tier updates
2026-06-18 20:28:35 +02:00

178 lines
4.3 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { apiClient } from "@/lib/api-client"
// --- Constants ---
const CLUB_ID = "00000000-0000-0000-0000-000000000001"
// --- Types ---
export type DocumentCategory =
| "SATZUNG"
| "PROTOKOLL"
| "VERTRAG"
| "VERSICHERUNG"
| "GENEHMIGUNG"
| "SONSTIGES"
export type DocumentAccessLevel = "ALL_MEMBERS" | "BOARD_ONLY"
export interface ClubDocument {
id: string
title: string
category: DocumentCategory
filename: string
contentType: string
fileSize: number
accessLevel: DocumentAccessLevel
description: string | null
uploadedBy: string
createdAt: string
updatedAt: string | null
}
export interface StorageUsage {
bytesUsed: number
}
export interface UploadDocumentRequest {
title: string
category: DocumentCategory
accessLevel: DocumentAccessLevel
description: string | null
file: File
}
// --- Raw API functions ---
export async function uploadDocument(
clubId: string,
title: string,
category: DocumentCategory,
accessLevel: DocumentAccessLevel,
description: string | null,
file: File
): Promise<ClubDocument> {
const formData = new FormData()
formData.append("file", file)
const params = new URLSearchParams({
clubId,
title,
category,
accessLevel,
})
if (description) params.append("description", description)
// Multipart upload — use raw fetch since apiClient assumes JSON
const res = await fetch(
`/api/backend/documents/upload?${params.toString()}`,
{
method: "POST",
body: formData,
}
)
if (!res.ok) {
if (res.status === 402) {
const problem = await res.json()
const error = new Error("Storage quota exceeded") as Error & {
status: number
problemDetail: unknown
}
error.status = 402
error.problemDetail = problem
throw error
}
throw new Error("Upload failed")
}
return res.json()
}
export function listDocuments(
clubId: string,
category?: DocumentCategory,
accessLevel?: DocumentAccessLevel
): Promise<ClubDocument[]> {
const params = new URLSearchParams({ clubId })
if (category) params.append("category", category)
if (accessLevel) params.append("accessLevel", accessLevel)
return apiClient<ClubDocument[]>(`/documents?${params.toString()}`)
}
export async function downloadDocument(id: string): Promise<Blob> {
const res = await fetch(`/api/backend/documents/${id}/download`)
if (!res.ok) throw new Error("Download failed")
return res.blob()
}
export function deleteDocument(id: string, clubId: string): Promise<void> {
return apiClient<void>(`/documents/${id}?clubId=${clubId}`, {
method: "DELETE",
})
}
export function getStorageUsage(clubId: string): Promise<StorageUsage> {
return apiClient<StorageUsage>(`/documents/usage?clubId=${clubId}`)
}
export function getPortalDocuments(clubId: string): Promise<ClubDocument[]> {
return apiClient<ClubDocument[]>(`/portal/documents?clubId=${clubId}`)
}
// --- React Query Hooks ---
export function useDocumentsQuery(category?: DocumentCategory) {
return useQuery({
queryKey: ["documents", CLUB_ID, category],
queryFn: () => listDocuments(CLUB_ID, category),
})
}
export function useUploadDocumentMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: UploadDocumentRequest) =>
uploadDocument(
CLUB_ID,
data.title,
data.category,
data.accessLevel,
data.description,
data.file
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["documents"] })
},
})
}
export function useDeleteDocumentMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: string) => deleteDocument(id, CLUB_ID),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["documents"] })
},
})
}
// --- Helper: format file size ---
export function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
// --- Category labels ---
export const categoryLabels: Record<DocumentCategory, string> = {
SATZUNG: "Satzung",
PROTOKOLL: "Protokoll",
VERTRAG: "Vertrag",
VERSICHERUNG: "Versicherung",
GENEHMIGUNG: "Genehmigung",
SONSTIGES: "Sonstiges",
}