mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Seafarer and vessel registration were filtered out of the operations step by `requiresOperatorMode !== false`, so someone registering as a seafarer or vessel owner landed on an onboarding screen that did not describe them. Both now appear, grouped apart from the company modes: declaring "I am a seafarer" is a different kind of statement from "my company forwards freight". The designer's preview was gated on the Handlebars source alone, which a canvas layout does not have until the server compiles it on save -- so the button was dead for exactly the designs the canvas exists for. Editing looked broken rather than deliberately read-only: every seeded template is PUBLISHED, and a published design is immutable because certificates were issued from it. Says so, and offers the new-version action that is the way forward. Reviewing officers saw company, capital and staff tabs on seafarer certificate applications, because PRESENTATION is keyed by the generic licence keys and the fifty-odd rank-specific CoC/CoP keys fell through to the company default. Matched by prefix instead, so a certificate configured tomorrow gets the right presentation without a code change. Seaman book and BTC are wired to the newly seeded licence types and no longer marked "soon". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
ColorInput,
|
|
Group,
|
|
NumberInput,
|
|
Paper,
|
|
SegmentedControl,
|
|
Select,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
import { IconTrash } from '@tabler/icons-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { TemplateFieldPlacement, TemplateVariable } from '@ema-platform/api';
|
|
|
|
interface Props {
|
|
block: TemplateFieldPlacement | null;
|
|
variables: TemplateVariable[];
|
|
onChange: (block: TemplateFieldPlacement) => void;
|
|
onDelete: (id: string) => void;
|
|
disabled: boolean;
|
|
}
|
|
|
|
/** Everything about the selected block that is not its position on the page. */
|
|
export function BlockPropertiesPanel({
|
|
block,
|
|
variables,
|
|
onChange,
|
|
onDelete,
|
|
disabled,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
|
|
if (!block) {
|
|
return (
|
|
<Paper withBorder p="md" radius="md">
|
|
<Text size="sm" c="dimmed">
|
|
{t('designer.noBlockSelected', 'Select a block on the page to edit it.')}
|
|
</Text>
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
const isLiteral = block.variable === null;
|
|
|
|
return (
|
|
<Paper withBorder p="md" radius="md">
|
|
<Stack gap="sm">
|
|
<Group justify="space-between" align="center">
|
|
<Text fw={600} size="sm">
|
|
{t('designer.blockProperties', 'Selected block')}
|
|
</Text>
|
|
<Tooltip label={t('designer.deleteBlock', 'Remove block')}>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
color="red"
|
|
disabled={disabled}
|
|
onClick={() => onDelete(block.id)}
|
|
>
|
|
<IconTrash size={16} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
|
|
<SegmentedControl
|
|
fullWidth
|
|
size="xs"
|
|
value={isLiteral ? 'text' : 'variable'}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
onChange(
|
|
value === 'text'
|
|
? { ...block, variable: null, text: block.text ?? '' }
|
|
: { ...block, variable: variables[0]?.key ?? 'companyName' },
|
|
)
|
|
}
|
|
data={[
|
|
{ value: 'variable', label: t('designer.blockVariable', 'Variable') },
|
|
{ value: 'text', label: t('designer.blockText', 'Fixed text') },
|
|
]}
|
|
/>
|
|
|
|
{isLiteral ? (
|
|
<TextInput
|
|
label={t('designer.blockTextLabel', 'Text')}
|
|
value={block.text ?? ''}
|
|
onChange={(e) => onChange({ ...block, text: e.currentTarget.value })}
|
|
disabled={disabled}
|
|
/>
|
|
) : (
|
|
<Select
|
|
label={t('designer.blockVariableLabel', 'Variable')}
|
|
data={variables.map((variable) => ({
|
|
value: variable.key,
|
|
label: variable.label,
|
|
}))}
|
|
value={block.variable}
|
|
onChange={(value) => onChange({ ...block, variable: value })}
|
|
searchable
|
|
disabled={disabled}
|
|
/>
|
|
)}
|
|
|
|
<Group gap="xs" grow>
|
|
<NumberInput
|
|
label={t('designer.blockFontSize', 'Font size')}
|
|
value={block.fontSize ?? 14}
|
|
onChange={(value) => onChange({ ...block, fontSize: Number(value) || 14 })}
|
|
min={4}
|
|
max={200}
|
|
disabled={disabled}
|
|
/>
|
|
<NumberInput
|
|
label={t('designer.blockWidth', 'Width (%)')}
|
|
value={block.widthPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, widthPct: Math.min(100, Math.max(1, Number(value) || 1)) })
|
|
}
|
|
min={1}
|
|
max={100}
|
|
disabled={disabled}
|
|
/>
|
|
</Group>
|
|
|
|
<Group gap="xs" grow>
|
|
<NumberInput
|
|
label={t('designer.blockX', 'X (%)')}
|
|
value={block.xPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, xPct: Math.min(100, Math.max(0, Number(value) || 0)) })
|
|
}
|
|
min={0}
|
|
max={100}
|
|
decimalScale={2}
|
|
disabled={disabled}
|
|
/>
|
|
<NumberInput
|
|
label={t('designer.blockY', 'Y (%)')}
|
|
value={block.yPct}
|
|
onChange={(value) =>
|
|
onChange({ ...block, yPct: Math.min(100, Math.max(0, Number(value) || 0)) })
|
|
}
|
|
min={0}
|
|
max={100}
|
|
decimalScale={2}
|
|
disabled={disabled}
|
|
/>
|
|
</Group>
|
|
|
|
<SegmentedControl
|
|
fullWidth
|
|
size="xs"
|
|
value={block.align ?? 'left'}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
onChange({ ...block, align: value as TemplateFieldPlacement['align'] })
|
|
}
|
|
data={[
|
|
{ value: 'left', label: t('designer.alignLeft', 'Left') },
|
|
{ value: 'center', label: t('designer.alignCenter', 'Centre') },
|
|
{ value: 'right', label: t('designer.alignRight', 'Right') },
|
|
]}
|
|
/>
|
|
|
|
<SegmentedControl
|
|
fullWidth
|
|
size="xs"
|
|
value={block.fontWeight ?? 'normal'}
|
|
disabled={disabled}
|
|
onChange={(value) =>
|
|
onChange({ ...block, fontWeight: value as TemplateFieldPlacement['fontWeight'] })
|
|
}
|
|
data={[
|
|
{ value: 'normal', label: t('designer.weightNormal', 'Normal') },
|
|
{ value: 'bold', label: t('designer.weightBold', 'Bold') },
|
|
]}
|
|
/>
|
|
|
|
<ColorInput
|
|
label={t('designer.blockColor', 'Colour')}
|
|
value={block.color ?? '#111111'}
|
|
onChange={(value) => onChange({ ...block, color: value })}
|
|
disabled={disabled}
|
|
format="hex"
|
|
/>
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|