mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 18:53:06 +00:00
- Replace kind enum with free-form tags (text[] hasMany) - Remove origin and deadline fields - Status: remove rascunho/planejado, keep confirmado/realizado/cancelado - startAt required for user-facing creates (optional for tour drafts) - Add deputyPresent time gate (only coordinator/candidate can reschedule) - UI: calendar-like form with tag input, no origin/deadline, no draft status - E13 giros: tourStopRoleActivityKind → tourStopRoleActivityTags - Migration: kind→tags with label mapping, status remap, enum recreation - Activity texts table creation (was missing from migration history) Closes #389
275 lines
7.3 KiB
TypeScript
275 lines
7.3 KiB
TypeScript
/**
|
|
* Activity list omnibox adapter (B128 + B138). Pure / client-safe.
|
|
* Window presets and text search live inside the omnibox.
|
|
*/
|
|
import {
|
|
createOmniboxSuggestionSeed,
|
|
filterOmniboxSuggestionSeeds,
|
|
type CampaignListOmniboxChip,
|
|
type CampaignListOmniboxSuggestion,
|
|
} from '@/lib/campaignListOmnibox'
|
|
import { isContactSearchQueryReady, normalizeContactSearchQuery } from '@/lib/contactSearchQuery'
|
|
import { activityStatusLabels } from '@/lib/schemas/activity'
|
|
import {
|
|
activityTabLabels,
|
|
activityTabs,
|
|
buildActivityListSearchParams,
|
|
parseActivityListParams,
|
|
type ActivityListState,
|
|
type ActivityTab,
|
|
} from '@/utilities/activityUi'
|
|
|
|
export type ActivityFilterOption = {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
export type ActivityOmniboxAction =
|
|
| { kind: 'url'; state: ActivityListState }
|
|
| { kind: 'clear'; state: ActivityListState }
|
|
|
|
const chipLabel = (dimension: string, value: string): string => `${dimension}: ${value}`
|
|
|
|
const withPageReset = (state: ActivityListState): ActivityListState => ({ ...state, page: 1 })
|
|
|
|
const parseStateFromParams = (
|
|
raw: Record<string, string | string[] | undefined>,
|
|
): ActivityListState => parseActivityListParams(raw)
|
|
|
|
const setExclusiveField = (
|
|
state: ActivityListState,
|
|
field: 'tag' | 'status' | 'municipality',
|
|
value: string | undefined,
|
|
): ActivityListState => {
|
|
const params = buildActivityListSearchParams(withPageReset(state))
|
|
const raw: Record<string, string | string[] | undefined> = Object.fromEntries(params.entries())
|
|
|
|
if (field === 'tag') {
|
|
raw.tag = value
|
|
} else if (field === 'status') {
|
|
raw.status = value
|
|
} else {
|
|
raw.municipality = value
|
|
}
|
|
|
|
return parseStateFromParams(raw)
|
|
}
|
|
|
|
const tabKeywords: Record<ActivityTab, string[]> = {
|
|
proximos: ['proximos', 'próximos', 'proximo', 'próximo'],
|
|
todos: ['todos', 'todas'],
|
|
realizados: ['realizados', 'realizado'],
|
|
}
|
|
|
|
const setTab = (state: ActivityListState, tab: ActivityTab): ActivityListState => {
|
|
const params = buildActivityListSearchParams(withPageReset({ ...state, tab }))
|
|
const raw: Record<string, string | string[] | undefined> = Object.fromEntries(params.entries())
|
|
|
|
if (tab !== 'todos') {
|
|
delete raw.status
|
|
}
|
|
|
|
return parseStateFromParams(raw)
|
|
}
|
|
|
|
export const buildActivityOmniboxChips = ({
|
|
state,
|
|
municipalityLabelsById,
|
|
}: {
|
|
state: ActivityListState
|
|
municipalityLabelsById: ReadonlyMap<number, string>
|
|
}): CampaignListOmniboxChip[] => {
|
|
const chips: CampaignListOmniboxChip[] = []
|
|
|
|
if (state.q) chips.push({ id: 'q', label: chipLabel('Busca', state.q) })
|
|
|
|
if (state.tab !== 'proximos') {
|
|
chips.push({
|
|
id: `tab:${state.tab}`,
|
|
label: activityTabLabels[state.tab],
|
|
})
|
|
}
|
|
|
|
if (state.tag) {
|
|
chips.push({
|
|
id: `tag:${state.tag}`,
|
|
label: chipLabel('Tag', state.tag),
|
|
})
|
|
}
|
|
|
|
if (state.tab === 'todos' && state.status) {
|
|
chips.push({
|
|
id: `status:${state.status}`,
|
|
label: chipLabel('Status', activityStatusLabels[state.status]),
|
|
})
|
|
}
|
|
|
|
if (state.municipality) {
|
|
chips.push({
|
|
id: `municipality:${state.municipality}`,
|
|
label: chipLabel(
|
|
'Município',
|
|
municipalityLabelsById.get(state.municipality) ?? `Município #${state.municipality}`,
|
|
),
|
|
})
|
|
}
|
|
|
|
return chips
|
|
}
|
|
|
|
export const buildActivityOmniboxSuggestionSeeds = ({
|
|
tab,
|
|
municipalityOptions,
|
|
knownTags,
|
|
}: {
|
|
tab: ActivityTab
|
|
municipalityOptions: readonly ActivityFilterOption[]
|
|
knownTags?: readonly string[]
|
|
}) => {
|
|
const seeds = []
|
|
|
|
for (const tabValue of activityTabs) {
|
|
seeds.push(
|
|
createOmniboxSuggestionSeed(
|
|
{
|
|
id: `tab:${tabValue}`,
|
|
group: 'Janela',
|
|
label: activityTabLabels[tabValue],
|
|
keywords: ['janela', ...tabKeywords[tabValue]],
|
|
},
|
|
{ emptyQueryVisible: true },
|
|
),
|
|
)
|
|
}
|
|
|
|
for (const tag of knownTags ?? []) {
|
|
seeds.push(
|
|
createOmniboxSuggestionSeed(
|
|
{
|
|
id: `tag:${tag}`,
|
|
group: 'Tag',
|
|
label: tag,
|
|
keywords: ['tag', 'tipo'],
|
|
},
|
|
{ emptyQueryVisible: true },
|
|
),
|
|
)
|
|
}
|
|
|
|
if (tab === 'todos') {
|
|
for (const [value, label] of Object.entries(activityStatusLabels)) {
|
|
seeds.push(
|
|
createOmniboxSuggestionSeed(
|
|
{
|
|
id: `status:${value}`,
|
|
group: 'Status',
|
|
label,
|
|
keywords: ['status'],
|
|
},
|
|
{ emptyQueryVisible: true },
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
for (const option of municipalityOptions) {
|
|
seeds.push(
|
|
createOmniboxSuggestionSeed({
|
|
id: `municipality:${option.value}`,
|
|
group: 'Município',
|
|
label: option.label,
|
|
keywords: ['municipio'],
|
|
}),
|
|
)
|
|
}
|
|
|
|
return seeds
|
|
}
|
|
|
|
export const filterActivityOmniboxSuggestions = (
|
|
seeds: ReturnType<typeof buildActivityOmniboxSuggestionSeeds>,
|
|
query: string,
|
|
): CampaignListOmniboxSuggestion[] => filterOmniboxSuggestionSeeds(seeds, query)
|
|
|
|
export const applyActivityOmniboxSuggestion = ({
|
|
state,
|
|
suggestionId,
|
|
}: {
|
|
state: ActivityListState
|
|
suggestionId: string
|
|
}): ActivityOmniboxAction => {
|
|
if (suggestionId.startsWith('q:')) {
|
|
const { trimmed } = normalizeContactSearchQuery(suggestionId.slice(2))
|
|
const q = isContactSearchQueryReady(trimmed) ? trimmed : undefined
|
|
return { kind: 'url', state: withPageReset({ ...state, q }) }
|
|
}
|
|
|
|
if (suggestionId.startsWith('tab:')) {
|
|
const value = suggestionId.slice(4) as ActivityTab
|
|
if (!activityTabs.includes(value)) return { kind: 'url', state }
|
|
const next = state.tab === value ? setTab(state, 'proximos') : setTab(state, value)
|
|
return { kind: 'url', state: next }
|
|
}
|
|
|
|
if (suggestionId.startsWith('tag:')) {
|
|
const value = suggestionId.slice(4)
|
|
const next =
|
|
state.tag === value
|
|
? setExclusiveField(state, 'tag', undefined)
|
|
: setExclusiveField(state, 'tag', value)
|
|
return { kind: 'url', state: next }
|
|
}
|
|
|
|
if (suggestionId.startsWith('status:')) {
|
|
const value = suggestionId.slice(7)
|
|
const next =
|
|
state.status === value
|
|
? setExclusiveField(state, 'status', undefined)
|
|
: setExclusiveField(state, 'status', value)
|
|
return { kind: 'url', state: next }
|
|
}
|
|
|
|
if (suggestionId.startsWith('municipality:')) {
|
|
const value = suggestionId.slice(13)
|
|
const next =
|
|
state.municipality === Number(value)
|
|
? setExclusiveField(state, 'municipality', undefined)
|
|
: setExclusiveField(state, 'municipality', value)
|
|
return { kind: 'url', state: next }
|
|
}
|
|
|
|
return { kind: 'url', state }
|
|
}
|
|
|
|
export const removeActivityOmniboxChip = ({
|
|
state,
|
|
chipId,
|
|
}: {
|
|
state: ActivityListState
|
|
chipId: string
|
|
}): ActivityOmniboxAction => {
|
|
if (chipId === 'q') return { kind: 'url', state: withPageReset({ ...state, q: undefined }) }
|
|
|
|
if (chipId.startsWith('tab:')) {
|
|
return { kind: 'url', state: setTab(state, 'proximos') }
|
|
}
|
|
|
|
if (chipId.startsWith('tag:')) {
|
|
return { kind: 'url', state: setExclusiveField(state, 'tag', undefined) }
|
|
}
|
|
|
|
if (chipId.startsWith('status:')) {
|
|
return { kind: 'url', state: setExclusiveField(state, 'status', undefined) }
|
|
}
|
|
|
|
if (chipId.startsWith('municipality:')) {
|
|
return { kind: 'url', state: setExclusiveField(state, 'municipality', undefined) }
|
|
}
|
|
|
|
return { kind: 'url', state }
|
|
}
|
|
|
|
export const clearActivityOmnibox = (state: ActivityListState): ActivityOmniboxAction => ({
|
|
kind: 'clear',
|
|
state: withPageReset({ page: 1, tab: state.tab }),
|
|
})
|