Mejora en los cuadros de diálogo
This commit is contained in:
parent
91988a26b9
commit
791fa06ddd
@ -285,3 +285,13 @@ contrae el menú y conservan la selección y las ayudas del botón de navegació
|
|||||||
Componentes de @repo/rdx-ui independientes de los DTO, consultas y endpoints de Metis Desk. FieldSections recibe secciones, elementos con sectionId opcional y renderItem; presenta una cuadrícula plana cuando no hay secciones, o bloques desplegables accesibles con descripción y «Sin sección». Admite presentación de valores como listas de descripción y apertura ante errores externos.
|
Componentes de @repo/rdx-ui independientes de los DTO, consultas y endpoints de Metis Desk. FieldSections recibe secciones, elementos con sectionId opcional y renderItem; presenta una cuadrícula plana cuando no hay secciones, o bloques desplegables accesibles con descripción y «Sin sección». Admite presentación de valores como listas de descripción y apertura ante errores externos.
|
||||||
|
|
||||||
TemplateSectionsEditor recibe secciones, referencias de campos, nombres, textos traducidos, generador de identidad y onChange. Permite ordenar secciones/campos, editar nombre/descripción, asignar campos y mantener su obligatoriedad mediante SwitchField. El módulo consumidor conserva el estado en React Hook Form y genera UUID v7. Los helpers removeTemplateSection y copyTemplateLayout conservan referencias y remapean identidades respectivamente. No se modifican los componentes vendor.
|
TemplateSectionsEditor recibe secciones, referencias de campos, nombres, textos traducidos, generador de identidad y onChange. Permite ordenar secciones/campos, editar nombre/descripción, asignar campos y mantener su obligatoriedad mediante SwitchField. El módulo consumidor conserva el estado en React Hook Form y genera UUID v7. Los helpers removeTemplateSection y copyTemplateLayout conservan referencias y remapean identidades respectivamente. No se modifican los componentes vendor.
|
||||||
|
|
||||||
|
## Formularios modales adaptativos
|
||||||
|
|
||||||
|
`FormDialog` admite `size="small" | "medium" | "large"`. Los formularios sencillos mantienen el tamaño pequeño; personas y usuarios emplean el intermedio. `wide` sigue siendo un alias del tamaño grande: 90 % del ancho disponible, con máximo de 1.200 px y márgenes en móvil. No se ensanchan automáticamente las altas sencillas.
|
||||||
|
|
||||||
|
La cabecera permanece visible. Un formulario modal utiliza una raíz flex con altura mínima cero, `FormBody` para campos, errores y ayuda, y `FormActions` como hermano final fuera del cuerpo desplazable. Los botones permanecen dentro del formulario HTML original: se conservan envío por teclado, validación nativa, estados pendientes y cancelación. La ayuda queda al final del cuerpo, inmediatamente antes de las acciones. Los diálogos de consulta conservan el cuerpo desplazable y su pie externo.
|
||||||
|
|
||||||
|
`FormSection` agrupa datos relacionados mediante título accesible y un borde discreto. Activos, recursos y plantillas lo utilizan para sus datos principales. Las características y las secciones del editor se distribuyen en dos bloques por fila cuando el contenedor dispone de al menos 900 px; los controles dentro de cada bloque mantienen una o dos columnas según el espacio. Una plantilla sin secciones puede aprovechar hasta tres columnas de características. Observaciones y ayuda ocupan toda la fila. La disposición responde al espacio del modal, no solo al ancho del navegador.
|
||||||
|
|
||||||
|
El desplazamiento vertical se conserva cuando el contenido supera la altura disponible; no se ocultan campos ni se reduce artificialmente su tamaño para eliminarlo. No se introducen pestañas ni páginas nuevas sin una necesidad específica del formulario.
|
||||||
|
|||||||
@ -299,5 +299,9 @@
|
|||||||
"assign": "Section",
|
"assign": "Section",
|
||||||
"help": "Organize fields without changing their values or requirements. Removing a section moves its fields to Unsectioned.",
|
"help": "Organize fields without changing their values or requirements. Removing a section moves its fields to Unsectioned.",
|
||||||
"empty": "This section has no fields yet."
|
"empty": "This section has no fields yet."
|
||||||
|
},
|
||||||
|
"formLayout": {
|
||||||
|
"identity": "Identification and assignment",
|
||||||
|
"template": "Template details"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -299,5 +299,9 @@
|
|||||||
"assign": "Sección",
|
"assign": "Sección",
|
||||||
"help": "Organiza los campos sin cambiar sus valores ni su obligatoriedad. Al eliminar una sección, sus campos pasan a «Sin sección».",
|
"help": "Organiza los campos sin cambiar sus valores ni su obligatoriedad. Al eliminar una sección, sus campos pasan a «Sin sección».",
|
||||||
"empty": "Esta sección aún no tiene campos."
|
"empty": "Esta sección aún no tiene campos."
|
||||||
|
},
|
||||||
|
"formLayout": {
|
||||||
|
"identity": "Identificación y asignación",
|
||||||
|
"template": "Datos de la plantilla"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions, FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -22,11 +22,13 @@ export function EndRelationshipDialog(props: EndRelationshipDialogProps) {
|
|||||||
open
|
open
|
||||||
title={t("endRelationship")}
|
title={t("endRelationship")}
|
||||||
>
|
>
|
||||||
<p className="text-sm">
|
<FormBody>
|
||||||
{relationship.values.name} · {sourceName} → {targetName}
|
<p className="text-sm">
|
||||||
</p>
|
{relationship.values.name} · {sourceName} → {targetName}
|
||||||
<p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p>
|
</p>
|
||||||
<InventoryFeedback error={mutation.error} />
|
<p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p>
|
||||||
|
<InventoryFeedback error={mutation.error} />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Label } from "@repo/shadcn-ui/components/label";
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
@ -34,51 +34,56 @@ export function RelationshipForm(props: RelationshipFormProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={`${id}-type`}>{t("relationshipType")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={`${id}-type`}>{t("relationshipType")}</Label>
|
||||||
action={{ label: t("newRelation"), onClick: () => setQuick(true) }}
|
<SelectWithAction
|
||||||
disabled={mutation.isPending || catalog.isPending}
|
action={{ label: t("newRelation"), onClick: () => setQuick(true) }}
|
||||||
id={`${id}-type`}
|
disabled={mutation.isPending || catalog.isPending}
|
||||||
label={t("relationshipType")}
|
id={`${id}-type`}
|
||||||
onValueChange={setTypeId}
|
label={t("relationshipType")}
|
||||||
options={relationshipTypes.map((item) => ({ value: item.id, label: item.values.name }))}
|
onValueChange={setTypeId}
|
||||||
placeholder={t("chooseRelation")}
|
options={relationshipTypes.map((item) => ({
|
||||||
required
|
value: item.id,
|
||||||
value={typeId}
|
label: item.values.name,
|
||||||
|
}))}
|
||||||
|
placeholder={t("chooseRelation")}
|
||||||
|
required
|
||||||
|
value={typeId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-target`}>{t("targetAsset")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
disabled={mutation.isPending || inventory.isPending}
|
||||||
|
id={`${id}-target`}
|
||||||
|
label={t("targetAsset")}
|
||||||
|
onValueChange={setTargetId}
|
||||||
|
options={targetAssets.map((asset) => ({ value: asset.id, label: asset.values.name }))}
|
||||||
|
placeholder={t("chooseAsset")}
|
||||||
|
required
|
||||||
|
value={targetId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-notes`}>{t("notes")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${id}-notes`}
|
||||||
|
maxLength={10000}
|
||||||
|
onChange={(event) => setNotes(event.target.value)}
|
||||||
|
value={notes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<InventoryFeedback error={mutation.error ?? catalog.error ?? inventory.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("relationshipHelp")}
|
||||||
|
examples={t("relationshipExample")}
|
||||||
|
examplesLabel={t("examplesLabel")}
|
||||||
|
guidance={t("relationshipGuidance")}
|
||||||
|
title={t("helpTitle")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</FormBody>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-target`}>{t("targetAsset")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
disabled={mutation.isPending || inventory.isPending}
|
|
||||||
id={`${id}-target`}
|
|
||||||
label={t("targetAsset")}
|
|
||||||
onValueChange={setTargetId}
|
|
||||||
options={targetAssets.map((asset) => ({ value: asset.id, label: asset.values.name }))}
|
|
||||||
placeholder={t("chooseAsset")}
|
|
||||||
required
|
|
||||||
value={targetId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-notes`}>{t("notes")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${id}-notes`}
|
|
||||||
maxLength={10000}
|
|
||||||
onChange={(event) => setNotes(event.target.value)}
|
|
||||||
value={notes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<InventoryFeedback error={mutation.error ?? catalog.error ?? inventory.error} />
|
|
||||||
<ContextHelp
|
|
||||||
description={t("relationshipHelp")}
|
|
||||||
examples={t("relationshipExample")}
|
|
||||||
examplesLabel={t("examplesLabel")}
|
|
||||||
guidance={t("relationshipGuidance")}
|
|
||||||
title={t("helpTitle")}
|
|
||||||
/>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={!(typeId && targetId) || mutation.isPending} type="submit">
|
<Button disabled={!(typeId && targetId) || mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { FormSection } from "@repo/rdx-ui/components/form-section";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -43,160 +44,169 @@ export function AssetForm(props: AssetFormProps) {
|
|||||||
const selectProps = { disabled: mutation.isPending, placeholder: t("noAssignment") };
|
const selectProps = { disabled: mutation.isPending, placeholder: t("noAssignment") };
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-5" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-5" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<FormSection title={t("formLayout.identity")}>
|
||||||
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
|
<div className="space-y-2">
|
||||||
</div>
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
asset || !allowQuickCreate
|
||||||
|
? undefined
|
||||||
|
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
|
||||||
|
}
|
||||||
|
disabled={!!asset || mutation.isPending}
|
||||||
|
id={`${prefix}-template`}
|
||||||
|
label={t("template")}
|
||||||
|
onValueChange={selectTemplate}
|
||||||
|
options={
|
||||||
|
asset
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
value: asset.values.templateId,
|
||||||
|
label: asset.specification.templateName,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: templates.map((item) => ({ value: item.id, label: item.values.name }))
|
||||||
|
}
|
||||||
|
placeholder={t("chooseValue")}
|
||||||
|
required
|
||||||
|
value={values.templateId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
id={`${prefix}-status`}
|
||||||
|
label={t("status")}
|
||||||
|
onValueChange={changeStatus}
|
||||||
|
options={AssetStatusSchema.options.map((value) => ({ value, label: t(value) }))}
|
||||||
|
placeholder={t("chooseValue")}
|
||||||
|
required
|
||||||
|
value={values.status}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
{...selectProps}
|
||||||
|
action={
|
||||||
|
allowQuickCreate
|
||||||
|
? {
|
||||||
|
label: t("newSite"),
|
||||||
|
onClick: () => lookups.onCreateSite(siteCreated),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
id={`${prefix}-site`}
|
||||||
|
label={t("site")}
|
||||||
|
onValueChange={(id) => form.setValue("siteId", id || null)}
|
||||||
|
options={lookups.sites.map((site) => ({ value: site.id, label: site.name }))}
|
||||||
|
value={values.siteId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-person`}>{t("person")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
{...selectProps}
|
||||||
|
action={
|
||||||
|
allowQuickCreate
|
||||||
|
? {
|
||||||
|
label: t("newPerson"),
|
||||||
|
onClick: () => lookups.onCreatePerson(personCreated),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
id={`${prefix}-person`}
|
||||||
|
label={t("person")}
|
||||||
|
onValueChange={(id) => form.setValue("personId", id || null)}
|
||||||
|
options={lookups.people
|
||||||
|
.filter((person) => person.active || person.id === asset?.values.personId)
|
||||||
|
.map((person) => ({
|
||||||
|
value: person.id,
|
||||||
|
label: person.name,
|
||||||
|
disabled: !person.active && person.id !== values.personId,
|
||||||
|
}))}
|
||||||
|
value={values.personId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h3 className="font-medium">{t("characteristics")}</h3>
|
||||||
|
<FieldSections
|
||||||
|
hasErrors={!!mutation.error || Object.keys(form.formState.errors).length > 0}
|
||||||
|
items={fields}
|
||||||
|
renderItem={(field) =>
|
||||||
|
field.fieldId === brandFieldId ? (
|
||||||
|
<BrandSelect
|
||||||
|
allowQuickCreate={allowQuickCreate}
|
||||||
|
brandId={values.brandId ?? null}
|
||||||
|
csrfToken={csrfToken}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
id={`${prefix}-${field.fieldId}`}
|
||||||
|
key={field.fieldId}
|
||||||
|
label={field.name}
|
||||||
|
legacyName={
|
||||||
|
typeof values.characteristics[field.fieldId] === "string"
|
||||||
|
? String(values.characteristics[field.fieldId])
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
onChange={(brand) => {
|
||||||
|
form.setValue("brandId", brand?.id ?? null, { shouldDirty: true });
|
||||||
|
setCharacteristic(field.fieldId, brand?.name);
|
||||||
|
}}
|
||||||
|
required={field.required}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AssetCharacteristicControl
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
field={field}
|
||||||
|
key={field.fieldId}
|
||||||
|
onChange={(value) => setCharacteristic(field.fieldId, value)}
|
||||||
|
prefix={prefix}
|
||||||
|
value={values.characteristics[field.fieldId]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sections={sections}
|
||||||
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
|
<Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label>
|
||||||
<SelectWithAction
|
<Textarea
|
||||||
action={
|
id={`${prefix}-notes`}
|
||||||
asset || !allowQuickCreate
|
maxLength={10000}
|
||||||
? undefined
|
rows={3}
|
||||||
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
|
{...form.register("notes")}
|
||||||
}
|
|
||||||
disabled={!!asset || mutation.isPending}
|
|
||||||
id={`${prefix}-template`}
|
|
||||||
label={t("template")}
|
|
||||||
onValueChange={selectTemplate}
|
|
||||||
options={
|
|
||||||
asset
|
|
||||||
? [{ value: asset.values.templateId, label: asset.specification.templateName }]
|
|
||||||
: templates.map((item) => ({ value: item.id, label: item.values.name }))
|
|
||||||
}
|
|
||||||
placeholder={t("chooseValue")}
|
|
||||||
required
|
|
||||||
value={values.templateId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</fieldset>
|
||||||
<Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
<SelectWithAction
|
<p className="text-sm text-destructive" role="alert">
|
||||||
disabled={mutation.isPending}
|
{t("inventoryErrors.INVALID_REQUEST")}
|
||||||
id={`${prefix}-status`}
|
</p>
|
||||||
label={t("status")}
|
|
||||||
onValueChange={changeStatus}
|
|
||||||
options={AssetStatusSchema.options.map((value) => ({ value, label: t(value) }))}
|
|
||||||
placeholder={t("chooseValue")}
|
|
||||||
required
|
|
||||||
value={values.status}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
{...selectProps}
|
|
||||||
action={
|
|
||||||
allowQuickCreate
|
|
||||||
? {
|
|
||||||
label: t("newSite"),
|
|
||||||
onClick: () => lookups.onCreateSite(siteCreated),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
id={`${prefix}-site`}
|
|
||||||
label={t("site")}
|
|
||||||
onValueChange={(id) => form.setValue("siteId", id || null)}
|
|
||||||
options={lookups.sites.map((site) => ({ value: site.id, label: site.name }))}
|
|
||||||
value={values.siteId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("person")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
{...selectProps}
|
|
||||||
action={
|
|
||||||
allowQuickCreate
|
|
||||||
? {
|
|
||||||
label: t("newPerson"),
|
|
||||||
onClick: () => lookups.onCreatePerson(personCreated),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
id={`${prefix}-person`}
|
|
||||||
label={t("person")}
|
|
||||||
onValueChange={(id) => form.setValue("personId", id || null)}
|
|
||||||
options={lookups.people
|
|
||||||
.filter((person) => person.active || person.id === asset?.values.personId)
|
|
||||||
.map((person) => ({
|
|
||||||
value: person.id,
|
|
||||||
label: person.name,
|
|
||||||
disabled: !person.active && person.id !== values.personId,
|
|
||||||
}))}
|
|
||||||
value={values.personId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{fields.length > 0 && (
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h3 className="font-medium">{t("characteristics")}</h3>
|
|
||||||
<FieldSections
|
|
||||||
hasErrors={!!mutation.error || Object.keys(form.formState.errors).length > 0}
|
|
||||||
items={fields}
|
|
||||||
renderItem={(field) =>
|
|
||||||
field.fieldId === brandFieldId ? (
|
|
||||||
<BrandSelect
|
|
||||||
allowQuickCreate={allowQuickCreate}
|
|
||||||
brandId={values.brandId ?? null}
|
|
||||||
csrfToken={csrfToken}
|
|
||||||
disabled={mutation.isPending}
|
|
||||||
id={`${prefix}-${field.fieldId}`}
|
|
||||||
key={field.fieldId}
|
|
||||||
label={field.name}
|
|
||||||
legacyName={
|
|
||||||
typeof values.characteristics[field.fieldId] === "string"
|
|
||||||
? String(values.characteristics[field.fieldId])
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
onChange={(brand) => {
|
|
||||||
form.setValue("brandId", brand?.id ?? null, { shouldDirty: true });
|
|
||||||
setCharacteristic(field.fieldId, brand?.name);
|
|
||||||
}}
|
|
||||||
required={field.required}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AssetCharacteristicControl
|
|
||||||
disabled={mutation.isPending}
|
|
||||||
field={field}
|
|
||||||
key={field.fieldId}
|
|
||||||
onChange={(value) => setCharacteristic(field.fieldId, value)}
|
|
||||||
prefix={prefix}
|
|
||||||
value={values.characteristics[field.fieldId]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sections={sections}
|
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<InventoryFeedback error={mutation.error} />
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label>
|
<ContextHelp
|
||||||
<Textarea
|
description={t("assetDescription")}
|
||||||
id={`${prefix}-notes`}
|
examples={t("assetExamples")}
|
||||||
maxLength={10000}
|
examplesLabel={t("examplesLabel")}
|
||||||
rows={3}
|
guidance={t("assetGuidance")}
|
||||||
{...form.register("notes")}
|
scopeNote={t("assetScope")}
|
||||||
/>
|
title={t("helpTitle")}
|
||||||
</div>
|
/>
|
||||||
</fieldset>
|
</FormBody>
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("inventoryErrors.INVALID_REQUEST")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<InventoryFeedback error={mutation.error} />
|
|
||||||
<ContextHelp
|
|
||||||
description={t("assetDescription")}
|
|
||||||
examples={t("assetExamples")}
|
|
||||||
examplesLabel={t("examplesLabel")}
|
|
||||||
guidance={t("assetGuidance")}
|
|
||||||
scopeNote={t("assetScope")}
|
|
||||||
title={t("helpTitle")}
|
|
||||||
/>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -29,54 +29,56 @@ export function BrandForm(props: BrandFormProps) {
|
|||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending || isReadingLogo}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending || isReadingLogo}>
|
||||||
<Label htmlFor={id}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor={id}>{t("name")}</Label>
|
||||||
id={id}
|
<Input
|
||||||
maxLength={100}
|
id={id}
|
||||||
readOnly={brand?.builtin}
|
maxLength={100}
|
||||||
required
|
readOnly={brand?.builtin}
|
||||||
{...form.register("name")}
|
required
|
||||||
|
{...form.register("name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BrandLogoField
|
||||||
|
error={logoError ?? form.formState.errors.logoSvg?.message}
|
||||||
|
logoSvg={logoSvg}
|
||||||
|
name={name}
|
||||||
|
onRemove={removeLogo}
|
||||||
|
onSelect={selectLogo}
|
||||||
/>
|
/>
|
||||||
</div>
|
{!quickCreate && (
|
||||||
<BrandLogoField
|
<SwitchField
|
||||||
error={logoError ?? form.formState.errors.logoSvg?.message}
|
checked={form.watch("active")}
|
||||||
logoSvg={logoSvg}
|
id={`${id}-active`}
|
||||||
name={name}
|
label={t("brands.active")}
|
||||||
onRemove={removeLogo}
|
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
||||||
onSelect={selectLogo}
|
/>
|
||||||
/>
|
|
||||||
{!quickCreate && (
|
|
||||||
<SwitchField
|
|
||||||
checked={form.watch("active")}
|
|
||||||
id={`${id}-active`}
|
|
||||||
label={t("brands.active")}
|
|
||||||
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
{(mutation.error || form.formState.errors.name || form.formState.errors.active) && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t(
|
|
||||||
"brands.errors." +
|
|
||||||
(mutation.error instanceof BrandApiError
|
|
||||||
? mutation.error.code
|
|
||||||
: mutation.error
|
|
||||||
? "INTERNAL_ERROR"
|
|
||||||
: "INVALID_REQUEST")
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</fieldset>
|
||||||
)}
|
{(mutation.error || form.formState.errors.name || form.formState.errors.active) && (
|
||||||
<ContextHelp
|
<p className="text-sm text-destructive" role="alert">
|
||||||
description={t("brands.help")}
|
{t(
|
||||||
examples={t("brands.examples")}
|
"brands.errors." +
|
||||||
examplesLabel={t("examplesLabel")}
|
(mutation.error instanceof BrandApiError
|
||||||
guidance={t("brands.guidance")}
|
? mutation.error.code
|
||||||
scopeNote={t("globalHelp")}
|
: mutation.error
|
||||||
title={t("helpTitle")}
|
? "INTERNAL_ERROR"
|
||||||
/>
|
: "INVALID_REQUEST")
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<ContextHelp
|
||||||
|
description={t("brands.help")}
|
||||||
|
examples={t("brands.examples")}
|
||||||
|
examplesLabel={t("examplesLabel")}
|
||||||
|
guidance={t("brands.guidance")}
|
||||||
|
scopeNote={t("globalHelp")}
|
||||||
|
title={t("helpTitle")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending || isReadingLogo} type="submit">
|
<Button disabled={mutation.isPending || isReadingLogo} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -19,54 +19,56 @@ export function CatalogFieldForm(props: CatalogFieldFormProps) {
|
|||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-type`}>{t("type")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
disabled={!!item || mutation.isPending}
|
|
||||||
id={`${id}-type`}
|
|
||||||
label={t("type")}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const parsed = FieldTypeSchema.safeParse(value);
|
|
||||||
if (parsed.success) {
|
|
||||||
form.setValue("type", parsed.data);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
|
|
||||||
placeholder={t("chooseType")}
|
|
||||||
required
|
|
||||||
value={type}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-unit`}>{t("unit")}</Label>
|
|
||||||
<Input id={`${id}-unit`} maxLength={30} {...form.register("unit")} />
|
|
||||||
</div>
|
|
||||||
{type === "choice" && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-choices`}>{t("choices")}</Label>
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
<Textarea id={`${id}-choices`} required rows={5} {...form.register("choicesText")} />
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
<p className="text-xs text-muted-foreground">{t("choicesHelp")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor={`${id}-type`}>{t("type")}</Label>
|
||||||
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
<SelectWithAction
|
||||||
<Textarea
|
disabled={!!item || mutation.isPending}
|
||||||
id={`${id}-description`}
|
id={`${id}-type`}
|
||||||
maxLength={1000}
|
label={t("type")}
|
||||||
rows={3}
|
onValueChange={(value) => {
|
||||||
{...form.register("description")}
|
const parsed = FieldTypeSchema.safeParse(value);
|
||||||
|
if (parsed.success) {
|
||||||
|
form.setValue("type", parsed.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
|
||||||
|
placeholder={t("chooseType")}
|
||||||
|
required
|
||||||
|
value={type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-unit`}>{t("unit")}</Label>
|
||||||
|
<Input id={`${id}-unit`} maxLength={30} {...form.register("unit")} />
|
||||||
|
</div>
|
||||||
|
{type === "choice" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-choices`}>{t("choices")}</Label>
|
||||||
|
<Textarea id={`${id}-choices`} required rows={5} {...form.register("choicesText")} />
|
||||||
|
<p className="text-xs text-muted-foreground">{t("choicesHelp")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${id}-description`}
|
||||||
|
maxLength={1000}
|
||||||
|
rows={3}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CatalogError
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
<CatalogHelp kind="field" scope={scope} />
|
||||||
<CatalogError
|
</FormBody>
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<CatalogHelp kind="field" scope={scope} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -18,49 +18,56 @@ export function CatalogRelationForm(props: CatalogRelationFormProps) {
|
|||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={id}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor={id}>{t("name")}</Label>
|
||||||
id={id}
|
<Input
|
||||||
maxLength={100}
|
id={id}
|
||||||
required
|
maxLength={100}
|
||||||
{...form.register("name")}
|
required
|
||||||
onChange={(event) => {
|
{...form.register("name")}
|
||||||
form.setValue("name", event.target.value);
|
onChange={(event) => {
|
||||||
if (symmetric) {
|
form.setValue("name", event.target.value);
|
||||||
form.setValue("inverseName", event.target.value);
|
if (symmetric) {
|
||||||
|
form.setValue("inverseName", event.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={symmetric}
|
||||||
|
disabled={!!item}
|
||||||
|
id={`${id}-symmetric`}
|
||||||
|
label={t("symmetric")}
|
||||||
|
onCheckedChange={(isSymmetric) => {
|
||||||
|
form.setValue("symmetric", isSymmetric);
|
||||||
|
if (isSymmetric) {
|
||||||
|
form.setValue("inverseName", form.getValues("name"));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
{!symmetric && (
|
||||||
<SwitchField
|
<div className="space-y-2">
|
||||||
checked={symmetric}
|
<Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label>
|
||||||
disabled={!!item}
|
<Input
|
||||||
id={`${id}-symmetric`}
|
id={`${id}-inverse`}
|
||||||
label={t("symmetric")}
|
maxLength={100}
|
||||||
onCheckedChange={(isSymmetric) => {
|
required
|
||||||
form.setValue("symmetric", isSymmetric);
|
{...form.register("inverseName")}
|
||||||
if (isSymmetric) {
|
/>
|
||||||
form.setValue("inverseName", form.getValues("name"));
|
</div>
|
||||||
}
|
)}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!symmetric && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label>
|
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
||||||
<Input id={`${id}-inverse`} maxLength={100} required {...form.register("inverseName")} />
|
<Textarea id={`${id}-description`} maxLength={1000} {...form.register("description")} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
<CatalogError
|
||||||
<div className="space-y-2">
|
error={mutation.error}
|
||||||
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
<Textarea id={`${id}-description`} maxLength={1000} {...form.register("description")} />
|
/>
|
||||||
</div>
|
<CatalogHelp kind="relation" scope={scope} />
|
||||||
<CatalogError
|
</FormBody>
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<CatalogHelp kind="relation" scope={scope} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { FormSection } from "@repo/rdx-ui/components/form-section";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { TemplateSectionsEditor } from "@repo/rdx-ui/components/template-sections-editor";
|
import { TemplateSectionsEditor } from "@repo/rdx-ui/components/template-sections-editor";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -23,86 +24,90 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<FormBody>
|
||||||
|
<FormSection title={t("formLayout.template")}>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${id}-description`}
|
||||||
|
maxLength={1000}
|
||||||
|
rows={2}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
<Label htmlFor={`${id}-field`}>{t("selectField")}</Label>
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
<SelectWithAction
|
||||||
</div>
|
action={
|
||||||
<div className="space-y-2">
|
allowQuickCreate
|
||||||
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
? { label: t("addField"), onClick: () => setCreatingField(true) }
|
||||||
<Textarea
|
: undefined
|
||||||
id={`${id}-description`}
|
}
|
||||||
maxLength={1000}
|
disabled={mutation.isPending}
|
||||||
rows={2}
|
id={`${id}-field`}
|
||||||
{...form.register("description")}
|
label={t("selectField")}
|
||||||
|
onValueChange={add}
|
||||||
|
options={fields
|
||||||
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
|
||||||
|
)
|
||||||
|
.map((entry) => ({ value: entry.id, label: entry.values.name }))}
|
||||||
|
placeholder={t("choose")}
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
|
||||||
<div className="space-y-2">
|
<TemplateSectionsEditor
|
||||||
<Label htmlFor={`${id}-field`}>{t("selectField")}</Label>
|
createId={() => UniqueID.generateNewID().toPrimitive()}
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
allowQuickCreate
|
|
||||||
? { label: t("addField"), onClick: () => setCreatingField(true) }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
id={`${id}-field`}
|
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))}
|
||||||
label={t("selectField")}
|
fieldNotes={Object.fromEntries(
|
||||||
onValueChange={add}
|
fields
|
||||||
options={fields
|
.filter((field) => !field.effectiveActive)
|
||||||
.filter(
|
.map((field) => [field.id, t("disabledField")])
|
||||||
(entry) =>
|
)}
|
||||||
entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
|
fields={selected}
|
||||||
)
|
labels={{
|
||||||
.map((entry) => ({ value: entry.id, label: entry.values.name }))}
|
title: t("sections.title"),
|
||||||
placeholder={t("choose")}
|
add: t("sections.add"),
|
||||||
value=""
|
name: t("sections.name"),
|
||||||
|
description: t("sections.description"),
|
||||||
|
ungrouped: t("sections.ungrouped"),
|
||||||
|
moveUp: t("sections.moveUp"),
|
||||||
|
moveDown: t("sections.moveDown"),
|
||||||
|
remove: t("sections.remove"),
|
||||||
|
removeField: t("sections.removeField"),
|
||||||
|
required: t("sections.required"),
|
||||||
|
assign: t("sections.assign"),
|
||||||
|
help: t("sections.help"),
|
||||||
|
empty: t("sections.empty"),
|
||||||
|
}}
|
||||||
|
onChange={({ sections, fields: next }) => {
|
||||||
|
form.setValue("sections", sections, { shouldDirty: true });
|
||||||
|
form.setValue("fields", next, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
sections={form.watch("sections") ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
{form.formState.errors.sections && (
|
||||||
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
|
<p className="text-sm text-destructive" role="alert">
|
||||||
<TemplateSectionsEditor
|
{t("errors.INVALID_SECTIONS")}
|
||||||
createId={() => UniqueID.generateNewID().toPrimitive()}
|
</p>
|
||||||
disabled={mutation.isPending}
|
|
||||||
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))}
|
|
||||||
fieldNotes={Object.fromEntries(
|
|
||||||
fields
|
|
||||||
.filter((field) => !field.effectiveActive)
|
|
||||||
.map((field) => [field.id, t("disabledField")])
|
|
||||||
)}
|
)}
|
||||||
fields={selected}
|
<CatalogError
|
||||||
labels={{
|
error={mutation.error}
|
||||||
title: t("sections.title"),
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
add: t("sections.add"),
|
/>
|
||||||
name: t("sections.name"),
|
<CatalogHelp kind="template" scope={scope} />
|
||||||
description: t("sections.description"),
|
</FormBody>
|
||||||
ungrouped: t("sections.ungrouped"),
|
|
||||||
moveUp: t("sections.moveUp"),
|
|
||||||
moveDown: t("sections.moveDown"),
|
|
||||||
remove: t("sections.remove"),
|
|
||||||
removeField: t("sections.removeField"),
|
|
||||||
required: t("sections.required"),
|
|
||||||
assign: t("sections.assign"),
|
|
||||||
help: t("sections.help"),
|
|
||||||
empty: t("sections.empty"),
|
|
||||||
}}
|
|
||||||
onChange={({ sections, fields: next }) => {
|
|
||||||
form.setValue("sections", sections, { shouldDirty: true });
|
|
||||||
form.setValue("fields", next, { shouldDirty: true });
|
|
||||||
}}
|
|
||||||
sections={form.watch("sections") ?? []}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.sections && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("errors.INVALID_SECTIONS")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CatalogError
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<CatalogHelp kind="template" scope={scope} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -21,75 +21,81 @@ export function SupportForm(props: SupportFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={prefix}>{t("support.tool")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={prefix}>{t("support.tool")}</Label>
|
||||||
action={{ label: t("support.newTool"), onClick: () => setQuick(true) }}
|
<SelectWithAction
|
||||||
id={prefix}
|
action={{ label: t("support.newTool"), onClick: () => setQuick(true) }}
|
||||||
label={t("support.tool")}
|
id={prefix}
|
||||||
onValueChange={(id) => form.setValue("toolId", id)}
|
label={t("support.tool")}
|
||||||
options={tools
|
onValueChange={(id) => form.setValue("toolId", id)}
|
||||||
.filter((item) => item.active || item.id === connection?.values.toolId)
|
options={tools
|
||||||
.map((item) => ({ value: item.id, label: item.name }))}
|
.filter((item) => item.active || item.id === connection?.values.toolId)
|
||||||
placeholder={t("support.choose")}
|
.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
value={values.toolId}
|
placeholder={t("support.choose")}
|
||||||
/>
|
value={values.toolId}
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-destination`}>{t("support.destination")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${prefix}-destination`} maxLength={300} {...form.register("destination")} />
|
<Label htmlFor={`${prefix}-destination`}>{t("support.destination")}</Label>
|
||||||
</div>
|
<Input
|
||||||
<div className="space-y-2">
|
id={`${prefix}-destination`}
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("support.state")}</Label>
|
maxLength={300}
|
||||||
<SelectWithAction
|
{...form.register("destination")}
|
||||||
id={`${prefix}-state`}
|
/>
|
||||||
label={t("support.state")}
|
</div>
|
||||||
onValueChange={(state) => {
|
<div className="space-y-2">
|
||||||
if (state === "pending" || state === "available" || state === "retired") {
|
<Label htmlFor={`${prefix}-state`}>{t("support.state")}</Label>
|
||||||
form.setValue("state", state);
|
<SelectWithAction
|
||||||
if (state !== "available") {
|
id={`${prefix}-state`}
|
||||||
form.setValue("preferred", false);
|
label={t("support.state")}
|
||||||
|
onValueChange={(state) => {
|
||||||
|
if (state === "pending" || state === "available" || state === "retired") {
|
||||||
|
form.setValue("state", state);
|
||||||
|
if (state !== "available") {
|
||||||
|
form.setValue("preferred", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}}
|
options={["pending", "available", "retired"].map((state) => ({
|
||||||
options={["pending", "available", "retired"].map((state) => ({
|
value: state,
|
||||||
value: state,
|
label: t(`support.${state}`),
|
||||||
label: t(`support.${state}`),
|
}))}
|
||||||
}))}
|
placeholder={t("support.chooseState")}
|
||||||
placeholder={t("support.chooseState")}
|
value={values.state}
|
||||||
value={values.state}
|
/>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={values.requiresVpn}
|
||||||
|
id={`${prefix}-requires-vpn`}
|
||||||
|
label={t("support.requiresVpn")}
|
||||||
|
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<SwitchField
|
||||||
<SwitchField
|
checked={values.preferred}
|
||||||
checked={values.requiresVpn}
|
disabled={values.state !== "available"}
|
||||||
id={`${prefix}-requires-vpn`}
|
id={`${prefix}-preferred`}
|
||||||
label={t("support.requiresVpn")}
|
label={t("support.preferred")}
|
||||||
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
|
onCheckedChange={(preferred) => form.setValue("preferred", preferred)}
|
||||||
/>
|
|
||||||
<SwitchField
|
|
||||||
checked={values.preferred}
|
|
||||||
disabled={values.state !== "available"}
|
|
||||||
id={`${prefix}-preferred`}
|
|
||||||
label={t("support.preferred")}
|
|
||||||
onCheckedChange={(preferred) => form.setValue("preferred", preferred)}
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("support.notes")}</Label>
|
|
||||||
<textarea
|
|
||||||
className="min-h-24 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
|
||||||
id={`${prefix}-notes`}
|
|
||||||
maxLength={2000}
|
|
||||||
{...form.register("notes")}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
</fieldset>
|
<Label htmlFor={`${prefix}-notes`}>{t("support.notes")}</Label>
|
||||||
<SupportFeedback
|
<textarea
|
||||||
error={mutation.error}
|
className="min-h-24 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
id={`${prefix}-notes`}
|
||||||
/>
|
maxLength={2000}
|
||||||
<SupportHelp />
|
{...form.register("notes")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<SupportFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
/>
|
||||||
|
<SupportHelp />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -17,30 +17,32 @@ export function SupportToolForm(props: SupportToolFormProps) {
|
|||||||
const { t } = useTranslation("assets");
|
const { t } = useTranslation("assets");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={id}>{t("support.toolName")}</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor={id}>{t("support.toolName")}</Label>
|
||||||
id={id}
|
<Input
|
||||||
maxLength={100}
|
id={id}
|
||||||
readOnly={tool?.builtin}
|
maxLength={100}
|
||||||
required
|
readOnly={tool?.builtin}
|
||||||
{...form.register("name")}
|
required
|
||||||
|
{...form.register("name")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={form.watch("active")}
|
||||||
|
id={`${id}-active`}
|
||||||
|
label={t("support.active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</fieldset>
|
||||||
<SwitchField
|
<SupportFeedback
|
||||||
checked={form.watch("active")}
|
error={mutation.error}
|
||||||
id={`${id}-active`}
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
label={t("support.active")}
|
|
||||||
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
|
||||||
/>
|
/>
|
||||||
</fieldset>
|
<SupportHelp tool />
|
||||||
<SupportFeedback
|
</FormBody>
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<SupportHelp tool />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -36,143 +36,145 @@ export function WorkstationForm(props: WorkstationFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={prefix}>{t("name")}</Label>
|
|
||||||
<Input id={prefix} maxLength={100} required {...form.register("name")} />
|
|
||||||
</div>
|
|
||||||
<SwitchField
|
|
||||||
checked={values.active}
|
|
||||||
id={`${prefix}-active`}
|
|
||||||
label={t("active")}
|
|
||||||
onCheckedChange={(active) => form.setValue("active", active)}
|
|
||||||
/>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
|
<Label htmlFor={prefix}>{t("name")}</Label>
|
||||||
<SelectWithAction
|
<Input id={prefix} maxLength={100} required {...form.register("name")} />
|
||||||
action={{
|
|
||||||
label: t("newSite"),
|
|
||||||
onClick: () => lookups.onCreateSite(siteCreated),
|
|
||||||
}}
|
|
||||||
id={`${prefix}-site`}
|
|
||||||
label={t("site")}
|
|
||||||
onValueChange={(id) => form.setValue("siteId", id || null)}
|
|
||||||
options={lookups.sites.map((item) => ({ value: item.id, label: item.name }))}
|
|
||||||
placeholder={t("noAssignment")}
|
|
||||||
value={values.siteId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<SwitchField
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("workstationPerson")}</Label>
|
checked={values.active}
|
||||||
<SelectWithAction
|
id={`${prefix}-active`}
|
||||||
action={{
|
label={t("active")}
|
||||||
label: t("newPerson"),
|
onCheckedChange={(active) => form.setValue("active", active)}
|
||||||
onClick: () => lookups.onCreatePerson(personCreated),
|
/>
|
||||||
}}
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
id={`${prefix}-person`}
|
<div className="space-y-2">
|
||||||
label={t("workstationPerson")}
|
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
|
||||||
onValueChange={(id) => form.setValue("personId", id || null)}
|
<SelectWithAction
|
||||||
options={lookups.people
|
action={{
|
||||||
.filter((item) => item.active || item.id === workstation?.values.personId)
|
label: t("newSite"),
|
||||||
.map((item) => ({ value: item.id, label: item.name }))}
|
onClick: () => lookups.onCreateSite(siteCreated),
|
||||||
placeholder={t("noAssignment")}
|
}}
|
||||||
value={values.personId ?? ""}
|
id={`${prefix}-site`}
|
||||||
/>
|
label={t("site")}
|
||||||
|
onValueChange={(id) => form.setValue("siteId", id || null)}
|
||||||
|
options={lookups.sites.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
|
placeholder={t("noAssignment")}
|
||||||
|
value={values.siteId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-person`}>{t("workstationPerson")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={{
|
||||||
|
label: t("newPerson"),
|
||||||
|
onClick: () => lookups.onCreatePerson(personCreated),
|
||||||
|
}}
|
||||||
|
id={`${prefix}-person`}
|
||||||
|
label={t("workstationPerson")}
|
||||||
|
onValueChange={(id) => form.setValue("personId", id || null)}
|
||||||
|
options={lookups.people
|
||||||
|
.filter((item) => item.active || item.id === workstation?.values.personId)
|
||||||
|
.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
|
placeholder={t("noAssignment")}
|
||||||
|
value={values.personId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{(["assetIds", "sharedAssetIds"] as const).map((field) => (
|
{(["assetIds", "sharedAssetIds"] as const).map((field) => (
|
||||||
<div className="space-y-2" key={field}>
|
<div className="space-y-2" key={field}>
|
||||||
<Label htmlFor={`${prefix}-${field}`}>
|
<Label htmlFor={`${prefix}-${field}`}>
|
||||||
{t(field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset")}
|
{t(field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset")}
|
||||||
</Label>
|
</Label>
|
||||||
<SelectWithAction
|
<SelectWithAction
|
||||||
action={{
|
action={{
|
||||||
label: t("newAsset"),
|
label: t("newAsset"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setQuickField(field);
|
setQuickField(field);
|
||||||
setQuick(true);
|
setQuick(true);
|
||||||
},
|
},
|
||||||
disabled: !catalog.data,
|
disabled: !catalog.data,
|
||||||
}}
|
}}
|
||||||
disabled={inventory.isPending || values[field].length >= 200}
|
disabled={inventory.isPending || values[field].length >= 200}
|
||||||
id={`${prefix}-${field}`}
|
id={`${prefix}-${field}`}
|
||||||
label={t(
|
label={t(
|
||||||
field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset"
|
field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset"
|
||||||
)}
|
)}
|
||||||
onValueChange={(id) => add(field, id)}
|
onValueChange={(id) => add(field, id)}
|
||||||
options={assets
|
options={assets
|
||||||
.filter(
|
.filter(
|
||||||
(item) => ![...values.assetIds, ...values.sharedAssetIds].includes(item.id)
|
(item) => ![...values.assetIds, ...values.sharedAssetIds].includes(item.id)
|
||||||
)
|
)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: unavailable(field, item.id)
|
label: unavailable(field, item.id)
|
||||||
? t("workstationOccupied", { name: item.values.name })
|
? t("workstationOccupied", { name: item.values.name })
|
||||||
: item.values.name,
|
: item.values.name,
|
||||||
disabled: unavailable(field, item.id),
|
disabled: unavailable(field, item.id),
|
||||||
}))}
|
}))}
|
||||||
placeholder={t("chooseAsset")}
|
placeholder={t("chooseAsset")}
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
{values[field].length ? (
|
{values[field].length ? (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{values[field].map((id) => (
|
{values[field].map((id) => (
|
||||||
<li
|
<li
|
||||||
className="flex items-center justify-between gap-3 rounded-md border p-3 text-sm"
|
className="flex items-center justify-between gap-3 rounded-md border p-3 text-sm"
|
||||||
key={id}
|
key={id}
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>{assets.find((item) => item.id === id)?.values.name ?? id}</p>
|
|
||||||
{unavailable(field, id) && (
|
|
||||||
<p className="text-destructive">
|
|
||||||
{t("inventoryErrors.ASSET_IN_WORKSTATION")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
aria-label={t("removeWorkstationAsset", {
|
|
||||||
name: assets.find((item) => item.id === id)?.values.name ?? id,
|
|
||||||
})}
|
|
||||||
onClick={() =>
|
|
||||||
form.setValue(
|
|
||||||
field,
|
|
||||||
values[field].filter((item) => item !== id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
>
|
||||||
×
|
<div>
|
||||||
</Button>
|
<p>{assets.find((item) => item.id === id)?.values.name ?? id}</p>
|
||||||
</li>
|
{unavailable(field, id) && (
|
||||||
))}
|
<p className="text-destructive">
|
||||||
</ul>
|
{t("inventoryErrors.ASSET_IN_WORKSTATION")}
|
||||||
) : (
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
)}
|
||||||
{t(field === "assetIds" ? "noWorkstationAssets" : "noSharedWorkstationAssets")}
|
</div>
|
||||||
</p>
|
<Button
|
||||||
)}
|
aria-label={t("removeWorkstationAsset", {
|
||||||
</div>
|
name: assets.find((item) => item.id === id)?.values.name ?? id,
|
||||||
))}
|
})}
|
||||||
</fieldset>
|
onClick={() =>
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
form.setValue(
|
||||||
<p className="text-sm text-destructive" role="alert">
|
field,
|
||||||
{t("inventoryErrors.INVALID_REQUEST")}
|
values[field].filter((item) => item !== id)
|
||||||
</p>
|
)
|
||||||
)}
|
}
|
||||||
<InventoryFeedback error={mutation.error ?? inventory.error} />
|
size="sm"
|
||||||
<ContextHelp
|
type="button"
|
||||||
description={t("workstationDescription")}
|
variant="outline"
|
||||||
examples={t("workstationExamples")}
|
>
|
||||||
examplesLabel={t("examplesLabel")}
|
×
|
||||||
guidance={t("workstationGuidance")}
|
</Button>
|
||||||
scopeNote={t("workstationScope")}
|
</li>
|
||||||
title={t("helpTitle")}
|
))}
|
||||||
/>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(field === "assetIds" ? "noWorkstationAssets" : "noSharedWorkstationAssets")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{t("inventoryErrors.INVALID_REQUEST")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<InventoryFeedback error={mutation.error ?? inventory.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("workstationDescription")}
|
||||||
|
examples={t("workstationExamples")}
|
||||||
|
examplesLabel={t("examplesLabel")}
|
||||||
|
guidance={t("workstationGuidance")}
|
||||||
|
scopeNote={t("workstationScope")}
|
||||||
|
title={t("helpTitle")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions, FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
import { Label } from "@repo/shadcn-ui/components/label";
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
@ -23,48 +23,50 @@ export function QuickCreateDialog(props: QuickCreateDialogProps) {
|
|||||||
open
|
open
|
||||||
title={t(target.kind === "system" ? "quickAddSystem" : "quickAddEnvironment")}
|
title={t(target.kind === "system" ? "quickAddSystem" : "quickAddEnvironment")}
|
||||||
>
|
>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
{target.kind === "environment" && (
|
<FormBody>
|
||||||
<p className="text-sm text-muted-foreground">
|
{target.kind === "environment" && (
|
||||||
{t("quickEnvironmentContext", { name: target.systemName })}
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
{t("quickEnvironmentContext", { name: target.systemName })}
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={id}>{t("name")}</Label>
|
|
||||||
<Input
|
|
||||||
id={id}
|
|
||||||
maxLength={100}
|
|
||||||
required
|
|
||||||
{...form.register("name", { validate: (value) => value.trim().length > 0 })}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.name && (
|
|
||||||
<p className="text-destructive" role="alert">
|
|
||||||
{t("invalid")}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
{mutation.isError && (
|
|
||||||
<p className="text-destructive" role="alert">
|
|
||||||
{t(
|
|
||||||
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{existing && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>{t("quickExisting")}</p>
|
<Label htmlFor={id}>{t("name")}</Label>
|
||||||
<Button
|
<Input
|
||||||
disabled={mutation.isPending}
|
id={id}
|
||||||
onClick={() => onCreated(existing.id)}
|
maxLength={100}
|
||||||
type="button"
|
required
|
||||||
variant="outline"
|
{...form.register("name", { validate: (value) => value.trim().length > 0 })}
|
||||||
>
|
/>
|
||||||
{t("quickUseExisting")}
|
{form.formState.errors.name && (
|
||||||
</Button>
|
<p className="text-destructive" role="alert">
|
||||||
|
{t("invalid")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{mutation.isError && (
|
||||||
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
|
<p className="text-destructive" role="alert">
|
||||||
<EntityHelp kind={target.kind} scope={scope} />
|
{t(
|
||||||
|
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{existing && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("quickExisting")}</p>
|
||||||
|
<Button
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
onClick={() => onCreated(existing.id)}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("quickUseExisting")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
|
||||||
|
<EntityHelp kind={target.kind} scope={scope} />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "quickSaveSelect")}
|
{t(mutation.isPending ? "saving" : "quickSaveSelect")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
@ -37,52 +37,54 @@ export function AccessFormFields(
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<FormBody>
|
||||||
{editableFields.map((field) => (
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div
|
{editableFields.map((field) => (
|
||||||
className={["note", "permissions"].includes(field.type) ? "sm:col-span-2" : ""}
|
<div
|
||||||
key={field.key}
|
className={["note", "permissions"].includes(field.type) ? "sm:col-span-2" : ""}
|
||||||
>
|
key={field.key}
|
||||||
<AccessFieldControl
|
>
|
||||||
createLabel={
|
<AccessFieldControl
|
||||||
field.key === "systemId" || field.key === "environmentId"
|
createLabel={
|
||||||
? undefined
|
field.key === "systemId" || field.key === "environmentId"
|
||||||
: t("quickAddEntry", { name: t(field.key) })
|
? undefined
|
||||||
}
|
: t("quickAddEntry", { name: t(field.key) })
|
||||||
disabled={
|
}
|
||||||
Boolean(item && field.fixed) ||
|
disabled={
|
||||||
lockedFields.includes(field.key) ||
|
Boolean(item && field.fixed) ||
|
||||||
mutation.isPending
|
lockedFields.includes(field.key) ||
|
||||||
}
|
mutation.isPending
|
||||||
field={field}
|
}
|
||||||
form={form}
|
field={field}
|
||||||
id={`${prefix}-${field.key}`}
|
form={form}
|
||||||
onCreate={quickAction(field)}
|
id={`${prefix}-${field.key}`}
|
||||||
onSelected={(value) => select(field, value)}
|
onCreate={quickAction(field)}
|
||||||
options={options(field)}
|
onSelected={(value) => select(field, value)}
|
||||||
value={watched[field.key]}
|
options={options(field)}
|
||||||
/>
|
value={watched[field.key]}
|
||||||
</div>
|
/>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
{validation && (
|
</div>
|
||||||
<p className="text-destructive" role="alert">
|
{validation && (
|
||||||
{validation}
|
<p className="text-destructive" role="alert">
|
||||||
</p>
|
{validation}
|
||||||
)}
|
</p>
|
||||||
{mutation.isError && (
|
)}
|
||||||
<p className="text-destructive" role="alert">
|
{mutation.isError && (
|
||||||
{t(
|
<p className="text-destructive" role="alert">
|
||||||
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
|
{t(
|
||||||
)}
|
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
|
||||||
</p>
|
)}
|
||||||
)}
|
</p>
|
||||||
{help[kind] && <p className="text-sm text-muted-foreground">{t(help[kind])}</p>}
|
)}
|
||||||
{["account_person", "membership", "grant"].includes(kind) && (
|
{help[kind] && <p className="text-sm text-muted-foreground">{t(help[kind])}</p>}
|
||||||
<p className="text-sm text-muted-foreground">{t("configurationHelp")}</p>
|
{["account_person", "membership", "grant"].includes(kind) && (
|
||||||
)}
|
<p className="text-sm text-muted-foreground">{t("configurationHelp")}</p>
|
||||||
<EntityHelp kind={kind} scope={scope} />
|
)}
|
||||||
|
<EntityHelp kind={kind} scope={scope} />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -45,121 +45,123 @@ export function ApplicationConfigurationForm(props: ApplicationConfigurationForm
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
fixed
|
||||||
|
? undefined
|
||||||
|
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
||||||
|
}
|
||||||
|
disabled={fixed || !ready}
|
||||||
|
id={`${prefix}-profile`}
|
||||||
|
label={t("mail.profile")}
|
||||||
|
onValueChange={(id) => form.setValue("profileId", id)}
|
||||||
|
options={applicationProfiles(values.profileId, profiles.data?.profiles)}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.profileId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-resource`}>{t("applications.resource")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
configuration
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
label: t("applications.newResource"),
|
||||||
|
onClick: () => setQuick("application"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={!!configuration || !ready}
|
||||||
|
id={`${prefix}-resource`}
|
||||||
|
label={t("applications.resource")}
|
||||||
|
onValueChange={(id) => form.setValue("resourceId", id)}
|
||||||
|
options={applicationOptions(
|
||||||
|
values.resourceId,
|
||||||
|
resources.data?.resources,
|
||||||
|
catalog.data?.items
|
||||||
|
)}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.resourceId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-delivery`}>{t("applications.delivery")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-delivery`}
|
||||||
|
label={t("applications.delivery")}
|
||||||
|
onValueChange={changeDelivery}
|
||||||
|
options={["installed", "web"].map((value) => ({
|
||||||
|
value,
|
||||||
|
label: t(`applications.${value}`),
|
||||||
|
}))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.delivery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-state`}
|
||||||
|
label={t("mail.state")}
|
||||||
|
onValueChange={changeState}
|
||||||
|
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.state}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{values.delivery === "installed" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-version`}>{t("applications.version")}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-version`}
|
||||||
|
maxLength={100}
|
||||||
|
onChange={(event) => form.setValue("version", event.target.value || null)}
|
||||||
|
value={values.version ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
<SelectWithAction
|
<Textarea
|
||||||
action={
|
id={`${prefix}-notes`}
|
||||||
fixed
|
maxLength={4000}
|
||||||
? undefined
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
rows={3}
|
||||||
}
|
value={values.notes ?? ""}
|
||||||
disabled={fixed || !ready}
|
|
||||||
id={`${prefix}-profile`}
|
|
||||||
label={t("mail.profile")}
|
|
||||||
onValueChange={(id) => form.setValue("profileId", id)}
|
|
||||||
options={applicationProfiles(values.profileId, profiles.data?.profiles)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.profileId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-resource`}>{t("applications.resource")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
configuration
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
label: t("applications.newResource"),
|
|
||||||
onClick: () => setQuick("application"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disabled={!!configuration || !ready}
|
|
||||||
id={`${prefix}-resource`}
|
|
||||||
label={t("applications.resource")}
|
|
||||||
onValueChange={(id) => form.setValue("resourceId", id)}
|
|
||||||
options={applicationOptions(
|
|
||||||
values.resourceId,
|
|
||||||
resources.data?.resources,
|
|
||||||
catalog.data?.items
|
|
||||||
)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.resourceId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-delivery`}>{t("applications.delivery")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
id={`${prefix}-delivery`}
|
|
||||||
label={t("applications.delivery")}
|
|
||||||
onValueChange={changeDelivery}
|
|
||||||
options={["installed", "web"].map((value) => ({
|
|
||||||
value,
|
|
||||||
label: t(`applications.${value}`),
|
|
||||||
}))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.delivery}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
id={`${prefix}-state`}
|
|
||||||
label={t("mail.state")}
|
|
||||||
onValueChange={changeState}
|
|
||||||
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.state}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{values.delivery === "installed" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-version`}>{t("applications.version")}</Label>
|
|
||||||
<Input
|
|
||||||
id={`${prefix}-version`}
|
|
||||||
maxLength={100}
|
|
||||||
onChange={(event) => form.setValue("version", event.target.value || null)}
|
|
||||||
value={values.version ?? ""}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{configuration && (
|
||||||
|
<p className="text-sm text-muted-foreground">{t("applications.fixed")}</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
<p className="text-sm text-destructive" role="alert">
|
||||||
<Textarea
|
{t("applications.errors.INVALID_REQUEST")}
|
||||||
id={`${prefix}-notes`}
|
</p>
|
||||||
maxLength={4000}
|
)}
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
<ApplicationFeedback
|
||||||
rows={3}
|
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error}
|
||||||
value={values.notes ?? ""}
|
/>
|
||||||
/>
|
<ContextHelp
|
||||||
</div>
|
description={t("applications.description")}
|
||||||
</fieldset>
|
examples={t("applications.examples")}
|
||||||
{configuration && (
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
<p className="text-sm text-muted-foreground">{t("applications.fixed")}</p>
|
guidance={t("applications.guidance")}
|
||||||
)}
|
scopeNote={t("applications.scope")}
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
title={t("applications.help")}
|
||||||
<p className="text-sm text-destructive" role="alert">
|
/>
|
||||||
{t("applications.errors.INVALID_REQUEST")}
|
</FormBody>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<ApplicationFeedback
|
|
||||||
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error}
|
|
||||||
/>
|
|
||||||
<ContextHelp
|
|
||||||
description={t("applications.description")}
|
|
||||||
examples={t("applications.examples")}
|
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
|
||||||
guidance={t("applications.guidance")}
|
|
||||||
scopeNote={t("applications.scope")}
|
|
||||||
title={t("applications.help")}
|
|
||||||
/>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={!ready || mutation.isPending} type="submit">
|
<Button disabled={!ready || mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -21,69 +21,73 @@ export function AssignmentForm(props: AssignmentFormProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={`${id}-department`}>{t("department")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
assignment
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
label: t("newDepartment"),
|
|
||||||
onClick: () => setCreatingDepartment(true),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disabled={assignment !== undefined || mutation.isPending}
|
|
||||||
id={`${id}-department`}
|
|
||||||
invalid={!!form.formState.errors.departmentId}
|
|
||||||
label={t("department")}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
form.setValue("departmentId", value, { shouldDirty: true, shouldValidate: true })
|
|
||||||
}
|
|
||||||
options={company.departments
|
|
||||||
.filter((item) => item.active || item.id === assignment?.departmentId)
|
|
||||||
.map((department) => ({
|
|
||||||
value: department.id,
|
|
||||||
label: department.name + (department.active ? "" : ` · ${t("inactive")}`),
|
|
||||||
}))}
|
|
||||||
placeholder={t("chooseDepartment")}
|
|
||||||
required
|
|
||||||
value={form.watch("departmentId")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-position`}>{t("departmentPosition")}</Label>
|
|
||||||
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-start`}>{t("startDate")}</Label>
|
<Label htmlFor={`${id}-department`}>{t("department")}</Label>
|
||||||
<Input id={`${id}-start`} required type="date" {...form.register("startDate")} />
|
<SelectWithAction
|
||||||
</div>
|
action={
|
||||||
<div className="space-y-2">
|
assignment
|
||||||
<Label htmlFor={`${id}-end`}>{t("endDate")}</Label>
|
? undefined
|
||||||
<Input
|
: {
|
||||||
id={`${id}-end`}
|
label: t("newDepartment"),
|
||||||
min={form.watch("startDate")}
|
onClick: () => setCreatingDepartment(true),
|
||||||
type="date"
|
}
|
||||||
{...form.register("endDate")}
|
}
|
||||||
|
disabled={assignment !== undefined || mutation.isPending}
|
||||||
|
id={`${id}-department`}
|
||||||
|
invalid={!!form.formState.errors.departmentId}
|
||||||
|
label={t("department")}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
form.setValue("departmentId", value, { shouldDirty: true, shouldValidate: true })
|
||||||
|
}
|
||||||
|
options={company.departments
|
||||||
|
.filter((item) => item.active || item.id === assignment?.departmentId)
|
||||||
|
.map((department) => ({
|
||||||
|
value: department.id,
|
||||||
|
label: department.name + (department.active ? "" : ` · ${t("inactive")}`),
|
||||||
|
}))}
|
||||||
|
placeholder={t("chooseDepartment")}
|
||||||
|
required
|
||||||
|
value={form.watch("departmentId")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<SwitchField
|
<Label htmlFor={`${id}-position`}>{t("departmentPosition")}</Label>
|
||||||
checked={!form.watch("endDate") && form.watch("isPrimary")}
|
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
|
||||||
disabled={!!form.watch("endDate")}
|
</div>
|
||||||
id={`${id}-primary`}
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
label={t("primaryDepartment")}
|
<div className="space-y-2">
|
||||||
onCheckedChange={(checked) => form.setValue("isPrimary", checked, { shouldDirty: true })}
|
<Label htmlFor={`${id}-start`}>{t("startDate")}</Label>
|
||||||
/>
|
<Input id={`${id}-start`} required type="date" {...form.register("startDate")} />
|
||||||
<p className="text-xs text-muted-foreground">{t("assignmentHelp")}</p>
|
</div>
|
||||||
<CompanyFeedback
|
<div className="space-y-2">
|
||||||
error={mutation.error}
|
<Label htmlFor={`${id}-end`}>{t("endDate")}</Label>
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
<Input
|
||||||
/>
|
id={`${id}-end`}
|
||||||
|
min={form.watch("startDate")}
|
||||||
|
type="date"
|
||||||
|
{...form.register("endDate")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={!form.watch("endDate") && form.watch("isPrimary")}
|
||||||
|
disabled={!!form.watch("endDate")}
|
||||||
|
id={`${id}-primary`}
|
||||||
|
label={t("primaryDepartment")}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
form.setValue("isPrimary", checked, { shouldDirty: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("assignmentHelp")}</p>
|
||||||
|
<CompanyFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
<EntityHelp kind="assignment" />
|
<EntityHelp kind="assignment" />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -44,104 +44,106 @@ export function CertificateConfigurationForm(props: CertificateConfigurationForm
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
fixed
|
||||||
|
? undefined
|
||||||
|
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
||||||
|
}
|
||||||
|
disabled={fixed || !ready}
|
||||||
|
id={`${prefix}-profile`}
|
||||||
|
label={t("mail.profile")}
|
||||||
|
onValueChange={(id) => form.setValue("profileId", id)}
|
||||||
|
options={certificateProfiles(values.profileId, profiles.data?.profiles)}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.profileId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-resource`}>{t("certificates.resource")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
configuration
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
label: t("certificates.newResource"),
|
||||||
|
onClick: () => setQuick("certificate"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={!!configuration || !ready}
|
||||||
|
id={`${prefix}-resource`}
|
||||||
|
label={t("certificates.resource")}
|
||||||
|
onValueChange={(id) => form.setValue("resourceId", id)}
|
||||||
|
options={certificateOptions(
|
||||||
|
values.resourceId,
|
||||||
|
resources.data?.resources,
|
||||||
|
catalog.data?.items
|
||||||
|
)}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.resourceId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-state`}
|
||||||
|
label={t("mail.state")}
|
||||||
|
onValueChange={changeState}
|
||||||
|
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.state}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
<Label htmlFor={`${prefix}-location`}>{t("certificates.location")}</Label>
|
||||||
<SelectWithAction
|
<Input
|
||||||
action={
|
id={`${prefix}-location`}
|
||||||
fixed
|
maxLength={255}
|
||||||
? undefined
|
onChange={(event) => form.setValue("location", event.target.value || null)}
|
||||||
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
value={values.location ?? ""}
|
||||||
}
|
|
||||||
disabled={fixed || !ready}
|
|
||||||
id={`${prefix}-profile`}
|
|
||||||
label={t("mail.profile")}
|
|
||||||
onValueChange={(id) => form.setValue("profileId", id)}
|
|
||||||
options={certificateProfiles(values.profileId, profiles.data?.profiles)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.profileId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-resource`}>{t("certificates.resource")}</Label>
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
<SelectWithAction
|
<Textarea
|
||||||
action={
|
id={`${prefix}-notes`}
|
||||||
configuration
|
maxLength={4000}
|
||||||
? undefined
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
: {
|
rows={3}
|
||||||
label: t("certificates.newResource"),
|
value={values.notes ?? ""}
|
||||||
onClick: () => setQuick("certificate"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disabled={!!configuration || !ready}
|
|
||||||
id={`${prefix}-resource`}
|
|
||||||
label={t("certificates.resource")}
|
|
||||||
onValueChange={(id) => form.setValue("resourceId", id)}
|
|
||||||
options={certificateOptions(
|
|
||||||
values.resourceId,
|
|
||||||
resources.data?.resources,
|
|
||||||
catalog.data?.items
|
|
||||||
)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.resourceId}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
</fieldset>
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
{configuration && (
|
||||||
<SelectWithAction
|
<p className="text-sm text-muted-foreground">{t("certificates.fixed")}</p>
|
||||||
id={`${prefix}-state`}
|
)}
|
||||||
label={t("mail.state")}
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
onValueChange={changeState}
|
<p className="text-sm text-destructive" role="alert">
|
||||||
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
{t("certificates.errors.INVALID_REQUEST")}
|
||||||
placeholder={t("mail.choose")}
|
</p>
|
||||||
required
|
)}
|
||||||
value={values.state}
|
<CertificateFeedback
|
||||||
/>
|
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error}
|
||||||
</div>
|
/>
|
||||||
</div>
|
<ContextHelp
|
||||||
<div className="space-y-2">
|
description={t("certificates.description")}
|
||||||
<Label htmlFor={`${prefix}-location`}>{t("certificates.location")}</Label>
|
examples={t("certificates.examples")}
|
||||||
<Input
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
id={`${prefix}-location`}
|
guidance={t("certificates.guidance")}
|
||||||
maxLength={255}
|
scopeNote={t("certificates.scope")}
|
||||||
onChange={(event) => form.setValue("location", event.target.value || null)}
|
title={t("certificates.help")}
|
||||||
value={values.location ?? ""}
|
/>
|
||||||
/>
|
</FormBody>
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-notes`}
|
|
||||||
maxLength={4000}
|
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
|
||||||
rows={3}
|
|
||||||
value={values.notes ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{configuration && (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("certificates.fixed")}</p>
|
|
||||||
)}
|
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("certificates.errors.INVALID_REQUEST")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CertificateFeedback
|
|
||||||
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error}
|
|
||||||
/>
|
|
||||||
<ContextHelp
|
|
||||||
description={t("certificates.description")}
|
|
||||||
examples={t("certificates.examples")}
|
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
|
||||||
guidance={t("certificates.guidance")}
|
|
||||||
scopeNote={t("certificates.scope")}
|
|
||||||
title={t("certificates.help")}
|
|
||||||
/>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={!ready || mutation.isPending} type="submit">
|
<Button disabled={!ready || mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -20,40 +20,42 @@ export function CompanyForm(props: CompanyFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
{showTitle && (
|
<FormBody>
|
||||||
<h2 className="text-lg font-semibold">{t(company ? "editCompany" : "newCompany")}</h2>
|
{showTitle && (
|
||||||
)}
|
<h2 className="text-lg font-semibold">{t(company ? "editCompany" : "newCompany")}</h2>
|
||||||
<div className="space-y-2">
|
)}
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
<SwitchField
|
</div>
|
||||||
checked={form.watch("active")}
|
<SwitchField
|
||||||
id={`${id}-active`}
|
checked={form.watch("active")}
|
||||||
label={t("activeCompany")}
|
id={`${id}-active`}
|
||||||
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
|
label={t("activeCompany")}
|
||||||
/>
|
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
|
||||||
<CompanyLogoField
|
/>
|
||||||
companyId={savedCompanyId}
|
<CompanyLogoField
|
||||||
disabled={mutation.isPending}
|
companyId={savedCompanyId}
|
||||||
draft={logoDraft}
|
disabled={mutation.isPending}
|
||||||
logoVersion={mutation.data ? mutation.data.company.logoVersion : company?.logoVersion}
|
draft={logoDraft}
|
||||||
name={form.watch("name")}
|
logoVersion={mutation.data ? mutation.data.company.logoVersion : company?.logoVersion}
|
||||||
onChange={setLogoDraft}
|
name={form.watch("name")}
|
||||||
/>
|
onChange={setLogoDraft}
|
||||||
{mutation.isError && partialSave && (
|
/>
|
||||||
<p className="text-sm text-muted-foreground" role="status">
|
{mutation.isError && partialSave && (
|
||||||
{t("logo.partialSave")}
|
<p className="text-sm text-muted-foreground" role="status">
|
||||||
</p>
|
{t("logo.partialSave")}
|
||||||
)}
|
</p>
|
||||||
<CompanyFeedback
|
)}
|
||||||
error={mutation.error}
|
<CompanyFeedback
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
error={mutation.error}
|
||||||
success={mutation.isSuccess}
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
/>
|
success={mutation.isSuccess}
|
||||||
|
/>
|
||||||
|
|
||||||
<EntityHelp kind="company" />
|
<EntityHelp kind="company" />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -17,27 +17,29 @@ export function DepartmentForm(props: DepartmentFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={id}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={id} maxLength={100} required {...form.register("name")} />
|
<Label htmlFor={id}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={id} maxLength={100} required {...form.register("name")} />
|
||||||
<SwitchField
|
</div>
|
||||||
checked={form.watch("active")}
|
<SwitchField
|
||||||
disabled={requireActive}
|
checked={form.watch("active")}
|
||||||
id={`${id}-active`}
|
disabled={requireActive}
|
||||||
label={t("activeDepartment")}
|
id={`${id}-active`}
|
||||||
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
|
label={t("activeDepartment")}
|
||||||
/>
|
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
|
||||||
{department && (
|
/>
|
||||||
<p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p>
|
{department && (
|
||||||
)}
|
<p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p>
|
||||||
<CompanyFeedback
|
)}
|
||||||
error={mutation.error}
|
<CompanyFeedback
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
error={mutation.error}
|
||||||
/>
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
<EntityHelp kind="department" />
|
<EntityHelp kind="department" />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -40,93 +40,95 @@ export function FolderMappingForm(props: FolderMappingFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<FolderMappingTargets
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
company={company}
|
<FolderMappingTargets
|
||||||
fixed={Boolean(mapping)}
|
company={company}
|
||||||
form={form}
|
fixed={Boolean(mapping)}
|
||||||
initialProfileId={initialProfileId}
|
form={form}
|
||||||
items={items}
|
initialProfileId={initialProfileId}
|
||||||
needs={needs}
|
items={items}
|
||||||
onNeedChange={changeNeed}
|
needs={needs}
|
||||||
onQuickCreate={setQuick}
|
onNeedChange={changeNeed}
|
||||||
prefix={prefix}
|
onQuickCreate={setQuick}
|
||||||
profile={profile}
|
prefix={prefix}
|
||||||
profiles={profiles.data?.profiles ?? []}
|
profile={profile}
|
||||||
ready={ready}
|
profiles={profiles.data?.profiles ?? []}
|
||||||
/>
|
ready={ready}
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-path`}>{t("folders.path")}</Label>
|
|
||||||
<Input id={`${prefix}-path`} readOnly value={values.path} />
|
|
||||||
</div>
|
|
||||||
{mapping && currentPath !== values.path && (
|
|
||||||
<div className="rounded-md border p-3 text-sm space-y-2">
|
|
||||||
<p>
|
|
||||||
{t("folders.currentPath")}: {currentPath}
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
onClick={() => form.setValue("path", currentPath)}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t("folders.useCurrentPath")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-letter`}>{t("folders.letter")}</Label>
|
|
||||||
<Input
|
|
||||||
id={`${prefix}-letter`}
|
|
||||||
maxLength={2}
|
|
||||||
onChange={(event) => form.setValue("driveLetter", event.target.value || null)}
|
|
||||||
placeholder="G:"
|
|
||||||
value={values.driveLetter ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
id={`${prefix}-state`}
|
|
||||||
label={t("mail.state")}
|
|
||||||
onValueChange={(state) => {
|
|
||||||
const parsed = FolderMappingInputSchema.shape.state.safeParse(state);
|
|
||||||
if (parsed.success) {
|
|
||||||
form.setValue("state", parsed.data);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={states.map((state) => ({ value: state, label: t(`mail.${state}`) }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.state}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-notes`}
|
|
||||||
maxLength={4000}
|
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
|
||||||
rows={3}
|
|
||||||
value={values.notes ?? ""}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
</fieldset>
|
<Label htmlFor={`${prefix}-path`}>{t("folders.path")}</Label>
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
<Input id={`${prefix}-path`} readOnly value={values.path} />
|
||||||
<p role="alert">{t("folders.errors.INVALID_REQUEST")}</p>
|
</div>
|
||||||
)}
|
{mapping && currentPath !== values.path && (
|
||||||
<FolderFeedback error={mutation.error ?? access.error ?? profiles.error} />
|
<div className="rounded-md border p-3 text-sm space-y-2">
|
||||||
<ContextHelp
|
<p>
|
||||||
description={t("folders.description")}
|
{t("folders.currentPath")}: {currentPath}
|
||||||
examples={t("folders.examples")}
|
</p>
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
<Button
|
||||||
guidance={t("folders.guidance")}
|
onClick={() => form.setValue("path", currentPath)}
|
||||||
scopeNote={t("folders.scope")}
|
type="button"
|
||||||
title={t("folders.help")}
|
variant="outline"
|
||||||
/>
|
>
|
||||||
|
{t("folders.useCurrentPath")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-letter`}>{t("folders.letter")}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-letter`}
|
||||||
|
maxLength={2}
|
||||||
|
onChange={(event) => form.setValue("driveLetter", event.target.value || null)}
|
||||||
|
placeholder="G:"
|
||||||
|
value={values.driveLetter ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-state`}
|
||||||
|
label={t("mail.state")}
|
||||||
|
onValueChange={(state) => {
|
||||||
|
const parsed = FolderMappingInputSchema.shape.state.safeParse(state);
|
||||||
|
if (parsed.success) {
|
||||||
|
form.setValue("state", parsed.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={states.map((state) => ({ value: state, label: t(`mail.${state}`) }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.state}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-notes`}
|
||||||
|
maxLength={4000}
|
||||||
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
|
rows={3}
|
||||||
|
value={values.notes ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p role="alert">{t("folders.errors.INVALID_REQUEST")}</p>
|
||||||
|
)}
|
||||||
|
<FolderFeedback error={mutation.error ?? access.error ?? profiles.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("folders.description")}
|
||||||
|
examples={t("folders.examples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("folders.guidance")}
|
||||||
|
scopeNote={t("folders.scope")}
|
||||||
|
title={t("folders.help")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending || !ready} type="submit">
|
<Button disabled={mutation.isPending || !ready} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -20,53 +20,57 @@ export function InterventionTemplateForm(props: InterventionTemplateFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-description`}>{t("interventionTemplates.description")}</Label>
|
<div className="space-y-2">
|
||||||
<Textarea
|
<Label htmlFor={`${prefix}-description`}>
|
||||||
id={`${prefix}-description`}
|
{t("interventionTemplates.description")}
|
||||||
maxLength={4000}
|
</Label>
|
||||||
rows={3}
|
<Textarea
|
||||||
{...form.register("description")}
|
id={`${prefix}-description`}
|
||||||
|
maxLength={4000}
|
||||||
|
rows={3}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={form.watch("active")}
|
||||||
|
id={`${prefix}-active`}
|
||||||
|
label={t("active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
||||||
/>
|
/>
|
||||||
</div>
|
<h3 className="font-medium">{t("interventionTemplates.steps")}</h3>
|
||||||
<SwitchField
|
{steps.fields.map((step, index) => (
|
||||||
checked={form.watch("active")}
|
<TemplateStepFields
|
||||||
id={`${prefix}-active`}
|
count={steps.fields.length}
|
||||||
label={t("active")}
|
form={form}
|
||||||
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
index={index}
|
||||||
|
key={step.fieldKey}
|
||||||
|
onMove={steps.move}
|
||||||
|
onRemove={steps.remove}
|
||||||
|
prefix={prefix}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
disabled={steps.fields.length >= 100}
|
||||||
|
onClick={() => steps.append(newTemplateStep())}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("interventionTemplates.addStep")}
|
||||||
|
</Button>
|
||||||
|
</fieldset>
|
||||||
|
<InterventionTemplateFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
/>
|
/>
|
||||||
<h3 className="font-medium">{t("interventionTemplates.steps")}</h3>
|
<InterventionTemplateHelp />
|
||||||
{steps.fields.map((step, index) => (
|
</FormBody>
|
||||||
<TemplateStepFields
|
|
||||||
count={steps.fields.length}
|
|
||||||
form={form}
|
|
||||||
index={index}
|
|
||||||
key={step.fieldKey}
|
|
||||||
onMove={steps.move}
|
|
||||||
onRemove={steps.remove}
|
|
||||||
prefix={prefix}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
disabled={steps.fields.length >= 100}
|
|
||||||
onClick={() => steps.append(newTemplateStep())}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t("interventionTemplates.addStep")}
|
|
||||||
</Button>
|
|
||||||
</fieldset>
|
|
||||||
<InterventionTemplateFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<InterventionTemplateHelp />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -52,103 +52,105 @@ export function InterventionForm(props: InterventionFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<InterventionCompanyField
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
companies={data.companies}
|
<InterventionCompanyField
|
||||||
companyId={companyId}
|
companies={data.companies}
|
||||||
fixed={isCompanyFixed(scope, intervention, initialTicket)}
|
|
||||||
onChange={changeCompany}
|
|
||||||
onNew={() => setQuick("company")}
|
|
||||||
prefix={prefix}
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-subject`}>{t("interventions.subject")}</Label>
|
|
||||||
<Input
|
|
||||||
id={`${prefix}-subject`}
|
|
||||||
maxLength={200}
|
|
||||||
required
|
|
||||||
{...form.register("subject")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-ticket`}>{t("interventions.ticket")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={{
|
|
||||||
label: t("tickets.new"),
|
|
||||||
onClick: () => setQuick("ticket"),
|
|
||||||
disabled: !(companyId && lookups.data),
|
|
||||||
}}
|
|
||||||
disabled={!lookups.data}
|
|
||||||
id={`${prefix}-ticket`}
|
|
||||||
label={t("interventions.ticket")}
|
|
||||||
onValueChange={selectTicket}
|
|
||||||
options={
|
|
||||||
lookups.data?.tickets.map((item) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: `${item.reference} · ${item.values.subject}`,
|
|
||||||
})) ?? []
|
|
||||||
}
|
|
||||||
placeholder={t("interventions.internal")}
|
|
||||||
value={values.ticketId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-assignee`}>{t("interventions.assignee")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
id={`${prefix}-assignee`}
|
|
||||||
label={t("interventions.assignee")}
|
|
||||||
onValueChange={(id) => form.setValue("assigneeId", id || null)}
|
|
||||||
options={data.technicians
|
|
||||||
.filter((item) => item.active || item.id === intervention?.values.assigneeId)
|
|
||||||
.map((item) => ({ value: item.id, label: item.name }))}
|
|
||||||
placeholder={t("interventions.unassigned")}
|
|
||||||
value={values.assigneeId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<InterventionWorkFields form={form} prefix={prefix} />
|
|
||||||
{companyId && lookups.isPending && <p role="status">{t("loading")}</p>}
|
|
||||||
<InterventionFeedback error={lookups.error} />
|
|
||||||
{lookups.isError && (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
lookups.refetch().catch(() => undefined);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t("retry")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{lookups.data && (
|
|
||||||
<TicketTargets
|
|
||||||
companyId={companyId}
|
companyId={companyId}
|
||||||
csrfToken={csrfToken}
|
fixed={isCompanyFixed(scope, intervention, initialTicket)}
|
||||||
lookupKey={interventionKeys.lookups(companyId)}
|
onChange={changeCompany}
|
||||||
lookups={lookups.data}
|
onNew={() => setQuick("company")}
|
||||||
onChange={(affected) => form.setValue("affected", affected)}
|
prefix={prefix}
|
||||||
values={values.affected}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
<InterventionProcedureFields
|
<Label htmlFor={`${prefix}-subject`}>{t("interventions.subject")}</Label>
|
||||||
applied={applied}
|
<Input
|
||||||
intervention={intervention}
|
id={`${prefix}-subject`}
|
||||||
lookups={lookups.data}
|
maxLength={200}
|
||||||
onApply={applyTemplate}
|
required
|
||||||
onNewTemplate={() => setQuick("template")}
|
{...form.register("subject")}
|
||||||
onSelect={setSelectedTemplate}
|
/>
|
||||||
prefix={prefix}
|
</div>
|
||||||
selectedTemplate={selectedTemplate}
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-ticket`}>{t("interventions.ticket")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={{
|
||||||
|
label: t("tickets.new"),
|
||||||
|
onClick: () => setQuick("ticket"),
|
||||||
|
disabled: !(companyId && lookups.data),
|
||||||
|
}}
|
||||||
|
disabled={!lookups.data}
|
||||||
|
id={`${prefix}-ticket`}
|
||||||
|
label={t("interventions.ticket")}
|
||||||
|
onValueChange={selectTicket}
|
||||||
|
options={
|
||||||
|
lookups.data?.tickets.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: `${item.reference} · ${item.values.subject}`,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
placeholder={t("interventions.internal")}
|
||||||
|
value={values.ticketId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-assignee`}>{t("interventions.assignee")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-assignee`}
|
||||||
|
label={t("interventions.assignee")}
|
||||||
|
onValueChange={(id) => form.setValue("assigneeId", id || null)}
|
||||||
|
options={data.technicians
|
||||||
|
.filter((item) => item.active || item.id === intervention?.values.assigneeId)
|
||||||
|
.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
|
placeholder={t("interventions.unassigned")}
|
||||||
|
value={values.assigneeId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<InterventionWorkFields form={form} prefix={prefix} />
|
||||||
|
{companyId && lookups.isPending && <p role="status">{t("loading")}</p>}
|
||||||
|
<InterventionFeedback error={lookups.error} />
|
||||||
|
{lookups.isError && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
lookups.refetch().catch(() => undefined);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{lookups.data && (
|
||||||
|
<TicketTargets
|
||||||
|
companyId={companyId}
|
||||||
|
csrfToken={csrfToken}
|
||||||
|
lookupKey={interventionKeys.lookups(companyId)}
|
||||||
|
lookups={lookups.data}
|
||||||
|
onChange={(affected) => form.setValue("affected", affected)}
|
||||||
|
values={values.affected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<InterventionProcedureFields
|
||||||
|
applied={applied}
|
||||||
|
intervention={intervention}
|
||||||
|
lookups={lookups.data}
|
||||||
|
onApply={applyTemplate}
|
||||||
|
onNewTemplate={() => setQuick("template")}
|
||||||
|
onSelect={setSelectedTemplate}
|
||||||
|
prefix={prefix}
|
||||||
|
selectedTemplate={selectedTemplate}
|
||||||
|
/>
|
||||||
|
<InterventionSteps form={form} prefix={prefix} />
|
||||||
|
</fieldset>
|
||||||
|
<InterventionFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
/>
|
/>
|
||||||
<InterventionSteps form={form} prefix={prefix} />
|
<InterventionHelp />
|
||||||
</fieldset>
|
</FormBody>
|
||||||
<InterventionFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<InterventionHelp />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isPending || !companyId || !lookups.data || lookups.isError}
|
disabled={mutation.isPending || !companyId || !lookups.data || lookups.isError}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Label } from "@repo/shadcn-ui/components/label";
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
import { Textarea } from "@repo/shadcn-ui/components/textarea";
|
import { Textarea } from "@repo/shadcn-ui/components/textarea";
|
||||||
@ -44,71 +44,73 @@ export function MailConfigurationForm(props: MailConfigurationFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={disabled}>
|
<FormBody>
|
||||||
{legacy && (
|
<fieldset className="space-y-4" disabled={disabled}>
|
||||||
<p className="rounded-md border p-3 text-sm">
|
{legacy && (
|
||||||
{t("mail.legacyLink")} {legacy.address} · {personLabel(legacy.personId)} ·{" "}
|
<p className="rounded-md border p-3 text-sm">
|
||||||
{assetLabel(legacy.assetId)}
|
{t("mail.legacyLink")} {legacy.address} · {personLabel(legacy.personId)} ·{" "}
|
||||||
</p>
|
{assetLabel(legacy.assetId)}
|
||||||
)}
|
</p>
|
||||||
<MailConfigurationTargets
|
|
||||||
fixed={fixed}
|
|
||||||
form={form}
|
|
||||||
initialProfileId={initialProfileId}
|
|
||||||
mailboxes={mailboxOptions(
|
|
||||||
mailboxes.data?.mailboxes ?? [],
|
|
||||||
values.mailboxId,
|
|
||||||
legacy
|
|
||||||
).map((item) => ({ value: item.id, label: item.values.address }))}
|
|
||||||
onQuickCreate={setQuick}
|
|
||||||
prefix={prefix}
|
|
||||||
profiles={profileOptions(profiles.data?.profiles ?? [], values.profileId, legacy).map(
|
|
||||||
(item) => ({
|
|
||||||
value: item.id,
|
|
||||||
label:
|
|
||||||
profileLabel(item.id) +
|
|
||||||
" · " +
|
|
||||||
personLabel(item.values.personId) +
|
|
||||||
" · " +
|
|
||||||
assetLabel(item.values.assetId),
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
ready={ready}
|
<MailConfigurationTargets
|
||||||
states={states}
|
fixed={fixed}
|
||||||
/>
|
form={form}
|
||||||
<MailboxUsagePrompt
|
initialProfileId={initialProfileId}
|
||||||
authorized={authorized ?? false}
|
mailboxes={mailboxOptions(
|
||||||
mailbox={selectedMailbox}
|
mailboxes.data?.mailboxes ?? [],
|
||||||
onRegister={() => setQuick("use")}
|
values.mailboxId,
|
||||||
profile={selectedProfile}
|
legacy
|
||||||
/>
|
).map((item) => ({ value: item.id, label: item.values.address }))}
|
||||||
<div className="space-y-2">
|
onQuickCreate={setQuick}
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
prefix={prefix}
|
||||||
<Textarea
|
profiles={profileOptions(profiles.data?.profiles ?? [], values.profileId, legacy).map(
|
||||||
id={`${prefix}-notes`}
|
(item) => ({
|
||||||
maxLength={4000}
|
value: item.id,
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
label:
|
||||||
rows={3}
|
profileLabel(item.id) +
|
||||||
value={values.notes ?? ""}
|
" · " +
|
||||||
|
personLabel(item.values.personId) +
|
||||||
|
" · " +
|
||||||
|
assetLabel(item.values.assetId),
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
ready={ready}
|
||||||
|
states={states}
|
||||||
/>
|
/>
|
||||||
</div>
|
<MailboxUsagePrompt
|
||||||
</fieldset>
|
authorized={authorized ?? false}
|
||||||
{fixed && <p className="text-sm text-muted-foreground">{t("mail.fixed")}</p>}
|
mailbox={selectedMailbox}
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
onRegister={() => setQuick("use")}
|
||||||
<p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
|
profile={selectedProfile}
|
||||||
)}
|
/>
|
||||||
<MailFeedback
|
<div className="space-y-2">
|
||||||
error={mutation.error ?? profiles.error ?? mailboxes.error ?? inventory.error}
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
/>
|
<Textarea
|
||||||
<ContextHelp
|
id={`${prefix}-notes`}
|
||||||
description={t("mail.description")}
|
maxLength={4000}
|
||||||
examples={t("mail.examples")}
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
rows={3}
|
||||||
guidance={t("mail.guidance")}
|
value={values.notes ?? ""}
|
||||||
scopeNote={t("mail.scope")}
|
/>
|
||||||
title={t("mail.help")}
|
</div>
|
||||||
/>
|
</fieldset>
|
||||||
|
{fixed && <p className="text-sm text-muted-foreground">{t("mail.fixed")}</p>}
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
|
||||||
|
)}
|
||||||
|
<MailFeedback
|
||||||
|
error={mutation.error ?? profiles.error ?? mailboxes.error ?? inventory.error}
|
||||||
|
/>
|
||||||
|
<ContextHelp
|
||||||
|
description={t("mail.description")}
|
||||||
|
examples={t("mail.examples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("mail.guidance")}
|
||||||
|
scopeNote={t("mail.scope")}
|
||||||
|
title={t("mail.help")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={disabled || !ready} type="submit">
|
<Button disabled={disabled || !ready} type="submit">
|
||||||
{t(disabled ? "saving" : "save")}
|
{t(disabled ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -31,82 +31,84 @@ export function MailboxUseForm(props: MailboxUseFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-mailbox`}>{t("mailbox.titleSingle")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
canCreate(!!use, initialMailboxId, allowQuickCreate)
|
|
||||||
? { label: t("mailbox.new"), onClick: () => setQuick("mailbox") }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={!!use || !!initialMailboxId}
|
|
||||||
id={`${prefix}-mailbox`}
|
|
||||||
label={t("mailbox.titleSingle")}
|
|
||||||
onValueChange={(id) => form.setValue("mailboxId", id)}
|
|
||||||
options={(query.data?.mailboxes ?? [])
|
|
||||||
.filter((item) => item.values.active || item.id === values.mailboxId)
|
|
||||||
.map((item) => ({ value: item.id, label: item.values.address }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.mailboxId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("mail.person")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
canCreate(!!use, initialPersonId, allowQuickCreate)
|
|
||||||
? { label: t("newPerson"), onClick: () => setQuick("person") }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={!!use || !!initialPersonId}
|
|
||||||
id={`${prefix}-person`}
|
|
||||||
label={t("mail.person")}
|
|
||||||
onValueChange={(id) => form.setValue("personId", id)}
|
|
||||||
options={company.people
|
|
||||||
.filter((item) => item.active || item.id === values.personId)
|
|
||||||
.map((item) => ({ value: item.id, label: item.name }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.personId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label>
|
<Label htmlFor={`${prefix}-mailbox`}>{t("mailbox.titleSingle")}</Label>
|
||||||
<Input
|
<SelectWithAction
|
||||||
disabled={!!use}
|
action={
|
||||||
id={`${prefix}-start`}
|
canCreate(!!use, initialMailboxId, allowQuickCreate)
|
||||||
|
? { label: t("mailbox.new"), onClick: () => setQuick("mailbox") }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
disabled={!!use || !!initialMailboxId}
|
||||||
|
id={`${prefix}-mailbox`}
|
||||||
|
label={t("mailbox.titleSingle")}
|
||||||
|
onValueChange={(id) => form.setValue("mailboxId", id)}
|
||||||
|
options={(query.data?.mailboxes ?? [])
|
||||||
|
.filter((item) => item.values.active || item.id === values.mailboxId)
|
||||||
|
.map((item) => ({ value: item.id, label: item.values.address }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
required
|
required
|
||||||
type="date"
|
value={values.mailboxId}
|
||||||
{...form.register("startDate")}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-end`}>{t("mailbox.end")}</Label>
|
<Label htmlFor={`${prefix}-person`}>{t("mail.person")}</Label>
|
||||||
<Input
|
<SelectWithAction
|
||||||
id={`${prefix}-end`}
|
action={
|
||||||
onChange={(event) => form.setValue("endDate", event.target.value || null)}
|
canCreate(!!use, initialPersonId, allowQuickCreate)
|
||||||
required={!!use}
|
? { label: t("newPerson"), onClick: () => setQuick("person") }
|
||||||
type="date"
|
: undefined
|
||||||
value={values.endDate ?? ""}
|
}
|
||||||
|
disabled={!!use || !!initialPersonId}
|
||||||
|
id={`${prefix}-person`}
|
||||||
|
label={t("mail.person")}
|
||||||
|
onValueChange={(id) => form.setValue("personId", id)}
|
||||||
|
options={company.people
|
||||||
|
.filter((item) => item.active || item.id === values.personId)
|
||||||
|
.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.personId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
</fieldset>
|
<div className="space-y-2">
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
<Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label>
|
||||||
<p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
|
<Input
|
||||||
)}
|
disabled={!!use}
|
||||||
<MailFeedback error={mutation.error ?? query.error} />
|
id={`${prefix}-start`}
|
||||||
<ContextHelp
|
required
|
||||||
description={t("mailbox.useDescription")}
|
type="date"
|
||||||
examples={t("mailbox.useExamples")}
|
{...form.register("startDate")}
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
/>
|
||||||
guidance={t("mailbox.useGuidance")}
|
</div>
|
||||||
title={t("mailbox.useHelp")}
|
<div className="space-y-2">
|
||||||
/>
|
<Label htmlFor={`${prefix}-end`}>{t("mailbox.end")}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-end`}
|
||||||
|
onChange={(event) => form.setValue("endDate", event.target.value || null)}
|
||||||
|
required={!!use}
|
||||||
|
type="date"
|
||||||
|
value={values.endDate ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
|
||||||
|
)}
|
||||||
|
<MailFeedback error={mutation.error ?? query.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("mailbox.useDescription")}
|
||||||
|
examples={t("mailbox.useExamples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("mailbox.useGuidance")}
|
||||||
|
title={t("mailbox.useHelp")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending || !query.data} type="submit">
|
<Button disabled={mutation.isPending || !query.data} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export function PersonEditorDialog({
|
|||||||
formActions
|
formActions
|
||||||
onOpenChange={onOpenChange}
|
onOpenChange={onOpenChange}
|
||||||
open={open}
|
open={open}
|
||||||
|
size="medium"
|
||||||
title={t(formProps.person ? "editPerson" : "newPerson")}
|
title={t(formProps.person ? "editPerson" : "newPerson")}
|
||||||
wide
|
|
||||||
>
|
>
|
||||||
<PersonForm {...formProps} key={formProps.person?.id ?? "new"} />
|
<PersonForm {...formProps} key={formProps.person?.id ?? "new"} />
|
||||||
</FormDialog>
|
</FormDialog>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -17,49 +17,51 @@ export function PersonForm(props: PersonFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="max-w-2xl space-y-6" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-position`}>{t("generalPosition")}</Label>
|
||||||
|
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-email`}>{t("emailOptional")}</Label>
|
||||||
|
<Input id={`${id}-email`} maxLength={254} type="email" {...form.register("email")} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-phone`}>{t("phoneOptional")}</Label>
|
||||||
|
<Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<SwitchField
|
||||||
<Label htmlFor={`${id}-position`}>{t("generalPosition")}</Label>
|
checked={form.watch("active")}
|
||||||
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
|
disabled={requireActive}
|
||||||
</div>
|
id={`${id}-active`}
|
||||||
<div className="space-y-2">
|
label={t("activePerson")}
|
||||||
<Label htmlFor={`${id}-email`}>{t("emailOptional")}</Label>
|
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
||||||
<Input id={`${id}-email`} maxLength={254} type="email" {...form.register("email")} />
|
/>
|
||||||
</div>
|
<SwitchField
|
||||||
<div className="space-y-2">
|
checked={form.watch("isContact")}
|
||||||
<Label htmlFor={`${id}-phone`}>{t("phoneOptional")}</Label>
|
id={`${id}-contact`}
|
||||||
<Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} />
|
label={t("isContact")}
|
||||||
</div>
|
onCheckedChange={(isContact) =>
|
||||||
</div>
|
form.setValue("isContact", isContact, { shouldDirty: true })
|
||||||
<SwitchField
|
}
|
||||||
checked={form.watch("active")}
|
/>
|
||||||
disabled={requireActive}
|
{person?.active && !form.watch("active") && (
|
||||||
id={`${id}-active`}
|
<p className="text-sm text-muted-foreground">{t("deactivationHelp")}</p>
|
||||||
label={t("activePerson")}
|
)}
|
||||||
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
<CompanyFeedback
|
||||||
/>
|
error={mutation.error}
|
||||||
<SwitchField
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
checked={form.watch("isContact")}
|
/>
|
||||||
id={`${id}-contact`}
|
<p className="text-sm text-muted-foreground">{t("personHelp")}</p>
|
||||||
label={t("isContact")}
|
<EntityHelp kind="person" />
|
||||||
onCheckedChange={(isContact) =>
|
</FormBody>
|
||||||
form.setValue("isContact", isContact, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{person?.active && !form.watch("active") && (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("deactivationHelp")}</p>
|
|
||||||
)}
|
|
||||||
<CompanyFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">{t("personHelp")}</p>
|
|
||||||
<EntityHelp kind="person" />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -47,118 +47,121 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<div className="space-y-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
|
||||||
action={
|
<SelectWithAction
|
||||||
profileFixed
|
action={
|
||||||
? undefined
|
profileFixed
|
||||||
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
? undefined
|
||||||
}
|
: { label: t("profiles.new"), onClick: () => setQuick("profile") }
|
||||||
disabled={profileFixed || !ready}
|
|
||||||
id={`${prefix}-profile`}
|
|
||||||
label={t("mail.profile")}
|
|
||||||
onValueChange={(id) => {
|
|
||||||
form.setValue("profileId", id);
|
|
||||||
form.setValue("printerId", "");
|
|
||||||
}}
|
|
||||||
options={profileOptions(profiles.data?.profiles ?? [], values.profileId)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.profileId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-printer`}>{t("printers.printer")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
configuration
|
|
||||||
? undefined
|
|
||||||
: { label: t("printers.newAsset"), onClick: () => setQuick("printer") }
|
|
||||||
}
|
|
||||||
disabled={!!configuration || !ready || !profile}
|
|
||||||
id={`${prefix}-printer`}
|
|
||||||
label={t("printers.printer")}
|
|
||||||
onValueChange={(id) => form.setValue("printerId", id)}
|
|
||||||
options={printerOptions(
|
|
||||||
inventory.data?.assets ?? [],
|
|
||||||
profile?.values.assetId,
|
|
||||||
values.printerId
|
|
||||||
)}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.printerId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-connection`}>{t("printers.connection")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
id={`${prefix}-connection`}
|
|
||||||
label={t("printers.connection")}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const parsed = PrinterConfigurationInputSchema.shape.connection.safeParse(value);
|
|
||||||
if (parsed.success) {
|
|
||||||
form.setValue("connection", parsed.data);
|
|
||||||
}
|
}
|
||||||
}}
|
disabled={profileFixed || !ready}
|
||||||
options={["usb", "network"].map((value) => ({
|
id={`${prefix}-profile`}
|
||||||
value,
|
label={t("mail.profile")}
|
||||||
label: t(`printers.${value}`),
|
onValueChange={(id) => {
|
||||||
}))}
|
form.setValue("profileId", id);
|
||||||
placeholder={t("mail.choose")}
|
form.setValue("printerId", "");
|
||||||
required
|
}}
|
||||||
value={values.connection}
|
options={profileOptions(profiles.data?.profiles ?? [], values.profileId)}
|
||||||
/>
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.profileId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-printer`}>{t("printers.printer")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={
|
||||||
|
configuration
|
||||||
|
? undefined
|
||||||
|
: { label: t("printers.newAsset"), onClick: () => setQuick("printer") }
|
||||||
|
}
|
||||||
|
disabled={!!configuration || !ready || !profile}
|
||||||
|
id={`${prefix}-printer`}
|
||||||
|
label={t("printers.printer")}
|
||||||
|
onValueChange={(id) => form.setValue("printerId", id)}
|
||||||
|
options={printerOptions(
|
||||||
|
inventory.data?.assets ?? [],
|
||||||
|
profile?.values.assetId,
|
||||||
|
values.printerId
|
||||||
|
)}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.printerId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-connection`}>{t("printers.connection")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-connection`}
|
||||||
|
label={t("printers.connection")}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const parsed =
|
||||||
|
PrinterConfigurationInputSchema.shape.connection.safeParse(value);
|
||||||
|
if (parsed.success) {
|
||||||
|
form.setValue("connection", parsed.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={["usb", "network"].map((value) => ({
|
||||||
|
value,
|
||||||
|
label: t(`printers.${value}`),
|
||||||
|
}))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.connection}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-state`}
|
||||||
|
label={t("mail.state")}
|
||||||
|
onValueChange={changeState}
|
||||||
|
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.state}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<SwitchField
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
checked={values.isDefault}
|
||||||
<SelectWithAction
|
disabled={!["pending", "configured"].includes(values.state)}
|
||||||
id={`${prefix}-state`}
|
id={`${prefix}-default`}
|
||||||
label={t("mail.state")}
|
label={t("printers.default")}
|
||||||
onValueChange={changeState}
|
onCheckedChange={(isDefault) => form.setValue("isDefault", isDefault)}
|
||||||
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.state}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SwitchField
|
|
||||||
checked={values.isDefault}
|
|
||||||
disabled={!["pending", "configured"].includes(values.state)}
|
|
||||||
id={`${prefix}-default`}
|
|
||||||
label={t("printers.default")}
|
|
||||||
onCheckedChange={(isDefault) => form.setValue("isDefault", isDefault)}
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-notes`}
|
|
||||||
maxLength={4000}
|
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
|
||||||
rows={3}
|
|
||||||
value={values.notes ?? ""}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
</fieldset>
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
{configuration && <p className="text-sm text-muted-foreground">{t("printers.fixed")}</p>}
|
<Textarea
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
id={`${prefix}-notes`}
|
||||||
<p className="text-sm text-destructive" role="alert">
|
maxLength={4000}
|
||||||
{t("printers.errors.INVALID_REQUEST")}
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
</p>
|
rows={3}
|
||||||
)}
|
value={values.notes ?? ""}
|
||||||
<PrinterFeedback error={mutation.error ?? profiles.error ?? inventory.error} />
|
/>
|
||||||
<ContextHelp
|
</div>
|
||||||
description={t("printers.description")}
|
</fieldset>
|
||||||
examples={t("printers.examples")}
|
{configuration && <p className="text-sm text-muted-foreground">{t("printers.fixed")}</p>}
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
guidance={t("printers.guidance")}
|
<p className="text-sm text-destructive" role="alert">
|
||||||
scopeNote={t("printers.scope")}
|
{t("printers.errors.INVALID_REQUEST")}
|
||||||
title={t("printers.help")}
|
</p>
|
||||||
/>
|
)}
|
||||||
|
<PrinterFeedback error={mutation.error ?? profiles.error ?? inventory.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("printers.description")}
|
||||||
|
examples={t("printers.examples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("printers.guidance")}
|
||||||
|
scopeNote={t("printers.scope")}
|
||||||
|
title={t("printers.help")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={!ready || mutation.isPending} type="submit">
|
<Button disabled={!ready || mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -18,58 +18,65 @@ export function ProcessTemplateForm(props: ProcessTemplateFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-kind`}>{t("processes.kind")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={`${prefix}-kind`}>{t("processes.kind")}</Label>
|
||||||
id={`${prefix}-kind`}
|
<SelectWithAction
|
||||||
label={t("processes.kind")}
|
id={`${prefix}-kind`}
|
||||||
onValueChange={(value) => {
|
label={t("processes.kind")}
|
||||||
const parsed = ProcessTemplateInputSchema.shape.kind.safeParse(value);
|
onValueChange={(value) => {
|
||||||
if (parsed.success) {
|
const parsed = ProcessTemplateInputSchema.shape.kind.safeParse(value);
|
||||||
form.setValue("kind", parsed.data);
|
if (parsed.success) {
|
||||||
}
|
form.setValue("kind", parsed.data);
|
||||||
}}
|
}
|
||||||
options={["onboarding", "change", "offboarding"].map((value) => ({
|
}}
|
||||||
value,
|
options={["onboarding", "change", "offboarding"].map((value) => ({
|
||||||
label: t(`processes.${value}`),
|
value,
|
||||||
}))}
|
label: t(`processes.${value}`),
|
||||||
placeholder={t("mail.choose")}
|
}))}
|
||||||
value={form.watch("kind")}
|
placeholder={t("mail.choose")}
|
||||||
|
value={form.watch("kind")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-instructions`}>{t("processes.instructions")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-instructions`}
|
||||||
|
maxLength={16100}
|
||||||
|
required
|
||||||
|
rows={8}
|
||||||
|
{...form.register("lines")}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">{t("processes.instructionsHelp")}</p>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={form.watch("active")}
|
||||||
|
id={`${prefix}-active`}
|
||||||
|
label={t("active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
<Label htmlFor={`${prefix}-instructions`}>{t("processes.instructions")}</Label>
|
<Textarea
|
||||||
<Textarea
|
id={`${prefix}-notes`}
|
||||||
id={`${prefix}-instructions`}
|
maxLength={4000}
|
||||||
maxLength={16100}
|
rows={3}
|
||||||
required
|
{...form.register("notes")}
|
||||||
rows={8}
|
/>
|
||||||
{...form.register("lines")}
|
</div>
|
||||||
/>
|
</fieldset>
|
||||||
<p className="text-sm text-muted-foreground">{t("processes.instructionsHelp")}</p>
|
<ProcessFeedback
|
||||||
</div>
|
error={mutation.error}
|
||||||
<SwitchField
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
checked={form.watch("active")}
|
|
||||||
id={`${prefix}-active`}
|
|
||||||
label={t("active")}
|
|
||||||
onCheckedChange={(active) => form.setValue("active", active)}
|
|
||||||
/>
|
/>
|
||||||
<div className="space-y-2">
|
<ProcessHelp template />
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
</FormBody>
|
||||||
<Textarea id={`${prefix}-notes`} maxLength={4000} rows={3} {...form.register("notes")} />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
<ProcessFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<ProcessHelp template />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -20,73 +20,75 @@ export function ProcessStartForm(props: ProcessStartFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("profiles.person")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={{ label: t("newPerson"), onClick: () => setQuick("person") }}
|
|
||||||
id={`${prefix}-person`}
|
|
||||||
label={t("profiles.person")}
|
|
||||||
onValueChange={(id) => form.setValue("personId", id)}
|
|
||||||
options={company.people.map((p) => ({ value: p.id, label: p.name }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.personId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-template`}>{t("processes.template")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={{ label: t("processes.newTemplate"), onClick: () => setQuick("template") }}
|
|
||||||
disabled={!query.data || query.isError}
|
|
||||||
id={`${prefix}-template`}
|
|
||||||
label={t("processes.template")}
|
|
||||||
onValueChange={(id) => form.setValue("templateId", id)}
|
|
||||||
options={(query.data?.templates ?? [])
|
|
||||||
.filter((t) => t.values.active)
|
|
||||||
.map((t) => ({ value: t.id, label: t.values.name }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={values.templateId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{template && (
|
|
||||||
<div className="rounded-md border p-3 text-sm">
|
|
||||||
<p className="font-medium">{t(`processes.${template.values.kind}`)}</p>
|
|
||||||
<ol className="list-decimal pl-5">
|
|
||||||
{template.values.instructions.map((i) => (
|
|
||||||
<li key={i}>{i}</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
{template.values.kind !== "onboarding" && (
|
|
||||||
<p className="mt-2">
|
|
||||||
{review.isPending
|
|
||||||
? t("loading")
|
|
||||||
: t("processes.reviewCount", { count: review.data?.review.length ?? 0 })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor={`${prefix}-person`}>{t("profiles.person")}</Label>
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
<SelectWithAction
|
||||||
<Textarea
|
action={{ label: t("newPerson"), onClick: () => setQuick("person") }}
|
||||||
id={`${prefix}-notes`}
|
id={`${prefix}-person`}
|
||||||
maxLength={4000}
|
label={t("profiles.person")}
|
||||||
rows={3}
|
onValueChange={(id) => form.setValue("personId", id)}
|
||||||
{...form.register("notes")}
|
options={company.people.map((p) => ({ value: p.id, label: p.name }))}
|
||||||
/>
|
placeholder={t("mail.choose")}
|
||||||
</div>
|
required
|
||||||
</fieldset>
|
value={values.personId}
|
||||||
<ProcessFeedback
|
/>
|
||||||
error={mutation.error ?? query.error ?? review.error}
|
</div>
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
<div className="space-y-2">
|
||||||
/>
|
<Label htmlFor={`${prefix}-template`}>{t("processes.template")}</Label>
|
||||||
<ProcessHelp />
|
<SelectWithAction
|
||||||
|
action={{ label: t("processes.newTemplate"), onClick: () => setQuick("template") }}
|
||||||
|
disabled={!query.data || query.isError}
|
||||||
|
id={`${prefix}-template`}
|
||||||
|
label={t("processes.template")}
|
||||||
|
onValueChange={(id) => form.setValue("templateId", id)}
|
||||||
|
options={(query.data?.templates ?? [])
|
||||||
|
.filter((t) => t.values.active)
|
||||||
|
.map((t) => ({ value: t.id, label: t.values.name }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.templateId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{template && (
|
||||||
|
<div className="rounded-md border p-3 text-sm">
|
||||||
|
<p className="font-medium">{t(`processes.${template.values.kind}`)}</p>
|
||||||
|
<ol className="list-decimal pl-5">
|
||||||
|
{template.values.instructions.map((i) => (
|
||||||
|
<li key={i}>{i}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{template.values.kind !== "onboarding" && (
|
||||||
|
<p className="mt-2">
|
||||||
|
{review.isPending
|
||||||
|
? t("loading")
|
||||||
|
: t("processes.reviewCount", { count: review.data?.review.length ?? 0 })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-notes`}
|
||||||
|
maxLength={4000}
|
||||||
|
rows={3}
|
||||||
|
{...form.register("notes")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<ProcessFeedback
|
||||||
|
error={mutation.error ?? query.error ?? review.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
/>
|
||||||
|
<ProcessHelp />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -20,45 +20,52 @@ export function ProcessEditForm(props: ProcessEditFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
id={`${prefix}-state`}
|
<SelectWithAction
|
||||||
label={t("mail.state")}
|
id={`${prefix}-state`}
|
||||||
onValueChange={(value) => {
|
label={t("mail.state")}
|
||||||
const parsed = UpdateProcessInputSchema.shape.state.safeParse(value);
|
onValueChange={(value) => {
|
||||||
if (parsed.success) {
|
const parsed = UpdateProcessInputSchema.shape.state.safeParse(value);
|
||||||
form.setValue("state", parsed.data);
|
if (parsed.success) {
|
||||||
}
|
form.setValue("state", parsed.data);
|
||||||
}}
|
}
|
||||||
options={["open", "completed", "cancelled"].map((value) => ({
|
}}
|
||||||
value,
|
options={["open", "completed", "cancelled"].map((value) => ({
|
||||||
label: t(`processes.${value}`),
|
value,
|
||||||
}))}
|
label: t(`processes.${value}`),
|
||||||
placeholder={t("mail.choose")}
|
}))}
|
||||||
value={form.watch("state")}
|
placeholder={t("mail.choose")}
|
||||||
/>
|
value={form.watch("state")}
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
<div className="space-y-2">
|
||||||
<Textarea id={`${prefix}-notes`} maxLength={4000} rows={3} {...form.register("notes")} />
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
</div>
|
<Textarea
|
||||||
<p className="text-sm text-muted-foreground">{t("processes.resolutionHelp")}</p>
|
id={`${prefix}-notes`}
|
||||||
{process.values.tasks.map((task, index) => (
|
maxLength={4000}
|
||||||
<ProcessTaskField form={form} index={index} key={task.id} task={task} />
|
rows={3}
|
||||||
))}
|
{...form.register("notes")}
|
||||||
</fieldset>
|
/>
|
||||||
<ProcessFeedback
|
</div>
|
||||||
error={mutation.error}
|
<p className="text-sm text-muted-foreground">{t("processes.resolutionHelp")}</p>
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
{process.values.tasks.map((task, index) => (
|
||||||
/>
|
<ProcessTaskField form={form} index={index} key={task.id} task={task} />
|
||||||
<ProcessHelp />
|
))}
|
||||||
|
</fieldset>
|
||||||
|
<ProcessFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
/>
|
||||||
|
<ProcessHelp />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -40,101 +40,103 @@ export function RemoteConnectionForm(props: RemoteConnectionFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("remote.person")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
personFixed
|
|
||||||
? undefined
|
|
||||||
: { label: t("newPerson"), onClick: () => setQuick("person") }
|
|
||||||
}
|
|
||||||
disabled={personFixed}
|
|
||||||
id={`${prefix}-person`}
|
|
||||||
label={t("remote.person")}
|
|
||||||
onValueChange={(id) => form.setValue("personId", id)}
|
|
||||||
options={company.people.map((p) => ({ value: p.id, label: p.name }))}
|
|
||||||
placeholder={t("mail.choose")}
|
|
||||||
required
|
|
||||||
value={v.personId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<RemoteEndpointFields
|
|
||||||
assetOptions={assetOptions}
|
|
||||||
company={company}
|
|
||||||
fixed={fixed}
|
|
||||||
form={form}
|
|
||||||
prefix={prefix}
|
|
||||||
ready={ready}
|
|
||||||
resourceOptions={resourceOptions}
|
|
||||||
setQuick={setQuick}
|
|
||||||
/>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${prefix}-method`}>{t("remote.method")}</Label>
|
<Label htmlFor={`${prefix}-person`}>{t("remote.person")}</Label>
|
||||||
<Input
|
|
||||||
id={`${prefix}-method`}
|
|
||||||
list={`${prefix}-methods`}
|
|
||||||
maxLength={80}
|
|
||||||
placeholder={t("remote.methodExample")}
|
|
||||||
required
|
|
||||||
{...form.register("method")}
|
|
||||||
/>
|
|
||||||
<datalist id={`${prefix}-methods`}>
|
|
||||||
{["VPN", "RDP", "AnyDesk", "TeamViewer", "Supremo"].map((method) => (
|
|
||||||
<option key={method} value={method} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
|
||||||
<SelectWithAction
|
<SelectWithAction
|
||||||
id={`${prefix}-state`}
|
action={
|
||||||
label={t("mail.state")}
|
personFixed
|
||||||
onValueChange={(value) => {
|
? undefined
|
||||||
const parsed = RemoteConnectionInputSchema.shape.state.safeParse(value);
|
: { label: t("newPerson"), onClick: () => setQuick("person") }
|
||||||
if (parsed.success) {
|
}
|
||||||
form.setValue("state", parsed.data);
|
disabled={personFixed}
|
||||||
}
|
id={`${prefix}-person`}
|
||||||
}}
|
label={t("remote.person")}
|
||||||
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
onValueChange={(id) => form.setValue("personId", id)}
|
||||||
|
options={company.people.map((p) => ({ value: p.id, label: p.name }))}
|
||||||
placeholder={t("mail.choose")}
|
placeholder={t("mail.choose")}
|
||||||
value={v.state}
|
required
|
||||||
|
value={v.personId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<SwitchField
|
<RemoteEndpointFields
|
||||||
checked={v.requiresVpn}
|
assetOptions={assetOptions}
|
||||||
id={`${prefix}-vpn`}
|
company={company}
|
||||||
label={t("remote.requiresVpn")}
|
fixed={fixed}
|
||||||
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
|
form={form}
|
||||||
/>
|
prefix={prefix}
|
||||||
<div className="space-y-2">
|
ready={ready}
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
resourceOptions={resourceOptions}
|
||||||
<Textarea
|
setQuick={setQuick}
|
||||||
id={`${prefix}-notes`}
|
/>
|
||||||
maxLength={4000}
|
<div className="space-y-2">
|
||||||
rows={3}
|
<Label htmlFor={`${prefix}-method`}>{t("remote.method")}</Label>
|
||||||
{...form.register("notes")}
|
<Input
|
||||||
|
id={`${prefix}-method`}
|
||||||
|
list={`${prefix}-methods`}
|
||||||
|
maxLength={80}
|
||||||
|
placeholder={t("remote.methodExample")}
|
||||||
|
required
|
||||||
|
{...form.register("method")}
|
||||||
|
/>
|
||||||
|
<datalist id={`${prefix}-methods`}>
|
||||||
|
{["VPN", "RDP", "AnyDesk", "TeamViewer", "Supremo"].map((method) => (
|
||||||
|
<option key={method} value={method} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-state`}
|
||||||
|
label={t("mail.state")}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const parsed = RemoteConnectionInputSchema.shape.state.safeParse(value);
|
||||||
|
if (parsed.success) {
|
||||||
|
form.setValue("state", parsed.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
value={v.state}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={v.requiresVpn}
|
||||||
|
id={`${prefix}-vpn`}
|
||||||
|
label={t("remote.requiresVpn")}
|
||||||
|
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
</fieldset>
|
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
|
||||||
{fixed && <p className="text-sm text-muted-foreground">{t("remote.fixed")}</p>}
|
<Textarea
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
id={`${prefix}-notes`}
|
||||||
<p className="text-sm text-destructive" role="alert">
|
maxLength={4000}
|
||||||
{t("remote.errors.INVALID_REQUEST")}
|
rows={3}
|
||||||
</p>
|
{...form.register("notes")}
|
||||||
)}
|
/>
|
||||||
<RemoteFeedback error={mutation.error ?? assets.error ?? resources.error} />
|
</div>
|
||||||
<ContextHelp
|
</fieldset>
|
||||||
description={t("remote.description")}
|
{fixed && <p className="text-sm text-muted-foreground">{t("remote.fixed")}</p>}
|
||||||
examples={t("remote.examples")}
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
<p className="text-sm text-destructive" role="alert">
|
||||||
guidance={t("remote.guidance")}
|
{t("remote.errors.INVALID_REQUEST")}
|
||||||
scopeNote={t("remote.scope")}
|
</p>
|
||||||
title={t("remote.help")}
|
)}
|
||||||
/>
|
<RemoteFeedback error={mutation.error ?? assets.error ?? resources.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("remote.description")}
|
||||||
|
examples={t("remote.examples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("remote.guidance")}
|
||||||
|
scopeNote={t("remote.scope")}
|
||||||
|
title={t("remote.help")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={!ready || mutation.isPending} type="submit">
|
<Button disabled={!ready || mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
import { Label } from "@repo/shadcn-ui/components/label";
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
@ -17,30 +17,32 @@ export function SiteForm(props: SiteFormProps) {
|
|||||||
const { t } = useTranslation("companies");
|
const { t } = useTranslation("companies");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
{showTitle && <h3 className="text-lg font-semibold">{t(site ? "editSite" : "newSite")}</h3>}
|
<FormBody>
|
||||||
<div className="space-y-2">
|
{showTitle && <h3 className="text-lg font-semibold">{t(site ? "editSite" : "newSite")}</h3>}
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
</div>
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${id}-address`}>{t("addressOptional")}</Label>
|
<div className="space-y-2">
|
||||||
<Textarea
|
<Label htmlFor={`${id}-address`}>{t("addressOptional")}</Label>
|
||||||
className="min-h-40"
|
<Textarea
|
||||||
id={`${id}-address`}
|
className="min-h-40"
|
||||||
maxLength={10000}
|
id={`${id}-address`}
|
||||||
rows={6}
|
maxLength={10000}
|
||||||
{...form.register("address")}
|
rows={6}
|
||||||
|
{...form.register("address")}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("addressHelp")}</p>
|
||||||
|
</div>
|
||||||
|
<CompanyFeedback
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
|
success={mutation.isSuccess}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">{t("addressHelp")}</p>
|
|
||||||
</div>
|
|
||||||
<CompanyFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
success={mutation.isSuccess}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EntityHelp kind="site" />
|
<EntityHelp kind="site" />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -23,79 +23,83 @@ export function TeleworkAuthorizationForm(props: TeleworkAuthorizationFormProps)
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-person`}>{t("telework.person")}</Label>
|
<div className="space-y-2">
|
||||||
<SelectWithAction
|
<Label htmlFor={`${prefix}-person`}>{t("telework.person")}</Label>
|
||||||
action={fixed ? undefined : { label: t("newPerson"), onClick: () => setQuick(true) }}
|
<SelectWithAction
|
||||||
disabled={fixed}
|
action={
|
||||||
id={`${prefix}-person`}
|
fixed ? undefined : { label: t("newPerson"), onClick: () => setQuick(true) }
|
||||||
label={t("telework.person")}
|
}
|
||||||
onValueChange={(value) => form.setValue("personId", value)}
|
disabled={fixed}
|
||||||
options={company.people
|
id={`${prefix}-person`}
|
||||||
.filter(
|
label={t("telework.person")}
|
||||||
(person) =>
|
onValueChange={(value) => form.setValue("personId", value)}
|
||||||
person.id === values.personId || (person.active && !reserved.has(person.id))
|
options={company.people
|
||||||
)
|
.filter(
|
||||||
.map((person) => ({ value: person.id, label: person.name }))}
|
(person) =>
|
||||||
placeholder={t("mail.choose")}
|
person.id === values.personId || (person.active && !reserved.has(person.id))
|
||||||
required
|
)
|
||||||
value={values.personId}
|
.map((person) => ({ value: person.id, label: person.name }))}
|
||||||
|
placeholder={t("mail.choose")}
|
||||||
|
required
|
||||||
|
value={values.personId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SwitchField
|
||||||
|
checked={values.active}
|
||||||
|
id={`${prefix}-active`}
|
||||||
|
label={t("telework.active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<SwitchField
|
<div className="space-y-2">
|
||||||
checked={values.active}
|
<Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label>
|
||||||
id={`${prefix}-active`}
|
<Input
|
||||||
label={t("telework.active")}
|
id={`${prefix}-start`}
|
||||||
onCheckedChange={(active) => form.setValue("active", active)}
|
onInput={(event) => form.setValue("startsOn", event.currentTarget.value || null)}
|
||||||
|
type="date"
|
||||||
|
value={values.startsOn ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-end`}>{t("telework.expiresOn")}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-end`}
|
||||||
|
onInput={(event) => form.setValue("expiresOn", event.currentTarget.value || null)}
|
||||||
|
type="date"
|
||||||
|
value={values.expiresOn ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-notes`}>{t("telework.notes")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-notes`}
|
||||||
|
maxLength={4000}
|
||||||
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
|
rows={3}
|
||||||
|
value={values.notes ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{authorization && <p className="text-sm text-muted-foreground">{t("telework.fixed")}</p>}
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{t("telework.errors.INVALID_REQUEST")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<TeleworkFeedback error={mutation.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("telework.description")}
|
||||||
|
examples={t("telework.examples")}
|
||||||
|
examplesLabel={t("mail.examplesLabel")}
|
||||||
|
guidance={t("telework.guidance")}
|
||||||
|
scopeNote={t("telework.scope")}
|
||||||
|
title={t("telework.help")}
|
||||||
/>
|
/>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
</FormBody>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label>
|
|
||||||
<Input
|
|
||||||
id={`${prefix}-start`}
|
|
||||||
onInput={(event) => form.setValue("startsOn", event.currentTarget.value || null)}
|
|
||||||
type="date"
|
|
||||||
value={values.startsOn ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-end`}>{t("telework.expiresOn")}</Label>
|
|
||||||
<Input
|
|
||||||
id={`${prefix}-end`}
|
|
||||||
onInput={(event) => form.setValue("expiresOn", event.currentTarget.value || null)}
|
|
||||||
type="date"
|
|
||||||
value={values.expiresOn ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("telework.notes")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-notes`}
|
|
||||||
maxLength={4000}
|
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
|
||||||
rows={3}
|
|
||||||
value={values.notes ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
{authorization && <p className="text-sm text-muted-foreground">{t("telework.fixed")}</p>}
|
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("telework.errors.INVALID_REQUEST")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<TeleworkFeedback error={mutation.error} />
|
|
||||||
<ContextHelp
|
|
||||||
description={t("telework.description")}
|
|
||||||
examples={t("telework.examples")}
|
|
||||||
examplesLabel={t("mail.examplesLabel")}
|
|
||||||
guidance={t("telework.guidance")}
|
|
||||||
scopeNote={t("telework.scope")}
|
|
||||||
title={t("telework.help")}
|
|
||||||
/>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -38,108 +38,110 @@ export function TicketForm(props: TicketFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={prefix}>{t("tickets.company")}</Label>
|
<div className="space-y-2">
|
||||||
{scope || ticket ? (
|
<Label htmlFor={prefix}>{t("tickets.company")}</Label>
|
||||||
<p>{data.companies.find((item) => item.id === companyId)?.name ?? companyId}</p>
|
{scope || ticket ? (
|
||||||
) : (
|
<p>{data.companies.find((item) => item.id === companyId)?.name ?? companyId}</p>
|
||||||
<>
|
) : (
|
||||||
<CompanySelect
|
<>
|
||||||
action={{ label: t("newCompany"), onClick: () => setQuick("company") }}
|
<CompanySelect
|
||||||
id={prefix}
|
action={{ label: t("newCompany"), onClick: () => setQuick("company") }}
|
||||||
label={t("tickets.company")}
|
id={prefix}
|
||||||
onValueChange={changeCompany}
|
label={t("tickets.company")}
|
||||||
options={data.companies.map((item) => ({ value: item.id, label: item.name }))}
|
onValueChange={changeCompany}
|
||||||
placeholder={t("tickets.choose")}
|
options={data.companies.map((item) => ({ value: item.id, label: item.name }))}
|
||||||
value={companyId}
|
placeholder={t("tickets.choose")}
|
||||||
|
value={companyId}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("tickets.companyNotice")}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-subject`}>{t("tickets.subject")}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-subject`}
|
||||||
|
maxLength={200}
|
||||||
|
required
|
||||||
|
{...form.register("subject")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-description`}>{t("tickets.description")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-description`}
|
||||||
|
maxLength={10000}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-requester`}>{t("tickets.requester")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
action={{
|
||||||
|
label: t("newPerson"),
|
||||||
|
onClick: () => setQuick("person"),
|
||||||
|
disabled: !companyId,
|
||||||
|
}}
|
||||||
|
disabled={!lookups.data}
|
||||||
|
id={`${prefix}-requester`}
|
||||||
|
label={t("tickets.requester")}
|
||||||
|
onValueChange={(id) => form.setValue("requesterId", id || null)}
|
||||||
|
options={selectOptions(
|
||||||
|
lookups.data?.people ?? [],
|
||||||
|
ticket?.values.requesterId ?? null
|
||||||
|
)}
|
||||||
|
placeholder={t("tickets.unassigned")}
|
||||||
|
value={values.requesterId ?? ""}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">{t("tickets.companyNotice")}</p>
|
</div>
|
||||||
</>
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-assignee`}>{t("tickets.assignee")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
id={`${prefix}-assignee`}
|
||||||
|
label={t("tickets.assignee")}
|
||||||
|
onValueChange={(id) => form.setValue("assigneeId", id || null)}
|
||||||
|
options={selectOptions(data.technicians, ticket?.values.assigneeId ?? null)}
|
||||||
|
placeholder={t("tickets.unassigned")}
|
||||||
|
value={values.assigneeId ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TicketClassificationFields form={form} prefix={prefix} />
|
||||||
|
</div>
|
||||||
|
{companyId && lookups.isPending && <p role="status">{t("loading")}</p>}
|
||||||
|
{lookups.isError && (
|
||||||
|
<div role="alert">
|
||||||
|
<TicketFeedback error={lookups.error} />
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
lookups.refetch().catch(() => undefined);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
{lookups.data && (
|
||||||
<div className="space-y-2">
|
<TicketTargets
|
||||||
<Label htmlFor={`${prefix}-subject`}>{t("tickets.subject")}</Label>
|
companyId={companyId}
|
||||||
<Input
|
csrfToken={csrfToken}
|
||||||
id={`${prefix}-subject`}
|
lookups={lookups.data}
|
||||||
maxLength={200}
|
onChange={setTargets}
|
||||||
required
|
values={values.affected}
|
||||||
{...form.register("subject")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-description`}>{t("tickets.description")}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-description`}
|
|
||||||
maxLength={10000}
|
|
||||||
{...form.register("description")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-requester`}>{t("tickets.requester")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={{
|
|
||||||
label: t("newPerson"),
|
|
||||||
onClick: () => setQuick("person"),
|
|
||||||
disabled: !companyId,
|
|
||||||
}}
|
|
||||||
disabled={!lookups.data}
|
|
||||||
id={`${prefix}-requester`}
|
|
||||||
label={t("tickets.requester")}
|
|
||||||
onValueChange={(id) => form.setValue("requesterId", id || null)}
|
|
||||||
options={selectOptions(
|
|
||||||
lookups.data?.people ?? [],
|
|
||||||
ticket?.values.requesterId ?? null
|
|
||||||
)}
|
|
||||||
placeholder={t("tickets.unassigned")}
|
|
||||||
value={values.requesterId ?? ""}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className="space-y-2">
|
</fieldset>
|
||||||
<Label htmlFor={`${prefix}-assignee`}>{t("tickets.assignee")}</Label>
|
<TicketFeedback
|
||||||
<SelectWithAction
|
error={mutation.error}
|
||||||
id={`${prefix}-assignee`}
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
label={t("tickets.assignee")}
|
/>
|
||||||
onValueChange={(id) => form.setValue("assigneeId", id || null)}
|
<TicketHelp />
|
||||||
options={selectOptions(data.technicians, ticket?.values.assigneeId ?? null)}
|
</FormBody>
|
||||||
placeholder={t("tickets.unassigned")}
|
|
||||||
value={values.assigneeId ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TicketClassificationFields form={form} prefix={prefix} />
|
|
||||||
</div>
|
|
||||||
{companyId && lookups.isPending && <p role="status">{t("loading")}</p>}
|
|
||||||
{lookups.isError && (
|
|
||||||
<div role="alert">
|
|
||||||
<TicketFeedback error={lookups.error} />
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
lookups.refetch().catch(() => undefined);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{t("retry")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{lookups.data && (
|
|
||||||
<TicketTargets
|
|
||||||
companyId={companyId}
|
|
||||||
csrfToken={csrfToken}
|
|
||||||
lookups={lookups.data}
|
|
||||||
onChange={setTargets}
|
|
||||||
values={values.affected}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
<TicketFeedback
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<TicketHelp />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending || !companyId || !lookups.data} type="submit">
|
<Button disabled={mutation.isPending || !companyId || !lookups.data} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -35,90 +35,92 @@ export function WorkProfileForm(props: WorkProfileFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-4" disabled={disabled}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-4" disabled={disabled}>
|
||||||
<Label htmlFor={prefix}>{t("profiles.name")}</Label>
|
<div className="space-y-2">
|
||||||
<Input id={prefix} maxLength={100} required {...form.register("name")} />
|
<Label htmlFor={prefix}>{t("profiles.name")}</Label>
|
||||||
</div>
|
<Input id={prefix} maxLength={100} required {...form.register("name")} />
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
{selectors.map((selector) => {
|
|
||||||
const fixed = !!profile && selector.field !== "accountId";
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={selector.field}>
|
|
||||||
<Label htmlFor={`${prefix}-${selector.field}`}>
|
|
||||||
{t(`profiles.${selector.label}`)}
|
|
||||||
</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
fixed || !allowQuickCreate
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
label: t(`mail.${selector.create}`),
|
|
||||||
onClick: () => setQuick(selector.kind),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
disabled={disabled || fixed || !inventory.data || !access.data}
|
|
||||||
id={`${prefix}-${selector.field}`}
|
|
||||||
label={t(`profiles.${selector.label}`)}
|
|
||||||
onValueChange={(id) =>
|
|
||||||
form.setValue(
|
|
||||||
selector.field,
|
|
||||||
selector.field === "accountId" ? id || null : id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={selector.options}
|
|
||||||
placeholder={t(
|
|
||||||
selector.field === "accountId" ? "profiles.noAccount" : "profiles.choose"
|
|
||||||
)}
|
|
||||||
required={selector.field !== "accountId"}
|
|
||||||
value={values[selector.field] ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">{t("profiles.accountHint")}</p>
|
|
||||||
{values.personId && values.accountId && !authorized && allowQuickCreate && (
|
|
||||||
<div className="space-y-2 rounded-md border p-3 text-sm">
|
|
||||||
<p>{t("mail.noAuthorization")}</p>
|
|
||||||
<Button onClick={() => setQuick("authorization")} type="button" variant="outline">
|
|
||||||
{t("mail.authorize")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<SwitchField
|
{selectors.map((selector) => {
|
||||||
checked={values.active}
|
const fixed = !!profile && selector.field !== "accountId";
|
||||||
id={`${prefix}-active`}
|
return (
|
||||||
label={t("profiles.active")}
|
<div className="space-y-2" key={selector.field}>
|
||||||
onCheckedChange={(active) => form.setValue("active", active)}
|
<Label htmlFor={`${prefix}-${selector.field}`}>
|
||||||
/>
|
{t(`profiles.${selector.label}`)}
|
||||||
<div className="space-y-2">
|
</Label>
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
|
<SelectWithAction
|
||||||
<Textarea
|
action={
|
||||||
id={`${prefix}-notes`}
|
fixed || !allowQuickCreate
|
||||||
maxLength={4000}
|
? undefined
|
||||||
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
: {
|
||||||
rows={3}
|
label: t(`mail.${selector.create}`),
|
||||||
value={values.notes ?? ""}
|
onClick: () => setQuick(selector.kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={disabled || fixed || !inventory.data || !access.data}
|
||||||
|
id={`${prefix}-${selector.field}`}
|
||||||
|
label={t(`profiles.${selector.label}`)}
|
||||||
|
onValueChange={(id) =>
|
||||||
|
form.setValue(
|
||||||
|
selector.field,
|
||||||
|
selector.field === "accountId" ? id || null : id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={selector.options}
|
||||||
|
placeholder={t(
|
||||||
|
selector.field === "accountId" ? "profiles.noAccount" : "profiles.choose"
|
||||||
|
)}
|
||||||
|
required={selector.field !== "accountId"}
|
||||||
|
value={values[selector.field] ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">{t("profiles.accountHint")}</p>
|
||||||
|
{values.personId && values.accountId && !authorized && allowQuickCreate && (
|
||||||
|
<div className="space-y-2 rounded-md border p-3 text-sm">
|
||||||
|
<p>{t("mail.noAuthorization")}</p>
|
||||||
|
<Button onClick={() => setQuick("authorization")} type="button" variant="outline">
|
||||||
|
{t("mail.authorize")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<SwitchField
|
||||||
|
checked={values.active}
|
||||||
|
id={`${prefix}-active`}
|
||||||
|
label={t("profiles.active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
</fieldset>
|
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
|
||||||
{profile && <p className="text-sm text-muted-foreground">{t("profiles.fixed")}</p>}
|
<Textarea
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
id={`${prefix}-notes`}
|
||||||
<p className="text-destructive" role="alert">
|
maxLength={4000}
|
||||||
{t("profiles.errors.INVALID_REQUEST")}
|
onChange={(event) => form.setValue("notes", event.target.value || null)}
|
||||||
</p>
|
rows={3}
|
||||||
)}
|
value={values.notes ?? ""}
|
||||||
<ProfileFeedback error={mutation.error ?? access.error ?? inventory.error} />
|
/>
|
||||||
<ContextHelp
|
</div>
|
||||||
description={t("profiles.description")}
|
</fieldset>
|
||||||
examples={t("profiles.examples")}
|
{profile && <p className="text-sm text-muted-foreground">{t("profiles.fixed")}</p>}
|
||||||
examplesLabel={t("profiles.examplesLabel")}
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
guidance={t("profiles.guidance")}
|
<p className="text-destructive" role="alert">
|
||||||
scopeNote={t("profiles.scope")}
|
{t("profiles.errors.INVALID_REQUEST")}
|
||||||
title={t("profiles.help")}
|
</p>
|
||||||
/>
|
)}
|
||||||
|
<ProfileFeedback error={mutation.error ?? access.error ?? inventory.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t("profiles.description")}
|
||||||
|
examples={t("profiles.examples")}
|
||||||
|
examplesLabel={t("profiles.examplesLabel")}
|
||||||
|
guidance={t("profiles.guidance")}
|
||||||
|
scopeNote={t("profiles.scope")}
|
||||||
|
title={t("profiles.help")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={disabled || !inventory.data || !access.data} type="submit">
|
<Button disabled={disabled || !inventory.data || !access.data} type="submit">
|
||||||
{t(disabled ? "saving" : "save")}
|
{t(disabled ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
import { Label } from "@repo/shadcn-ui/components/label";
|
import { Label } from "@repo/shadcn-ui/components/label";
|
||||||
@ -15,47 +15,49 @@ export function PasswordForm(props: PasswordFormProps) {
|
|||||||
const { t } = useTranslation("identity");
|
const { t } = useTranslation("identity");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
{showTitle && (
|
<FormBody>
|
||||||
<h3 className="font-semibold">
|
{showTitle && (
|
||||||
{t(target ? "resetPassword" : "changePassword")}
|
<h3 className="font-semibold">
|
||||||
{target ? ` · ${target.name}` : ""}
|
{t(target ? "resetPassword" : "changePassword")}
|
||||||
</h3>
|
{target ? ` · ${target.name}` : ""}
|
||||||
)}
|
</h3>
|
||||||
{!showTitle && target && <p className="text-muted-foreground">{target.name}</p>}
|
)}
|
||||||
{!target && (
|
{!showTitle && target && <p className="text-muted-foreground">{target.name}</p>}
|
||||||
|
{!target && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-current`}>{t("currentPassword")}</Label>
|
||||||
|
<Input
|
||||||
|
autoComplete="current-password"
|
||||||
|
id={`${id}-current`}
|
||||||
|
maxLength={128}
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
{...form.register("currentPassword")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-current`}>{t("currentPassword")}</Label>
|
<Label htmlFor={`${id}-new`}>{t("newPassword")}</Label>
|
||||||
<Input
|
<Input
|
||||||
autoComplete="current-password"
|
autoComplete="new-password"
|
||||||
id={`${id}-current`}
|
id={`${id}-new`}
|
||||||
maxLength={128}
|
maxLength={128}
|
||||||
|
minLength={8}
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
{...form.register("currentPassword")}
|
{...form.register("password")}
|
||||||
/>
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{target && <p className="text-sm text-muted-foreground">{t("resetHelp")}</p>}
|
||||||
<div className="space-y-2">
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
<Label htmlFor={`${id}-new`}>{t("newPassword")}</Label>
|
<p className="text-sm text-destructive" role="alert">
|
||||||
<Input
|
{t("invalidForm")}
|
||||||
autoComplete="new-password"
|
</p>
|
||||||
id={`${id}-new`}
|
)}
|
||||||
maxLength={128}
|
<FormFeedback error={mutation.error} success={mutation.isSuccess} />
|
||||||
minLength={8}
|
</FormBody>
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
{...form.register("password")}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
|
|
||||||
</div>
|
|
||||||
{target && <p className="text-sm text-muted-foreground">{t("resetHelp")}</p>}
|
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("invalidForm")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<FormFeedback error={mutation.error} success={mutation.isSuccess} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export function UserEditorDialog(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
open={props.open}
|
open={props.open}
|
||||||
|
size="medium"
|
||||||
title={t(
|
title={t(
|
||||||
props.mode === "create"
|
props.mode === "create"
|
||||||
? "createUser"
|
? "createUser"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
import { SwitchField } from "@repo/rdx-ui/components/switch-field";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -16,62 +16,64 @@ export function UserForm(props: UserFormProps) {
|
|||||||
const { t } = useTranslation("identity");
|
const { t } = useTranslation("identity");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
{showTitle && <h3 className="font-semibold">{t(user ? "editUser" : "createUser")}</h3>}
|
<FormBody>
|
||||||
<div className="space-y-2">
|
{showTitle && <h3 className="font-semibold">{t(user ? "editUser" : "createUser")}</h3>}
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
|
||||||
<Input
|
|
||||||
autoComplete="name"
|
|
||||||
id={`${id}-name`}
|
|
||||||
maxLength={100}
|
|
||||||
required
|
|
||||||
{...form.register("name")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-email`}>{t("email")}</Label>
|
|
||||||
<Input
|
|
||||||
autoComplete="off"
|
|
||||||
id={`${id}-email`}
|
|
||||||
maxLength={254}
|
|
||||||
required
|
|
||||||
type="email"
|
|
||||||
{...form.register("email")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!user && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-password`}>{t("password")}</Label>
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
<Input
|
<Input
|
||||||
autoComplete="new-password"
|
autoComplete="name"
|
||||||
id={`${id}-password`}
|
id={`${id}-name`}
|
||||||
maxLength={128}
|
maxLength={100}
|
||||||
minLength={8}
|
|
||||||
required
|
required
|
||||||
type="password"
|
{...form.register("name")}
|
||||||
{...form.register("password")}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
{user && (
|
<Label htmlFor={`${id}-email`}>{t("email")}</Label>
|
||||||
<SwitchField
|
<Input
|
||||||
checked={form.watch("active")}
|
autoComplete="off"
|
||||||
disabled={user.isInitialAdmin}
|
id={`${id}-email`}
|
||||||
id={`${id}-active`}
|
maxLength={254}
|
||||||
label={t("active")}
|
required
|
||||||
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
type="email"
|
||||||
/>
|
{...form.register("email")}
|
||||||
)}
|
/>
|
||||||
{user?.isInitialAdmin && (
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{t("adminProtected")}</p>
|
{!user && (
|
||||||
)}
|
<div className="space-y-2">
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
<Label htmlFor={`${id}-password`}>{t("password")}</Label>
|
||||||
<p className="text-sm text-destructive" role="alert">
|
<Input
|
||||||
{t("invalidForm")}
|
autoComplete="new-password"
|
||||||
</p>
|
id={`${id}-password`}
|
||||||
)}
|
maxLength={128}
|
||||||
<FormFeedback error={mutation.error} success={mutation.isSuccess} />
|
minLength={8}
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
{...form.register("password")}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<SwitchField
|
||||||
|
checked={form.watch("active")}
|
||||||
|
disabled={user.isInitialAdmin}
|
||||||
|
id={`${id}-active`}
|
||||||
|
label={t("active")}
|
||||||
|
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{user?.isInitialAdmin && (
|
||||||
|
<p className="text-xs text-muted-foreground">{t("adminProtected")}</p>
|
||||||
|
)}
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{t("invalidForm")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<FormFeedback error={mutation.error} success={mutation.isSuccess} />
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -152,5 +152,9 @@
|
|||||||
"assign": "Section",
|
"assign": "Section",
|
||||||
"help": "Organize fields without changing their values or requirements. Removing a section moves its fields to Unsectioned.",
|
"help": "Organize fields without changing their values or requirements. Removing a section moves its fields to Unsectioned.",
|
||||||
"empty": "This section has no fields yet."
|
"empty": "This section has no fields yet."
|
||||||
|
},
|
||||||
|
"formLayout": {
|
||||||
|
"identity": "Resource identification",
|
||||||
|
"template": "Template details"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -152,5 +152,9 @@
|
|||||||
"assign": "Sección",
|
"assign": "Sección",
|
||||||
"help": "Organiza los campos sin cambiar sus valores ni su obligatoriedad. Al eliminar una sección, sus campos pasan a «Sin sección».",
|
"help": "Organiza los campos sin cambiar sus valores ni su obligatoriedad. Al eliminar una sección, sus campos pasan a «Sin sección».",
|
||||||
"empty": "Esta sección aún no tiene campos."
|
"empty": "Esta sección aún no tiene campos."
|
||||||
|
},
|
||||||
|
"formLayout": {
|
||||||
|
"identity": "Identificación del recurso",
|
||||||
|
"template": "Datos de la plantilla"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -19,54 +19,56 @@ export function CatalogFieldForm(props: CatalogFieldFormProps) {
|
|||||||
const { t } = useTranslation("resources");
|
const { t } = useTranslation("resources");
|
||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="space-y-2">
|
<FormBody>
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-type`}>{t("type")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
disabled={!!item || mutation.isPending}
|
|
||||||
id={`${id}-type`}
|
|
||||||
label={t("type")}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const parsed = FieldTypeSchema.safeParse(value);
|
|
||||||
if (parsed.success) {
|
|
||||||
form.setValue("type", parsed.data);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
|
|
||||||
placeholder={t("chooseType")}
|
|
||||||
required
|
|
||||||
value={type}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-unit`}>{t("unit")}</Label>
|
|
||||||
<Input id={`${id}-unit`} maxLength={30} {...form.register("unit")} />
|
|
||||||
</div>
|
|
||||||
{type === "choice" && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-choices`}>{t("choices")}</Label>
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
<Textarea id={`${id}-choices`} required rows={5} {...form.register("choicesText")} />
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
<p className="text-xs text-muted-foreground">{t("choicesHelp")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor={`${id}-type`}>{t("type")}</Label>
|
||||||
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
<SelectWithAction
|
||||||
<Textarea
|
disabled={!!item || mutation.isPending}
|
||||||
id={`${id}-description`}
|
id={`${id}-type`}
|
||||||
maxLength={1000}
|
label={t("type")}
|
||||||
rows={3}
|
onValueChange={(value) => {
|
||||||
{...form.register("description")}
|
const parsed = FieldTypeSchema.safeParse(value);
|
||||||
|
if (parsed.success) {
|
||||||
|
form.setValue("type", parsed.data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
|
||||||
|
placeholder={t("chooseType")}
|
||||||
|
required
|
||||||
|
value={type}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-unit`}>{t("unit")}</Label>
|
||||||
|
<Input id={`${id}-unit`} maxLength={30} {...form.register("unit")} />
|
||||||
|
</div>
|
||||||
|
{type === "choice" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-choices`}>{t("choices")}</Label>
|
||||||
|
<Textarea id={`${id}-choices`} required rows={5} {...form.register("choicesText")} />
|
||||||
|
<p className="text-xs text-muted-foreground">{t("choicesHelp")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${id}-description`}
|
||||||
|
maxLength={1000}
|
||||||
|
rows={3}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CatalogError
|
||||||
|
error={mutation.error}
|
||||||
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
<CatalogHelp kind="field" scope={scope} />
|
||||||
<CatalogError
|
</FormBody>
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<CatalogHelp kind="field" scope={scope} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { FormSection } from "@repo/rdx-ui/components/form-section";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { TemplateSectionsEditor } from "@repo/rdx-ui/components/template-sections-editor";
|
import { TemplateSectionsEditor } from "@repo/rdx-ui/components/template-sections-editor";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
@ -23,108 +24,112 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
|
|||||||
const id = useId();
|
const id = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-4" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<FormBody>
|
||||||
|
<FormSection title={t("formLayout.template")}>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
||||||
|
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${id}-description`}
|
||||||
|
maxLength={1000}
|
||||||
|
rows={2}
|
||||||
|
{...form.register("description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${id}-usage`}>{t("templateUsage")}</Label>
|
||||||
|
<SelectWithAction
|
||||||
|
disabled={!!item || mutation.isPending}
|
||||||
|
id={`${id}-usage`}
|
||||||
|
label={t("templateUsage")}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
if (value === "application" || value === "certificate") {
|
||||||
|
form.setValue("usage", value);
|
||||||
|
} else {
|
||||||
|
form.setValue("usage", undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={["general", "application", "certificate"].map((value) => ({
|
||||||
|
value,
|
||||||
|
label: t(`usage.${value}`),
|
||||||
|
}))}
|
||||||
|
placeholder={t("choose")}
|
||||||
|
value={form.watch("usage") ?? "general"}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">{t("templateUsageHelp")}</p>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${id}-name`}>{t("name")}</Label>
|
<Label htmlFor={`${id}-field`}>{t("selectField")}</Label>
|
||||||
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
|
<SelectWithAction
|
||||||
</div>
|
action={
|
||||||
<div className="space-y-2">
|
allowQuickCreate
|
||||||
<Label htmlFor={`${id}-description`}>{t("description")}</Label>
|
? { label: t("addField"), onClick: () => setCreatingField(true) }
|
||||||
<Textarea
|
: undefined
|
||||||
id={`${id}-description`}
|
}
|
||||||
maxLength={1000}
|
disabled={mutation.isPending}
|
||||||
rows={2}
|
id={`${id}-field`}
|
||||||
{...form.register("description")}
|
label={t("selectField")}
|
||||||
|
onValueChange={add}
|
||||||
|
options={fields
|
||||||
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
|
||||||
|
)
|
||||||
|
.map((entry) => ({ value: entry.id, label: entry.values.name }))}
|
||||||
|
placeholder={t("choose")}
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
|
||||||
<div className="space-y-2">
|
<TemplateSectionsEditor
|
||||||
<Label htmlFor={`${id}-usage`}>{t("templateUsage")}</Label>
|
createId={() => UniqueID.generateNewID().toPrimitive()}
|
||||||
<SelectWithAction
|
|
||||||
disabled={!!item || mutation.isPending}
|
|
||||||
id={`${id}-usage`}
|
|
||||||
label={t("templateUsage")}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value === "application" || value === "certificate") {
|
|
||||||
form.setValue("usage", value);
|
|
||||||
} else {
|
|
||||||
form.setValue("usage", undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={["general", "application", "certificate"].map((value) => ({
|
|
||||||
value,
|
|
||||||
label: t(`usage.${value}`),
|
|
||||||
}))}
|
|
||||||
placeholder={t("choose")}
|
|
||||||
value={form.watch("usage") ?? "general"}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">{t("templateUsageHelp")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${id}-field`}>{t("selectField")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
action={
|
|
||||||
allowQuickCreate
|
|
||||||
? { label: t("addField"), onClick: () => setCreatingField(true) }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
disabled={mutation.isPending}
|
disabled={mutation.isPending}
|
||||||
id={`${id}-field`}
|
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))}
|
||||||
label={t("selectField")}
|
fieldNotes={Object.fromEntries(
|
||||||
onValueChange={add}
|
fields
|
||||||
options={fields
|
.filter((field) => !field.effectiveActive)
|
||||||
.filter(
|
.map((field) => [field.id, t("disabledField")])
|
||||||
(entry) =>
|
)}
|
||||||
entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
|
fields={selected}
|
||||||
)
|
labels={{
|
||||||
.map((entry) => ({ value: entry.id, label: entry.values.name }))}
|
title: t("sections.title"),
|
||||||
placeholder={t("choose")}
|
add: t("sections.add"),
|
||||||
value=""
|
name: t("sections.name"),
|
||||||
|
description: t("sections.description"),
|
||||||
|
ungrouped: t("sections.ungrouped"),
|
||||||
|
moveUp: t("sections.moveUp"),
|
||||||
|
moveDown: t("sections.moveDown"),
|
||||||
|
remove: t("sections.remove"),
|
||||||
|
removeField: t("sections.removeField"),
|
||||||
|
required: t("sections.required"),
|
||||||
|
assign: t("sections.assign"),
|
||||||
|
help: t("sections.help"),
|
||||||
|
empty: t("sections.empty"),
|
||||||
|
}}
|
||||||
|
onChange={({ sections, fields: next }) => {
|
||||||
|
form.setValue("sections", sections, { shouldDirty: true });
|
||||||
|
form.setValue("fields", next, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
sections={form.watch("sections") ?? []}
|
||||||
/>
|
/>
|
||||||
</div>
|
{form.formState.errors.sections && (
|
||||||
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
|
<p className="text-sm text-destructive" role="alert">
|
||||||
<TemplateSectionsEditor
|
{t("errors.INVALID_SECTIONS")}
|
||||||
createId={() => UniqueID.generateNewID().toPrimitive()}
|
</p>
|
||||||
disabled={mutation.isPending}
|
|
||||||
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))}
|
|
||||||
fieldNotes={Object.fromEntries(
|
|
||||||
fields
|
|
||||||
.filter((field) => !field.effectiveActive)
|
|
||||||
.map((field) => [field.id, t("disabledField")])
|
|
||||||
)}
|
)}
|
||||||
fields={selected}
|
<CatalogError
|
||||||
labels={{
|
error={mutation.error}
|
||||||
title: t("sections.title"),
|
invalid={Object.keys(form.formState.errors).length > 0}
|
||||||
add: t("sections.add"),
|
/>
|
||||||
name: t("sections.name"),
|
<CatalogHelp kind="template" scope={scope} />
|
||||||
description: t("sections.description"),
|
</FormBody>
|
||||||
ungrouped: t("sections.ungrouped"),
|
|
||||||
moveUp: t("sections.moveUp"),
|
|
||||||
moveDown: t("sections.moveDown"),
|
|
||||||
remove: t("sections.remove"),
|
|
||||||
removeField: t("sections.removeField"),
|
|
||||||
required: t("sections.required"),
|
|
||||||
assign: t("sections.assign"),
|
|
||||||
help: t("sections.help"),
|
|
||||||
empty: t("sections.empty"),
|
|
||||||
}}
|
|
||||||
onChange={({ sections, fields: next }) => {
|
|
||||||
form.setValue("sections", sections, { shouldDirty: true });
|
|
||||||
form.setValue("fields", next, { shouldDirty: true });
|
|
||||||
}}
|
|
||||||
sections={form.watch("sections") ?? []}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.sections && (
|
|
||||||
<p className="text-sm text-destructive" role="alert">
|
|
||||||
{t("errors.INVALID_SECTIONS")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CatalogError
|
|
||||||
error={mutation.error}
|
|
||||||
invalid={Object.keys(form.formState.errors).length > 0}
|
|
||||||
/>
|
|
||||||
<CatalogHelp kind="template" scope={scope} />
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
import { ContextHelp } from "@repo/rdx-ui/components/context-help";
|
||||||
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
import { FieldSections } from "@repo/rdx-ui/components/field-sections";
|
||||||
import { FormActions } from "@repo/rdx-ui/components/form-dialog";
|
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog";
|
||||||
|
import { FormSection } from "@repo/rdx-ui/components/form-section";
|
||||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Input } from "@repo/shadcn-ui/components/input";
|
import { Input } from "@repo/shadcn-ui/components/input";
|
||||||
@ -42,111 +43,115 @@ export function ResourceForm(props: ResourceFormProps) {
|
|||||||
const prefix = useId();
|
const prefix = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="space-y-5" onSubmit={submit}>
|
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}>
|
||||||
<fieldset className="space-y-5" disabled={mutation.isPending}>
|
<FormBody>
|
||||||
<div className="space-y-2">
|
<fieldset className="space-y-5" disabled={mutation.isPending}>
|
||||||
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
<FormSection title={t("formLayout.identity")}>
|
||||||
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
|
<div className="space-y-2">
|
||||||
</div>
|
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<SelectWithAction
|
<div className="space-y-2">
|
||||||
action={
|
<Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
|
||||||
resource || !allowQuickCreate
|
<SelectWithAction
|
||||||
? undefined
|
action={
|
||||||
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
|
resource || !allowQuickCreate
|
||||||
}
|
? undefined
|
||||||
disabled={!!resource || mutation.isPending}
|
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
|
||||||
id={`${prefix}-template`}
|
}
|
||||||
label={t("template")}
|
disabled={!!resource || mutation.isPending}
|
||||||
onValueChange={selectTemplate}
|
id={`${prefix}-template`}
|
||||||
options={
|
label={t("template")}
|
||||||
resource
|
onValueChange={selectTemplate}
|
||||||
? [
|
options={
|
||||||
{
|
resource
|
||||||
value: resource.values.templateId,
|
? [
|
||||||
label: resource.specification.templateName,
|
{
|
||||||
},
|
value: resource.values.templateId,
|
||||||
]
|
label: resource.specification.templateName,
|
||||||
: templates.map((item) => ({ value: item.id, label: item.values.name }))
|
},
|
||||||
}
|
]
|
||||||
placeholder={t("chooseValue")}
|
: templates.map((item) => ({ value: item.id, label: item.values.name }))
|
||||||
required
|
}
|
||||||
value={values.templateId}
|
placeholder={t("chooseValue")}
|
||||||
/>
|
required
|
||||||
</div>
|
value={values.templateId}
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
|
|
||||||
<SelectWithAction
|
|
||||||
disabled={mutation.isPending}
|
|
||||||
id={`${prefix}-status`}
|
|
||||||
label={t("status")}
|
|
||||||
onValueChange={changeStatus}
|
|
||||||
options={["active", "inactive"].map((value) => ({ value, label: t(value) }))}
|
|
||||||
placeholder={t("chooseValue")}
|
|
||||||
required
|
|
||||||
value={values.active ? "active" : "inactive"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ResourceIntegrationFields
|
|
||||||
fixed={!!resource}
|
|
||||||
integration={values.integration}
|
|
||||||
items={items}
|
|
||||||
onChange={(integration) =>
|
|
||||||
form.setValue("integration", integration, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
references={referenceQuery.data?.references ?? []}
|
|
||||||
templateKey={template?.builtinKey}
|
|
||||||
/>
|
|
||||||
{fields.length > 0 && (
|
|
||||||
<section className="space-y-3">
|
|
||||||
<h3 className="font-medium">{t("characteristics")}</h3>
|
|
||||||
<FieldSections
|
|
||||||
hasErrors={!!mutation.error || Object.keys(form.formState.errors).length > 0}
|
|
||||||
items={fields}
|
|
||||||
renderItem={(field) => (
|
|
||||||
<ResourceCharacteristicControl
|
|
||||||
disabled={mutation.isPending}
|
|
||||||
field={requireMailboxAddress(field, items, values, template)}
|
|
||||||
key={field.fieldId}
|
|
||||||
onChange={(value) => setCharacteristic(field.fieldId, value)}
|
|
||||||
prefix={prefix}
|
|
||||||
value={values.characteristics[field.fieldId]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
sections={sections}
|
<div className="space-y-2">
|
||||||
ungroupedLabel={t("sections.ungrouped")}
|
<Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
|
||||||
/>
|
<SelectWithAction
|
||||||
</section>
|
disabled={mutation.isPending}
|
||||||
)}
|
id={`${prefix}-status`}
|
||||||
<div className="space-y-2">
|
label={t("status")}
|
||||||
<Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label>
|
onValueChange={changeStatus}
|
||||||
<Textarea
|
options={["active", "inactive"].map((value) => ({ value, label: t(value) }))}
|
||||||
id={`${prefix}-notes`}
|
placeholder={t("chooseValue")}
|
||||||
maxLength={
|
required
|
||||||
values.integration || template?.builtinKey === "template:mailbox" ? 4000 : 10000
|
value={values.active ? "active" : "inactive"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
<ResourceIntegrationFields
|
||||||
|
fixed={!!resource}
|
||||||
|
integration={values.integration}
|
||||||
|
items={items}
|
||||||
|
onChange={(integration) =>
|
||||||
|
form.setValue("integration", integration, { shouldDirty: true })
|
||||||
}
|
}
|
||||||
rows={3}
|
references={referenceQuery.data?.references ?? []}
|
||||||
{...form.register("notes")}
|
templateKey={template?.builtinKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
{fields.length > 0 && (
|
||||||
</fieldset>
|
<section className="space-y-3">
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
<h3 className="font-medium">{t("characteristics")}</h3>
|
||||||
<p className="text-sm text-destructive" role="alert">
|
<FieldSections
|
||||||
{t("inventoryErrors.INVALID_REQUEST")}
|
hasErrors={!!mutation.error || Object.keys(form.formState.errors).length > 0}
|
||||||
</p>
|
items={fields}
|
||||||
)}
|
renderItem={(field) => (
|
||||||
<InventoryFeedback error={mutation.error ?? referenceQuery.error} />
|
<ResourceCharacteristicControl
|
||||||
<ContextHelp
|
disabled={mutation.isPending}
|
||||||
description={t(`${helpPrefix}Description`)}
|
field={requireMailboxAddress(field, items, values, template)}
|
||||||
examples={t(`${helpPrefix}Examples`)}
|
key={field.fieldId}
|
||||||
examplesLabel={t("examplesLabel")}
|
onChange={(value) => setCharacteristic(field.fieldId, value)}
|
||||||
guidance={t(`${helpPrefix}Guidance`)}
|
prefix={prefix}
|
||||||
scopeNote={t(`${helpPrefix}Scope`)}
|
value={values.characteristics[field.fieldId]}
|
||||||
title={t("helpTitle")}
|
/>
|
||||||
/>
|
)}
|
||||||
|
sections={sections}
|
||||||
|
ungroupedLabel={t("sections.ungrouped")}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-notes`}
|
||||||
|
maxLength={
|
||||||
|
values.integration || template?.builtinKey === "template:mailbox" ? 4000 : 10000
|
||||||
|
}
|
||||||
|
rows={3}
|
||||||
|
{...form.register("notes")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<p className="text-sm text-destructive" role="alert">
|
||||||
|
{t("inventoryErrors.INVALID_REQUEST")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<InventoryFeedback error={mutation.error ?? referenceQuery.error} />
|
||||||
|
<ContextHelp
|
||||||
|
description={t(`${helpPrefix}Description`)}
|
||||||
|
examples={t(`${helpPrefix}Examples`)}
|
||||||
|
examplesLabel={t("examplesLabel")}
|
||||||
|
guidance={t(`${helpPrefix}Guidance`)}
|
||||||
|
scopeNote={t(`${helpPrefix}Scope`)}
|
||||||
|
title={t("helpTitle")}
|
||||||
|
/>
|
||||||
|
</FormBody>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button disabled={mutation.isPending} type="submit">
|
<Button disabled={mutation.isPending} type="submit">
|
||||||
{t(mutation.isPending ? "saving" : "save")}
|
{t(mutation.isPending ? "saving" : "save")}
|
||||||
|
|||||||
@ -29,9 +29,13 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
}, [hasErrors]);
|
}, [hasErrors]);
|
||||||
const content = (fields: readonly T[]) =>
|
const content = (fields: readonly T[]) =>
|
||||||
values ? (
|
values ? (
|
||||||
<dl className="grid gap-4 sm:grid-cols-2">{fields.map(renderItem)}</dl>
|
<dl className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
||||||
|
{fields.map(renderItem)}
|
||||||
|
</dl>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 sm:grid-cols-2">{fields.map(renderItem)}</div>
|
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3">
|
||||||
|
{fields.map(renderItem)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
if (!sections.length) {
|
if (!sections.length) {
|
||||||
return content(items);
|
return content(items);
|
||||||
@ -40,7 +44,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
const ungrouped = items.filter((item) => !(item.sectionId && known.has(item.sectionId)));
|
const ungrouped = items.filter((item) => !(item.sectionId && known.has(item.sectionId)));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="space-y-3"
|
className="grid items-start gap-4 @min-[900px]:grid-cols-2"
|
||||||
onInvalidCapture={(event) => {
|
onInvalidCapture={(event) => {
|
||||||
// Abrir antes de que el navegador intente enfocar un control inválido oculto.
|
// Abrir antes de que el navegador intente enfocar un control inválido oculto.
|
||||||
if (event.target instanceof HTMLElement) {
|
if (event.target instanceof HTMLElement) {
|
||||||
@ -58,7 +62,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<details className="rounded-md border" key={section.id} open>
|
<details className="@container rounded-md border" key={section.id} open>
|
||||||
<summary className="cursor-pointer rounded-md px-4 py-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
|
<summary className="cursor-pointer rounded-md px-4 py-3 text-sm font-medium focus-visible:outline-2 focus-visible:outline-ring">
|
||||||
{section.name}
|
{section.name}
|
||||||
</summary>
|
</summary>
|
||||||
@ -72,7 +76,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{ungrouped.length > 0 && (
|
{ungrouped.length > 0 && (
|
||||||
<section className="space-y-3">
|
<section className="col-span-full space-y-3">
|
||||||
<h4 className="text-sm font-medium">{ungroupedLabel}</h4>
|
<h4 className="text-sm font-medium">{ungroupedLabel}</h4>
|
||||||
{content(ungrouped)}
|
{content(ungrouped)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export function FormDialog({
|
|||||||
cancelLabel,
|
cancelLabel,
|
||||||
children,
|
children,
|
||||||
wide = false,
|
wide = false,
|
||||||
|
size,
|
||||||
formActions = false,
|
formActions = false,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -26,28 +27,39 @@ export function FormDialog({
|
|||||||
cancelLabel: string;
|
cancelLabel: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
wide?: boolean;
|
wide?: boolean;
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
formActions?: boolean;
|
formActions?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const width = size ?? (wide ? "large" : "small");
|
||||||
|
const widths = {
|
||||||
|
small: "sm:max-w-xl",
|
||||||
|
medium: "sm:max-w-3xl",
|
||||||
|
large: "sm:w-[90vw] sm:max-w-[1200px]",
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={`max-h-[calc(100dvh-2rem)] overflow-y-auto ${wide ? "sm:max-w-3xl" : "sm:max-w-xl"}`}
|
className={`flex max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden p-0 ${widths[width]}`}
|
||||||
showCloseButton={true}
|
showCloseButton={true}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader className="shrink-0 border-b px-6 py-5 pr-14">
|
||||||
<DialogTitle className="text-lg font-bold">{title}</DialogTitle>
|
<DialogTitle className="text-lg font-bold">{title}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<FormCancelContext.Provider value={formActions ? cancelLabel : undefined}>
|
<div
|
||||||
{children}
|
className="@container flex min-h-0 flex-1 flex-col overflow-y-auto px-6 pt-5 pb-5 [&:has([data-slot=form-body])]:overflow-hidden [&:has([data-slot=form-body])]:pb-0"
|
||||||
</FormCancelContext.Provider>
|
data-slot="form-dialog-content"
|
||||||
|
>
|
||||||
|
<FormCancelContext.Provider value={formActions ? cancelLabel : undefined}>
|
||||||
|
{children}
|
||||||
|
</FormCancelContext.Provider>
|
||||||
|
</div>
|
||||||
{!formActions && (
|
{!formActions && (
|
||||||
<DialogFooter>
|
<DialogFooter className="shrink-0 border-t px-6 py-4">
|
||||||
<DialogClose render={<Button type="button" variant="outline" />}>
|
<DialogClose render={<Button type="button" variant="outline" />}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@ -56,7 +68,7 @@ export function FormDialog({
|
|||||||
export function FormActions({ children }: { children: ReactNode }) {
|
export function FormActions({ children }: { children: ReactNode }) {
|
||||||
const cancelLabel = useContext(FormCancelContext);
|
const cancelLabel = useContext(FormCancelContext);
|
||||||
return (
|
return (
|
||||||
<DialogFooter className="border-t pt-4">
|
<DialogFooter className="shrink-0 border-t bg-popover py-4">
|
||||||
{cancelLabel && (
|
{cancelLabel && (
|
||||||
<DialogClose render={<Button type="button" variant="outline" />}>{cancelLabel}</DialogClose>
|
<DialogClose render={<Button type="button" variant="outline" />}>{cancelLabel}</DialogClose>
|
||||||
)}
|
)}
|
||||||
@ -64,3 +76,12 @@ export function FormActions({ children }: { children: ReactNode }) {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** El cuerpo se desplaza dentro del formulario; las acciones conservan su submit nativo. */
|
||||||
|
export function FormBody({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto pb-5 pr-1" data-slot="form-body">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
15
packages/rdx-ui/src/components/form-section.tsx
Normal file
15
packages/rdx-ui/src/components/form-section.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { useId } from "react";
|
||||||
|
|
||||||
|
/** Bloque visual reutilizable con título accesible; no altera validación ni datos. */
|
||||||
|
export function FormSection({ title, children }: { title: string; children: ReactNode }) {
|
||||||
|
const id = useId();
|
||||||
|
return (
|
||||||
|
<section aria-labelledby={id} className="space-y-4 rounded-lg border p-4">
|
||||||
|
<h3 className="text-sm font-semibold" id={id}>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -87,76 +87,82 @@ export function TemplateSectionsEditor({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">{labels.help}</p>
|
<p className="text-xs text-muted-foreground">{labels.help}</p>
|
||||||
{sections.map((section, index) => (
|
<div className="grid items-start gap-4 @min-[900px]:grid-cols-2">
|
||||||
<fieldset className="space-y-3 rounded-md border p-3" disabled={disabled} key={section.id}>
|
{sections.map((section, index) => (
|
||||||
<legend className="sr-only">{section.name || labels.name}</legend>
|
<fieldset
|
||||||
<div className="flex items-end gap-2">
|
className="space-y-3 rounded-md border p-3"
|
||||||
<div className="min-w-0 flex-1 space-y-2">
|
disabled={disabled}
|
||||||
<Label htmlFor={`${prefix}-${section.id}-name`}>{labels.name}</Label>
|
key={section.id}
|
||||||
<Input
|
>
|
||||||
id={`${prefix}-${section.id}-name`}
|
<legend className="sr-only">{section.name || labels.name}</legend>
|
||||||
maxLength={100}
|
<div className="flex items-end gap-2">
|
||||||
|
<div className="min-w-0 flex-1 space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-${section.id}-name`}>{labels.name}</Label>
|
||||||
|
<Input
|
||||||
|
id={`${prefix}-${section.id}-name`}
|
||||||
|
maxLength={100}
|
||||||
|
onChange={(event) =>
|
||||||
|
saveSections(
|
||||||
|
sections.map((item) =>
|
||||||
|
item.id === section.id ? { ...item, name: event.target.value } : item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
value={section.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
aria-label={`${labels.moveUp}: ${section.name}`}
|
||||||
|
disabled={disabled || index === 0}
|
||||||
|
onClick={() => moveSection(index, -1)}
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={`${labels.moveDown}: ${section.name}`}
|
||||||
|
disabled={disabled || index === sections.length - 1}
|
||||||
|
onClick={() => moveSection(index, 1)}
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={`${labels.remove}: ${section.name}`}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => removeSection(section.id)}
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`${prefix}-${section.id}-description`}>{labels.description}</Label>
|
||||||
|
<Textarea
|
||||||
|
id={`${prefix}-${section.id}-description`}
|
||||||
|
maxLength={1000}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
saveSections(
|
saveSections(
|
||||||
sections.map((item) =>
|
sections.map((item) =>
|
||||||
item.id === section.id ? { ...item, name: event.target.value } : item
|
item.id === section.id ? { ...item, description: event.target.value } : item
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
required
|
rows={2}
|
||||||
value={section.name}
|
value={section.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{fieldList(section.id)}
|
||||||
aria-label={`${labels.moveUp}: ${section.name}`}
|
</fieldset>
|
||||||
disabled={disabled || index === 0}
|
))}
|
||||||
onClick={() => moveSection(index, -1)}
|
</div>
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<ArrowUpIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
aria-label={`${labels.moveDown}: ${section.name}`}
|
|
||||||
disabled={disabled || index === sections.length - 1}
|
|
||||||
onClick={() => moveSection(index, 1)}
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
aria-label={`${labels.remove}: ${section.name}`}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={() => removeSection(section.id)}
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<Trash2Icon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`${prefix}-${section.id}-description`}>{labels.description}</Label>
|
|
||||||
<Textarea
|
|
||||||
id={`${prefix}-${section.id}-description`}
|
|
||||||
maxLength={1000}
|
|
||||||
onChange={(event) =>
|
|
||||||
saveSections(
|
|
||||||
sections.map((item) =>
|
|
||||||
item.id === section.id ? { ...item, description: event.target.value } : item
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
rows={2}
|
|
||||||
value={section.description}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{fieldList(section.id)}
|
|
||||||
</fieldset>
|
|
||||||
))}
|
|
||||||
{fieldList()}
|
{fieldList()}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user