From 0bff6771ce083cff6024428d569c43df46ebca95 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 25 Apr 2026 13:46:14 +0200 Subject: [PATCH] feat(admin): filter markets by missing fields + row indicators - Add missing= query param (description/image/website/location) to AdminSearchParams; both AdminSearch and AdminSearchGrouped apply the SQL condition - Add has_description/has_image/has_website/has_location booleans to AdminMarketSummary, populated in ToAdminSummary from existing Market fields - Dropdown filter in the admin market list routes to the missing param - Coloured dot indicators per row (amber=image, orange=desc, red=website, purple=location) with title tooltips --- backend/internal/domain/market/dto.go | 65 +++++++++++--------- backend/internal/domain/market/repository.go | 22 +++++++ web/src/lib/api/types.ts | 4 ++ web/src/routes/admin/maerkte/+page.server.ts | 5 +- web/src/routes/admin/maerkte/+page.svelte | 40 ++++++++++++ 5 files changed, 106 insertions(+), 30 deletions(-) diff --git a/backend/internal/domain/market/dto.go b/backend/internal/domain/market/dto.go index 4c31269..ef8dd8a 100644 --- a/backend/internal/domain/market/dto.go +++ b/backend/internal/domain/market/dto.go @@ -164,8 +164,9 @@ func ToDetail(m Market) MarketDetail { type AdminSearchParams struct { Status string `form:"status"` Query string `form:"q"` - Sort string `form:"sort"` // name, city, date, created, status - Order string `form:"order"` // asc, desc + Missing string `form:"missing"` // description, image, website, location + Sort string `form:"sort"` // name, city, date, created, status + Order string `form:"order"` // asc, desc Page int `form:"page"` PerPage int `form:"per_page"` } @@ -187,19 +188,23 @@ func (p *AdminSearchParams) Offset() int { } type AdminMarketSummary struct { - ID uuid.UUID `json:"id"` - SeriesID uuid.UUID `json:"series_id"` - Year int `json:"year"` - Slug string `json:"slug"` - Name string `json:"name"` - City string `json:"city"` - State string `json:"state"` - Status string `json:"status"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - OrganizerName string `json:"organizer_name"` - SubmitterName string `json:"submitter_name"` - CreatedAt string `json:"created_at"` + ID uuid.UUID `json:"id"` + SeriesID uuid.UUID `json:"series_id"` + Year int `json:"year"` + Slug string `json:"slug"` + Name string `json:"name"` + City string `json:"city"` + State string `json:"state"` + Status string `json:"status"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + OrganizerName string `json:"organizer_name"` + SubmitterName string `json:"submitter_name"` + CreatedAt string `json:"created_at"` + HasDescription bool `json:"has_description"` + HasImage bool `json:"has_image"` + HasWebsite bool `json:"has_website"` + HasLocation bool `json:"has_location"` } type AdminMarketDetail struct { @@ -259,19 +264,23 @@ type AdminDetailResponse struct { func ToAdminSummary(m Market) AdminMarketSummary { return AdminMarketSummary{ - ID: m.ID, - SeriesID: m.SeriesID, - Year: m.Year, - Slug: m.Slug, - Name: m.Name, - City: m.City, - State: m.State, - Status: m.Status, - StartDate: m.StartDate.Format(time.DateOnly), - EndDate: m.EndDate.Format(time.DateOnly), - OrganizerName: m.OrganizerName, - SubmitterName: m.SubmitterName, - CreatedAt: m.CreatedAt.Format(time.RFC3339), + ID: m.ID, + SeriesID: m.SeriesID, + Year: m.Year, + Slug: m.Slug, + Name: m.Name, + City: m.City, + State: m.State, + Status: m.Status, + StartDate: m.StartDate.Format(time.DateOnly), + EndDate: m.EndDate.Format(time.DateOnly), + OrganizerName: m.OrganizerName, + SubmitterName: m.SubmitterName, + CreatedAt: m.CreatedAt.Format(time.RFC3339), + HasDescription: m.Description != "", + HasImage: m.ImageURL != "", + HasWebsite: m.Website != "", + HasLocation: m.Latitude != nil, } } diff --git a/backend/internal/domain/market/repository.go b/backend/internal/domain/market/repository.go index 215fe04..ffc2eb4 100644 --- a/backend/internal/domain/market/repository.go +++ b/backend/internal/domain/market/repository.go @@ -618,6 +618,17 @@ func (r *pgRepository) AdminSearch(ctx context.Context, params AdminSearchParams args = append(args, params.Query, "%"+params.Query+"%") } + switch params.Missing { + case "description": + conditions = append(conditions, "(e.description IS NULL OR e.description = '')") + case "image": + conditions = append(conditions, "(e.image_url IS NULL OR e.image_url = '')") + case "website": + conditions = append(conditions, "(e.website IS NULL OR e.website = '')") + case "location": + conditions = append(conditions, "e.location IS NULL") + } + where := "" if len(conditions) > 0 { where = "WHERE " + strings.Join(conditions, " AND ") @@ -705,6 +716,17 @@ func (r *pgRepository) AdminSearchGrouped(ctx context.Context, params AdminSearc args = append(args, params.Query, "%"+params.Query+"%") } + switch params.Missing { + case "description": + conditions = append(conditions, "(e.description IS NULL OR e.description = '')") + case "image": + conditions = append(conditions, "(e.image_url IS NULL OR e.image_url = '')") + case "website": + conditions = append(conditions, "(e.website IS NULL OR e.website = '')") + case "location": + conditions = append(conditions, "e.location IS NULL") + } + where := "" if len(conditions) > 0 { where = "WHERE " + strings.Join(conditions, " AND ") diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 32ab8b6..b0f4765 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -132,6 +132,10 @@ export interface AdminMarketSummary { organizer_name: string; submitter_name: string; created_at: string; + has_description: boolean; + has_image: boolean; + has_website: boolean; + has_location: boolean; } export interface AdminMarketDetail { diff --git a/web/src/routes/admin/maerkte/+page.server.ts b/web/src/routes/admin/maerkte/+page.server.ts index bc75802..017b454 100644 --- a/web/src/routes/admin/maerkte/+page.server.ts +++ b/web/src/routes/admin/maerkte/+page.server.ts @@ -8,11 +8,12 @@ import type { PageServerLoad } from './$types.js'; export const load: PageServerLoad = async ({ url, cookies }) => { const status = url.searchParams.get('status') ?? ''; const q = url.searchParams.get('q') ?? ''; + const missing = url.searchParams.get('missing') ?? ''; const page = url.searchParams.get('page') ?? '1'; const sort = url.searchParams.get('sort') ?? ''; const order = url.searchParams.get('order') ?? ''; - const query = buildSearchQuery({ status, q, page, per_page: '20', sort, order }); + const query = buildSearchQuery({ status, q, missing, page, per_page: '20', sort, order }); try { const res = await serverFetch(`/admin/markets/grouped?${query}`, cookies); @@ -20,7 +21,7 @@ export const load: PageServerLoad = async ({ url, cookies }) => { return { groups: res.data, meta: res.meta as PaginationMeta, - filters: { status, q, sort, order } + filters: { status, q, missing, sort, order } }; } catch (err) { if (err instanceof ApiClientError) { diff --git a/web/src/routes/admin/maerkte/+page.svelte b/web/src/routes/admin/maerkte/+page.svelte index e3ae5f4..2138560 100644 --- a/web/src/routes/admin/maerkte/+page.svelte +++ b/web/src/routes/admin/maerkte/+page.svelte @@ -10,6 +10,14 @@ let searchValue = $state(untrack(() => data.filters.q)); let expandedSeries = $state(new Set()); + const missingOptions: { label: string; value: string }[] = [ + { label: 'Alle', value: '' }, + { label: 'Kein Bild', value: 'image' }, + { label: 'Keine Beschreibung', value: 'description' }, + { label: 'Keine Website', value: 'website' }, + { label: 'Kein Standort', value: 'location' } + ]; + const statusLabels: Record = { rumored: 'Ausstehend', confirmed: 'BestÃĪtigt', @@ -39,6 +47,7 @@ const currentStatus = $derived(page.url.searchParams.get('status') ?? ''); const currentQ = $derived(page.url.searchParams.get('q') ?? ''); + const currentMissing = $derived(page.url.searchParams.get('missing') ?? ''); const currentSort = $derived(page.url.searchParams.get('sort') ?? ''); const currentOrder = $derived(page.url.searchParams.get('order') ?? ''); @@ -52,12 +61,14 @@ const params = new URLSearchParams(); const status = overrides.status ?? currentStatus; const q = overrides.q ?? currentQ; + const missing = overrides.missing ?? currentMissing; const sort = overrides.sort ?? currentSort; const order = overrides.order ?? currentOrder; const pg = overrides.page ?? '1'; if (status) params.set('status', status); if (q) params.set('q', q); + if (missing) params.set('missing', missing); if (sort) params.set('sort', sort); if (order) params.set('order', order); if (pg !== '1') params.set('page', pg); @@ -127,6 +138,17 @@ {/if} + + @@ -216,6 +238,24 @@ ({group.editions.length} Ausgaben) {/if} + + {#if !latest.has_image}{/if} + {#if !latest.has_description}{/if} + {#if !latest.has_website}{/if} + {#if !latest.has_location}{/if} + {latest.city}