--- import { Icon } from 'astro-icon/components' import { Markdown } from 'astro-remote' import { actions, isInputError } from 'astro:actions' import { Code } from 'astro:components' import { orderBy } from 'lodash-es' import BadgeSmall from '../../../../components/BadgeSmall.astro' import Button from '../../../../components/Button.astro' import FormSection from '../../../../components/FormSection.astro' import FormSubSection from '../../../../components/FormSubSection.astro' import InputCardGroup from '../../../../components/InputCardGroup.astro' import InputCheckbox from '../../../../components/InputCheckbox.astro' import InputCheckboxGroup from '../../../../components/InputCheckboxGroup.astro' import InputImageFile from '../../../../components/InputImageFile.astro' import InputSelect from '../../../../components/InputSelect.astro' import InputSubmitButton from '../../../../components/InputSubmitButton.astro' import InputText from '../../../../components/InputText.astro' import InputTextArea from '../../../../components/InputTextArea.astro' import MyPicture from '../../../../components/MyPicture.astro' import ServiceCard from '../../../../components/ServiceCard.astro' import TimeFormatted from '../../../../components/TimeFormatted.astro' import Tooltip from '../../../../components/Tooltip.astro' import UserBadge from '../../../../components/UserBadge.astro' import { getAttributeCategoryInfo } from '../../../../constants/attributeCategories' import { getAttributeTypeInfo } from '../../../../constants/attributeTypes' import { contactMethodUrlTypes, formatContactMethod } from '../../../../constants/contactMethods' import { currencies } from '../../../../constants/currencies' import { eventTypes, getEventTypeInfo } from '../../../../constants/eventTypes' import { kycLevelClarifications } from '../../../../constants/kycLevelClarifications' import { kycLevels } from '../../../../constants/kycLevels' import { serviceVisibilities } from '../../../../constants/serviceVisibility' import { verificationStatuses } from '../../../../constants/verificationStatus' import { getVerificationStepStatusInfo, verificationStepStatuses, } from '../../../../constants/verificationStepStatus' import BaseLayout from '../../../../layouts/BaseLayout.astro' import { DEPLOYMENT_MODE } from '../../../../lib/client/envVariables' import { listFiles } from '../../../../lib/fileStorage' import { makeAdminApiCallInfo } from '../../../../lib/makeAdminApiCallInfo' import { pluralize } from '../../../../lib/pluralize' import { prisma } from '../../../../lib/prisma' const { slug } = Astro.params if (!slug) return Astro.rewrite('/404') const serviceResult = Astro.getActionResult(actions.admin.service.update) Astro.locals.banners.addIfSuccess(serviceResult, 'Service updated successfully') const serviceInputErrors = isInputError(serviceResult?.error) ? serviceResult.error.fields : {} if (serviceResult && !serviceResult.error && slug !== serviceResult.data.service.slug) { return Astro.redirect(`/admin/services/${serviceResult.data.service.slug}/edit`) } const eventCreateResult = Astro.getActionResult(actions.admin.event.create) Astro.locals.banners.addIfSuccess(eventCreateResult, 'Event created successfully') const eventInputErrors = isInputError(eventCreateResult?.error) ? eventCreateResult.error.fields : {} const eventUpdateResult = Astro.getActionResult(actions.admin.event.update) Astro.locals.banners.addIfSuccess(eventUpdateResult, 'Event updated successfully') const eventUpdateInputErrors = isInputError(eventUpdateResult?.error) ? eventUpdateResult.error.fields : {} const eventToggleResult = Astro.getActionResult(actions.admin.event.toggle) Astro.locals.banners.addIfSuccess(eventToggleResult, 'Event visibility updated successfully') const eventDeleteResult = Astro.getActionResult(actions.admin.event.delete) Astro.locals.banners.addIfSuccess(eventDeleteResult, 'Event deleted successfully') const verificationStepCreateResult = Astro.getActionResult(actions.admin.verificationStep.create) Astro.locals.banners.addIfSuccess(verificationStepCreateResult, 'Verification step added successfully') const verificationStepInputErrors = isInputError(verificationStepCreateResult?.error) ? verificationStepCreateResult.error.fields : {} const verificationStepUpdateResult = Astro.getActionResult(actions.admin.verificationStep.update) Astro.locals.banners.addIfSuccess(verificationStepUpdateResult, 'Verification step updated successfully') const verificationStepUpdateInputErrors = isInputError(verificationStepUpdateResult?.error) ? verificationStepUpdateResult.error.fields : {} const verificationStepDeleteResult = Astro.getActionResult(actions.admin.verificationStep.delete) Astro.locals.banners.addIfSuccess(verificationStepDeleteResult, 'Verification step deleted successfully') const internalNoteCreateResult = Astro.getActionResult(actions.admin.service.internalNote.add) Astro.locals.banners.addIfSuccess(internalNoteCreateResult, 'Internal note added successfully') const internalNoteInputErrors = isInputError(internalNoteCreateResult?.error) ? internalNoteCreateResult.error.fields : {} const contactMethodUpdateResult = Astro.getActionResult(actions.admin.service.contactMethod.update) Astro.locals.banners.addIfSuccess(contactMethodUpdateResult, 'Contact method updated successfully') const contactMethodUpdateInputErrors = isInputError(contactMethodUpdateResult?.error) ? contactMethodUpdateResult.error.fields : {} const contactMethodAddResult = Astro.getActionResult(actions.admin.service.contactMethod.add) Astro.locals.banners.addIfSuccess(contactMethodAddResult, 'Contact method added successfully') const contactMethodAddInputErrors = isInputError(contactMethodAddResult?.error) ? contactMethodAddResult.error.fields : {} const internalNoteDeleteResult = Astro.getActionResult(actions.admin.service.internalNote.delete) Astro.locals.banners.addIfSuccess(internalNoteDeleteResult, 'Internal note deleted successfully') const evidenceImageAddResult = Astro.getActionResult(actions.admin.service.evidenceImage.add) if (evidenceImageAddResult?.data?.imageUrl) { Astro.locals.banners.add({ uiMessage: 'Evidence image added successfully', type: 'success', origin: 'action', }) } const evidenceImageAddInputErrors = isInputError(evidenceImageAddResult?.error) ? evidenceImageAddResult.error.fields : {} const evidenceImageDeleteResult = Astro.getActionResult(actions.admin.service.evidenceImage.delete) Astro.locals.banners.addIfSuccess(evidenceImageDeleteResult, 'Evidence image deleted successfully') const [service, categories, attributes] = await Astro.locals.banners.tryMany([ [ 'Error fetching service', () => prisma.service.findUnique({ where: { slug }, include: { attributes: { select: { attribute: { select: { id: true, }, }, }, }, categories: { select: { id: true, name: true, icon: true, }, }, events: { orderBy: { startedAt: 'desc', }, }, verificationRequests: { select: { id: true, user: { select: { name: true, displayName: true, picture: true, }, }, createdAt: true, }, orderBy: { createdAt: 'desc', }, }, verificationSteps: { orderBy: { createdAt: 'desc', }, }, contactMethods: { orderBy: { label: 'asc', }, }, internalNotes: { include: { addedByUser: { select: { name: true, displayName: true, picture: true, }, }, }, orderBy: { createdAt: 'desc', }, }, _count: { select: { verificationRequests: true, }, }, }, }), ], [ 'Error fetching categories', () => prisma.category.findMany({ orderBy: { name: 'asc' }, }), [] as [], ], [ 'Error fetching attributes', () => prisma.attribute.findMany({ orderBy: { category: 'asc' }, }), [] as [], ], ]) if (!service) { try { const serviceWithOldSlug = await prisma.service.findFirst({ where: { previousSlugs: { has: slug } }, select: { slug: true }, }) if (serviceWithOldSlug) { return Astro.redirect(`/admin/services/${serviceWithOldSlug.slug}/edit`, 301) } } catch (error) { console.error(error) } return Astro.rewrite('/404') } const evidenceImageUrls = await Astro.locals.banners.try( 'Error listing evidence files', () => listFiles(`evidence/${service.slug}`), [] as string[] ) const apiCalls = await Astro.locals.banners.try( 'Error fetching api calls', () => Promise.all([ makeAdminApiCallInfo({ method: 'QUERY', path: '/service/get', input: { slug: service.slug }, baseUrl: Astro.url, }), ]), [] ) ---
{ !!service.imageUrl && ( ) }

{service.name}

0 ? `Old slugs: ${service.previousSlugs.join(', ')}` : '' }`} name="slug" inputProps={{ value: service.slug, class: 'font-title', }} error={serviceInputErrors.slug} class="font-title" />
({ label: category.name, value: category.id.toString(), icon: category.icon, }))} selectedValues={service.categories.map((c) => c.id.toString())} error={serviceInputErrors.categories} class="min-w-auto" /> ({ ...attribute, categoryInfo: getAttributeCategoryInfo(attribute.category), typeInfo: getAttributeTypeInfo(attribute.type), })), ['categoryInfo.order', 'typeInfo.order'] ).map((attribute) => { return { label: attribute.title, value: attribute.id.toString(), icon: [attribute.categoryInfo.icon, attribute.typeInfo.icon], iconClassName: [attribute.categoryInfo.classNames.icon, attribute.typeInfo.classNames.icon], } })} selectedValues={service.attributes.map((a) => a.attribute.id.toString())} error={serviceInputErrors.attributes} />
({ label: `${kycLevel.name} (${kycLevel.value}/4)`, value: kycLevel.id.toString(), icon: kycLevel.icon, description: kycLevel.description, }))} selectedValue={service.kycLevel.toString()} iconSize="md" cardSize="md" error={serviceInputErrors.kycLevel} class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" /> ({ label: clarification.label, value: clarification.value, icon: clarification.icon, description: clarification.description, }))} selectedValue={service.kycLevelClarification} iconSize="sm" cardSize="sm" error={serviceInputErrors.kycLevelClarification} /> ({ label: status.label, value: status.value, icon: status.icon, iconClass: status.classNames.icon, description: status.description, }))} selectedValue={service.verificationStatus} error={serviceInputErrors.verificationStatus} cardSize="sm" iconSize="sm" class="[&>div]:grid-cols-2 [&>div]:[--card-min-size:16rem]" /> ({ label: currency.name, value: currency.id, icon: currency.icon, }))} selectedValue={service.acceptedCurrencies} error={serviceInputErrors.acceptedCurrencies} required multiple />
({ label: visibility.label, value: visibility.value, icon: visibility.icon, iconClass: visibility.iconClass, description: visibility.description, }))} selectedValue={service.serviceVisibility} error={serviceInputErrors.serviceVisibility} cardSize="sm" />
{ service.internalNotes.length === 0 ? (

No internal notes yet.

) : (
{service.internalNotes.map((note) => (
{note.addedByUser && ( by )}
))}
) }
{ service.events.length === 0 ? (

No events yet.

) : (
{service.events.map((event) => { const eventTypeInfo = getEventTypeInfo(event.type) return (
{event.title} {!event.visible && ( )}

{event.content}

Started: {new Date(event.startedAt).toLocaleDateString()} Ended: {event.endedAt ? event.endedAt === event.startedAt ? '1-time event' : new Date(event.endedAt).toLocaleDateString() : 'Ongoing'} {event.source && Source: {event.source}}
{/* Edit Event Form - Hidden by default */}
) })}
) }
({ label: type.label, value: type.id, }))} selectProps={{ required: true }} error={eventInputErrors.type} />
{ service.verificationSteps.length === 0 ? (

No verification steps yet.

) : (
{service.verificationSteps.map((step) => { const verificationStepStatusInfo = getVerificationStepStatusInfo(step.status) return (
{step.title}

{step.description}

{step.evidenceMd && (

Evidence provided (see edit form)

)}

Created: {new Date(step.createdAt).toLocaleDateString()}

{/* Edit Verification Step Form - Hidden by default */}
) })}
) }
({ label: status.label, value: status.value, }))} selectProps={{ required: true }} error={verificationStepInputErrors.status} />
{ service.verificationRequests.length > 0 ? (
{service.verificationRequests.map((request) => ( ))}
User Requested
) : (

No verification requests yet.

) }
{ service.contactMethods.length === 0 ? (

No contact methods yet.

) : (
{service.contactMethods.map((method) => { const contactMethodInfo = formatContactMethod(method.value) return (
{method.label ?? contactMethodInfo.formattedValue}

{method.value}

{/* Edit Contact Method Form - Hidden by default */}
) })}
) }
type.labelPlural).join(', ')}`} name="value" inputProps={{ required: true, placeholder: 'contact@example.com', }} error={contactMethodAddInputErrors.value} />
{ DEPLOYMENT_MODE === 'staging' && (

This endpoints section doesn't work in PRE. Use curl commands instead.

) }
{ apiCalls.map((call) => (

Input:

Output:

)) }
{ evidenceImageUrls.length === 0 ? (

No evidence images yet.

) : (
{evidenceImageUrls.map((imageUrl: string) => (
))}
) }