import { ID, Query, databaseId, tables } from './appwrite'; import { teamRechte } from './permissions'; /** * Ein Post ist ein **Slot-Rezept**, kein Freitext-Prompt – nur deshalb ist er * überhaupt kopierbar (konzept-bilder-feed.md §3). Die Slots liegen strukturiert * in `slots`, damit P19 daraus später Chips ableiten kann, ohne den Prompt im * Wortlaut preiszugeben. */ export type Slots = { kulisse_asset_id?: string; person_asset_id?: string; produkt_asset_id?: string; licht?: string; kamera?: string; farbe?: string; werbetext?: string; }; export type Post = { $id: string; brand_id: string; folder_id?: string; titel?: string; user_prompt?: string; slots?: string; format?: string; bild_count?: number; status?: 'entwurf' | 'generiert' | 'fehler'; sichtbarkeit?: 'privat' | 'oeffentlich'; nische?: string; }; export const FORMATE = [ { wert: '1:1', titel: 'Quadrat', erklaerung: 'Feed-Beiträge, Produktkacheln.' }, { wert: '4:5', titel: 'Hochformat', erklaerung: 'Nimmt im Feed mehr Fläche ein.' }, { wert: '9:16', titel: 'Story', erklaerung: 'Bildschirmfüllend, für Stories und Reels.' }, ] as const; export type Format = (typeof FORMATE)[number]['wert']; export async function postsImOrdner(folderId: string): Promise { const res = await tables.listRows({ databaseId, tableId: 'posts', queries: [Query.equal('folder_id', folderId), Query.orderDesc('$createdAt'), Query.limit(100)], }); return res.rows as unknown as Post[]; } export async function postsDerMarke(brandId: string): Promise { const res = await tables.listRows({ databaseId, tableId: 'posts', queries: [Query.equal('brand_id', brandId), Query.orderDesc('$createdAt'), Query.limit(100)], }); return res.rows as unknown as Post[]; } /** * Legt das Rezept an und reiht die Generierungen ein. * * Der Post entsteht **sofort** mit `status: 'entwurf'`, bevor irgendetwas * generiert wird. Das ist Absicht: Generierung ist asynchron, und wer während * des Wartens weg navigiert, muss das Ergebnis wiederfinden (app-aufbau.md * §2.1). Ohne die Zeile gäbe es nichts, wohin man zurückkehren könnte. * * Je Bild der Kette eine `jobs`-Zeile. Abgearbeitet werden sie vom * Job-Dispatcher mit Server-Key – **nicht** vom Client, denn dafür müsste der * Anbieter-Schlüssel ins Bundle. */ export async function postAnlegen( brandId: string, teamId: string, daten: { folderId: string; titel: string; prompt: string; format: Format; bildCount: number; slots: Slots; nische?: string; }, ): Promise { const rechte = teamRechte(teamId); const post = (await tables.createRow({ databaseId, tableId: 'posts', rowId: ID.unique(), data: { brand_id: brandId, folder_id: daten.folderId, titel: daten.titel, user_prompt: daten.prompt, slots: JSON.stringify(daten.slots), format: daten.format, bild_count: daten.bildCount, status: 'entwurf', sichtbarkeit: 'privat', // Veröffentlichen ist ein aktiver, eigener Schritt kopien_count: 0, feed_score: 0, ...(daten.nische ? { nische: daten.nische } : {}), }, permissions: rechte, })) as unknown as Post; for (let i = 0; i < daten.bildCount; i++) { await tables.createRow({ databaseId, tableId: 'jobs', rowId: ID.unique(), data: { brand_id: brandId, typ: 'bild_gen', prompt_template_key: 'P7', status: 'wartend', refs: JSON.stringify({ post_id: post.$id, position: i + 1 }), }, permissions: rechte, }); } return post; } export type PostBild = { $id: string; post_id: string; position: number; typ: 'motiv' | 'text_overlay'; storage_file_id?: string; }; /** * Die Bilderketten mehrerer Posts in einer Abfrage – nicht eine je Post. * Ein Ordner mit 20 Posts wären sonst 20 Rundreisen für eine Listenansicht. */ export async function bilderZuPosts(postIds: string[]): Promise> { const nach = new Map(); if (!postIds.length) return nach; const res = await tables.listRows({ databaseId, tableId: 'post_images', queries: [Query.equal('post_id', postIds), Query.orderAsc('position'), Query.limit(200)], }); for (const b of res.rows as unknown as PostBild[]) { const liste = nach.get(b.post_id) ?? []; liste.push(b); nach.set(b.post_id, liste); } return nach; } export async function postLoeschen(id: string): Promise { await tables.deleteRow({ databaseId, tableId: 'posts', rowId: id }); } export function slotsLesen(post: Post): Slots { try { return post.slots ? (JSON.parse(post.slots) as Slots) : {}; } catch { return {}; } }