Compare commits

..

No commits in common. "ef4f41493a41b8b389d5cc71d93a4df272c2f39e" and "91988a26b9cbb8f8f8f33eab3b11609508003e4c" have entirely different histories.

50 changed files with 2584 additions and 2785 deletions

View File

@ -31,7 +31,7 @@
"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
monospace; monospace;
--color-shell-exterior: #000; --color-shell-exterior: #000;
--color-shell-content: var(--color-background); --color-shell-content: #f7f9fb;
--color-shell-header: #fff; --color-shell-header: #fff;
--color-shell-sidebar: #0e282c; --color-shell-sidebar: #0e282c;
--color-shell-sidebar-top: #0c252a; --color-shell-sidebar-top: #0c252a;

View File

@ -285,13 +285,3 @@ 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.

View File

@ -299,9 +299,5 @@
"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"
} }
} }

View File

@ -299,9 +299,5 @@
"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"
} }
} }

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog"; import { FormActions, 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,13 +22,11 @@ export function EndRelationshipDialog(props: EndRelationshipDialogProps) {
open open
title={t("endRelationship")} title={t("endRelationship")}
> >
<FormBody> <p className="text-sm">
<p className="text-sm"> {relationship.values.name} · {sourceName} → {targetName}
{relationship.values.name} · {sourceName} → {targetName} </p>
</p> <p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p>
<p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p> <InventoryFeedback error={mutation.error} />
<InventoryFeedback error={mutation.error} />
</FormBody>
<FormActions> <FormActions>
<Button <Button
disabled={mutation.isPending} disabled={mutation.isPending}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,56 +34,51 @@ export function RelationshipForm(props: RelationshipFormProps) {
const id = useId(); const id = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-type`}>{t("relationshipType")}</Label>
<Label htmlFor={`${id}-type`}>{t("relationshipType")}</Label> <SelectWithAction
<SelectWithAction action={{ label: t("newRelation"), onClick: () => setQuick(true) }}
action={{ label: t("newRelation"), onClick: () => setQuick(true) }} disabled={mutation.isPending || catalog.isPending}
disabled={mutation.isPending || catalog.isPending} id={`${id}-type`}
id={`${id}-type`} label={t("relationshipType")}
label={t("relationshipType")} onValueChange={setTypeId}
onValueChange={setTypeId} options={relationshipTypes.map((item) => ({ value: item.id, label: item.values.name }))}
options={relationshipTypes.map((item) => ({ placeholder={t("chooseRelation")}
value: item.id, required
label: item.values.name, value={typeId}
}))}
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")}
/> />
</FormBody> </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")}
/>
<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")}

View File

@ -1,7 +1,6 @@
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, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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";
@ -44,169 +43,160 @@ 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="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-5" disabled={mutation.isPending}>
<fieldset className="space-y-5" disabled={mutation.isPending}> <div className="space-y-2">
<FormSection title={t("formLayout.identity")}> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<div className="space-y-2"> <Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> </div>
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} /> <div className="grid gap-4 sm:grid-cols-2">
</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}-notes`}>{t("notes")}</Label> <Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
<Textarea <SelectWithAction
id={`${prefix}-notes`} action={
maxLength={10000} asset || !allowQuickCreate
rows={3} ? undefined
{...form.register("notes")} : { 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>
</fieldset> <div className="space-y-2">
{Object.keys(form.formState.errors).length > 0 && ( <Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
<p className="text-sm text-destructive" role="alert"> <SelectWithAction
{t("inventoryErrors.INVALID_REQUEST")} disabled={mutation.isPending}
</p> 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>
{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>
)} )}
<InventoryFeedback error={mutation.error} /> <div className="space-y-2">
<ContextHelp <Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label>
description={t("assetDescription")} <Textarea
examples={t("assetExamples")} id={`${prefix}-notes`}
examplesLabel={t("examplesLabel")} maxLength={10000}
guidance={t("assetGuidance")} rows={3}
scopeNote={t("assetScope")} {...form.register("notes")}
title={t("helpTitle")} />
/> </div>
</FormBody> </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} />
<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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,56 +29,54 @@ export function BrandForm(props: BrandFormProps) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending || isReadingLogo}>
<fieldset className="space-y-4" disabled={mutation.isPending || isReadingLogo}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={id}>{t("name")}</Label>
<Label htmlFor={id}>{t("name")}</Label> <Input
<Input id={id}
id={id} maxLength={100}
maxLength={100} readOnly={brand?.builtin}
readOnly={brand?.builtin} required
required {...form.register("name")}
{...form.register("name")}
/>
</div>
<BrandLogoField
error={logoError ?? form.formState.errors.logoSvg?.message}
logoSvg={logoSvg}
name={name}
onRemove={removeLogo}
onSelect={selectLogo}
/> />
{!quickCreate && ( </div>
<SwitchField <BrandLogoField
checked={form.watch("active")} error={logoError ?? form.formState.errors.logoSvg?.message}
id={`${id}-active`} logoSvg={logoSvg}
label={t("brands.active")} name={name}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })} onRemove={removeLogo}
/> onSelect={selectLogo}
)}
</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>
)}
<ContextHelp
description={t("brands.help")}
examples={t("brands.examples")}
examplesLabel={t("examplesLabel")}
guidance={t("brands.guidance")}
scopeNote={t("globalHelp")}
title={t("helpTitle")}
/> />
</FormBody> {!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>
)}
<ContextHelp
description={t("brands.help")}
examples={t("brands.examples")}
examplesLabel={t("examplesLabel")}
guidance={t("brands.guidance")}
scopeNote={t("globalHelp")}
title={t("helpTitle")}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,56 +19,54 @@ export function CatalogFieldForm(props: CatalogFieldFormProps) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-type`}>{t("type")}</Label>
<Label htmlFor={`${id}-type`}>{t("type")}</Label> <SelectWithAction
<SelectWithAction disabled={!!item || mutation.isPending}
disabled={!!item || mutation.isPending} id={`${id}-type`}
id={`${id}-type`} label={t("type")}
label={t("type")} onValueChange={(value) => {
onValueChange={(value) => { const parsed = FieldTypeSchema.safeParse(value);
const parsed = FieldTypeSchema.safeParse(value); if (parsed.success) {
if (parsed.success) { form.setValue("type", parsed.data);
form.setValue("type", parsed.data); }
} }}
}} options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))} placeholder={t("chooseType")}
placeholder={t("chooseType")} required
required value={type}
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}
/> />
<CatalogHelp kind="field" scope={scope} /> </div>
</FormBody> <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}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,56 +18,49 @@ export function CatalogRelationForm(props: CatalogRelationFormProps) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={id}>{t("name")}</Label>
<Label htmlFor={id}>{t("name")}</Label> <Input
<Input id={id}
id={id} maxLength={100}
maxLength={100} required
required {...form.register("name")}
{...form.register("name")} onChange={(event) => {
onChange={(event) => { form.setValue("name", event.target.value);
form.setValue("name", event.target.value); if (symmetric) {
if (symmetric) { form.setValue("inverseName", event.target.value);
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"));
} }
}} }}
/> />
{!symmetric && ( </div>
<div className="space-y-2"> <SwitchField
<Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label> checked={symmetric}
<Input disabled={!!item}
id={`${id}-inverse`} id={`${id}-symmetric`}
maxLength={100} label={t("symmetric")}
required onCheckedChange={(isSymmetric) => {
{...form.register("inverseName")} form.setValue("symmetric", isSymmetric);
/> if (isSymmetric) {
</div> form.setValue("inverseName", form.getValues("name"));
)} }
}}
/>
{!symmetric && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${id}-description`}>{t("description")}</Label> <Label htmlFor={`${id}-inverse`}>{t("inverseName")}</Label>
<Textarea id={`${id}-description`} maxLength={1000} {...form.register("description")} /> <Input id={`${id}-inverse`} maxLength={100} required {...form.register("inverseName")} />
</div> </div>
<CatalogError )}
error={mutation.error} <div className="space-y-2">
invalid={Object.keys(form.formState.errors).length > 0} <Label htmlFor={`${id}-description`}>{t("description")}</Label>
/> <Textarea id={`${id}-description`} maxLength={1000} {...form.register("description")} />
<CatalogHelp kind="relation" scope={scope} /> </div>
</FormBody> <CatalogError
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")}

View File

@ -1,6 +1,5 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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";
@ -24,90 +23,86 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
const id = useId(); const id = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="grid gap-4 sm:grid-cols-2">
<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}-field`}>{t("selectField")}</Label> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<SelectWithAction <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
action={ </div>
allowQuickCreate <div className="space-y-2">
? { label: t("addField"), onClick: () => setCreatingField(true) } <Label htmlFor={`${id}-description`}>{t("description")}</Label>
: undefined <Textarea
} id={`${id}-description`}
disabled={mutation.isPending} maxLength={1000}
id={`${id}-field`} rows={2}
label={t("selectField")} {...form.register("description")}
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>
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>} </div>
<TemplateSectionsEditor <div className="space-y-2">
createId={() => UniqueID.generateNewID().toPrimitive()} <Label htmlFor={`${id}-field`}>{t("selectField")}</Label>
<SelectWithAction
action={
allowQuickCreate
? { label: t("addField"), onClick: () => setCreatingField(true) }
: undefined
}
disabled={mutation.isPending} disabled={mutation.isPending}
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))} id={`${id}-field`}
fieldNotes={Object.fromEntries( label={t("selectField")}
fields onValueChange={add}
.filter((field) => !field.effectiveActive) options={fields
.map((field) => [field.id, t("disabledField")]) .filter(
)} (entry) =>
fields={selected} entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
labels={{ )
title: t("sections.title"), .map((entry) => ({ value: entry.id, label: entry.values.name }))}
add: t("sections.add"), placeholder={t("choose")}
name: t("sections.name"), value=""
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") ?? []}
/> />
{form.formState.errors.sections && ( </div>
<p className="text-sm text-destructive" role="alert"> {!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
{t("errors.INVALID_SECTIONS")} <TemplateSectionsEditor
</p> createId={() => UniqueID.generateNewID().toPrimitive()}
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")])
)} )}
<CatalogError fields={selected}
error={mutation.error} labels={{
invalid={Object.keys(form.formState.errors).length > 0} title: t("sections.title"),
/> add: t("sections.add"),
<CatalogHelp kind="template" scope={scope} /> name: t("sections.name"),
</FormBody> 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") ?? []}
/>
{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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,81 +21,75 @@ export function SupportForm(props: SupportFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={prefix}>{t("support.tool")}</Label>
<Label htmlFor={prefix}>{t("support.tool")}</Label> <SelectWithAction
<SelectWithAction action={{ label: t("support.newTool"), onClick: () => setQuick(true) }}
action={{ label: t("support.newTool"), onClick: () => setQuick(true) }} id={prefix}
id={prefix} label={t("support.tool")}
label={t("support.tool")} onValueChange={(id) => form.setValue("toolId", id)}
onValueChange={(id) => form.setValue("toolId", id)} options={tools
options={tools .filter((item) => item.active || item.id === connection?.values.toolId)
.filter((item) => item.active || item.id === connection?.values.toolId) .map((item) => ({ value: item.id, label: item.name }))}
.map((item) => ({ value: item.id, label: item.name }))} placeholder={t("support.choose")}
placeholder={t("support.choose")} value={values.toolId}
value={values.toolId} />
/> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-destination`}>{t("support.destination")}</Label>
<Label htmlFor={`${prefix}-destination`}>{t("support.destination")}</Label> <Input id={`${prefix}-destination`} maxLength={300} {...form.register("destination")} />
<Input </div>
id={`${prefix}-destination`} <div className="space-y-2">
maxLength={300} <Label htmlFor={`${prefix}-state`}>{t("support.state")}</Label>
{...form.register("destination")} <SelectWithAction
/> id={`${prefix}-state`}
</div> label={t("support.state")}
<div className="space-y-2"> onValueChange={(state) => {
<Label htmlFor={`${prefix}-state`}>{t("support.state")}</Label> if (state === "pending" || state === "available" || state === "retired") {
<SelectWithAction form.setValue("state", state);
id={`${prefix}-state`} if (state !== "available") {
label={t("support.state")} form.setValue("preferred", false);
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) => ({ }}
value: state, options={["pending", "available", "retired"].map((state) => ({
label: t(`support.${state}`), value: state,
}))} label: t(`support.${state}`),
placeholder={t("support.chooseState")} }))}
value={values.state} placeholder={t("support.chooseState")}
/> value={values.state}
</div>
<SwitchField
checked={values.requiresVpn}
id={`${prefix}-requires-vpn`}
label={t("support.requiresVpn")}
onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
/> />
<SwitchField </div>
checked={values.preferred} <SwitchField
disabled={values.state !== "available"} checked={values.requiresVpn}
id={`${prefix}-preferred`} id={`${prefix}-requires-vpn`}
label={t("support.preferred")} label={t("support.requiresVpn")}
onCheckedChange={(preferred) => form.setValue("preferred", preferred)} onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
/>
<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>
</fieldset>
<SupportFeedback
error={mutation.error}
invalid={Object.keys(form.formState.errors).length > 0}
/> />
<SupportHelp /> <SwitchField
</FormBody> 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>
</fieldset>
<SupportFeedback
error={mutation.error}
invalid={Object.keys(form.formState.errors).length > 0}
/>
<SupportHelp />
<FormActions> <FormActions>
<Button disabled={mutation.isPending} type="submit"> <Button disabled={mutation.isPending} type="submit">
{t(mutation.isPending ? "saving" : "save")} {t(mutation.isPending ? "saving" : "save")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,32 +17,30 @@ export function SupportToolForm(props: SupportToolFormProps) {
const { t } = useTranslation("assets"); const { t } = useTranslation("assets");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={id}>{t("support.toolName")}</Label>
<Label htmlFor={id}>{t("support.toolName")}</Label> <Input
<Input id={id}
id={id} maxLength={100}
maxLength={100} readOnly={tool?.builtin}
readOnly={tool?.builtin} required
required {...form.register("name")}
{...form.register("name")}
/>
</div>
<SwitchField
checked={form.watch("active")}
id={`${id}-active`}
label={t("support.active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/> />
</fieldset> </div>
<SupportFeedback <SwitchField
error={mutation.error} checked={form.watch("active")}
invalid={Object.keys(form.formState.errors).length > 0} id={`${id}-active`}
label={t("support.active")}
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
/> />
<SupportHelp tool /> </fieldset>
</FormBody> <SupportFeedback
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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,145 +36,143 @@ export function WorkstationForm(props: WorkstationFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={prefix}>{t("name")}</Label>
<Label htmlFor={prefix}>{t("name")}</Label> <Input id={prefix} maxLength={100} required {...form.register("name")} />
<Input id={prefix} maxLength={100} required {...form.register("name")} /> </div>
</div> <SwitchField
<SwitchField checked={values.active}
checked={values.active} id={`${prefix}-active`}
id={`${prefix}-active`} label={t("active")}
label={t("active")} onCheckedChange={(active) => form.setValue("active", active)}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
<SelectWithAction
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 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>
{(["assetIds", "sharedAssetIds"] as const).map((field) => (
<div className="space-y-2" key={field}>
<Label htmlFor={`${prefix}-${field}`}>
{t(field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset")}
</Label>
<SelectWithAction
action={{
label: t("newAsset"),
onClick: () => {
setQuickField(field);
setQuick(true);
},
disabled: !catalog.data,
}}
disabled={inventory.isPending || values[field].length >= 200}
id={`${prefix}-${field}`}
label={t(
field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset"
)}
onValueChange={(id) => add(field, id)}
options={assets
.filter(
(item) => ![...values.assetIds, ...values.sharedAssetIds].includes(item.id)
)
.map((item) => ({
value: item.id,
label: unavailable(field, item.id)
? t("workstationOccupied", { name: item.values.name })
: item.values.name,
disabled: unavailable(field, item.id),
}))}
placeholder={t("chooseAsset")}
value=""
/>
{values[field].length ? (
<ul className="space-y-2">
{values[field].map((id) => (
<li
className="flex items-center justify-between gap-3 rounded-md border p-3 text-sm"
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"
>
×
</Button>
</li>
))}
</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> <div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-site`}>{t("site")}</Label>
<SelectWithAction
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 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>
{(["assetIds", "sharedAssetIds"] as const).map((field) => (
<div className="space-y-2" key={field}>
<Label htmlFor={`${prefix}-${field}`}>
{t(field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset")}
</Label>
<SelectWithAction
action={{
label: t("newAsset"),
onClick: () => {
setQuickField(field);
setQuick(true);
},
disabled: !catalog.data,
}}
disabled={inventory.isPending || values[field].length >= 200}
id={`${prefix}-${field}`}
label={t(
field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset"
)}
onValueChange={(id) => add(field, id)}
options={assets
.filter(
(item) => ![...values.assetIds, ...values.sharedAssetIds].includes(item.id)
)
.map((item) => ({
value: item.id,
label: unavailable(field, item.id)
? t("workstationOccupied", { name: item.values.name })
: item.values.name,
disabled: unavailable(field, item.id),
}))}
placeholder={t("chooseAsset")}
value=""
/>
{values[field].length ? (
<ul className="space-y-2">
{values[field].map((id) => (
<li
className="flex items-center justify-between gap-3 rounded-md border p-3 text-sm"
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"
>
×
</Button>
</li>
))}
</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")}
/>
<FormActions> <FormActions>
<Button <Button
disabled={ disabled={

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog"; import { FormActions, 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,50 +23,48 @@ export function QuickCreateDialog(props: QuickCreateDialogProps) {
open open
title={t(target.kind === "system" ? "quickAddSystem" : "quickAddEnvironment")} title={t(target.kind === "system" ? "quickAddSystem" : "quickAddEnvironment")}
> >
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> {target.kind === "environment" && (
{target.kind === "environment" && ( <p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground"> {t("quickEnvironmentContext", { name: target.systemName })}
{t("quickEnvironmentContext", { name: target.systemName })} </p>
</p> )}
)} <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={id}>{t("name")}</Label>
<Label htmlFor={id}>{t("name")}</Label> <Input
<Input id={id}
id={id} maxLength={100}
maxLength={100} required
required {...form.register("name", { validate: (value) => value.trim().length > 0 })}
{...form.register("name", { validate: (value) => value.trim().length > 0 })} />
/> {form.formState.errors.name && (
{form.formState.errors.name && (
<p className="text-destructive" role="alert">
{t("invalid")}
</p>
)}
</div>
{mutation.isError && (
<p className="text-destructive" role="alert"> <p className="text-destructive" role="alert">
{t( {t("invalid")}
`errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
)}
</p> </p>
)} )}
{existing && ( </div>
<div className="space-y-2"> {mutation.isError && (
<p>{t("quickExisting")}</p> <p className="text-destructive" role="alert">
<Button {t(
disabled={mutation.isPending} `errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
onClick={() => onCreated(existing.id)} )}
type="button" </p>
variant="outline" )}
> {existing && (
{t("quickUseExisting")} <div className="space-y-2">
</Button> <p>{t("quickExisting")}</p>
</div> <Button
)} disabled={mutation.isPending}
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p> onClick={() => onCreated(existing.id)}
<EntityHelp kind={target.kind} scope={scope} /> type="button"
</FormBody> variant="outline"
>
{t("quickUseExisting")}
</Button>
</div>
)}
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
<EntityHelp kind={target.kind} scope={scope} />
<FormActions> <FormActions>
<Button disabled={mutation.isPending} type="submit"> <Button disabled={mutation.isPending} type="submit">
{t(mutation.isPending ? "saving" : "quickSaveSelect")} {t(mutation.isPending ? "saving" : "quickSaveSelect")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,54 +37,52 @@ export function AccessFormFields(
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-2"> {editableFields.map((field) => (
{editableFields.map((field) => ( <div
<div className={["note", "permissions"].includes(field.type) ? "sm:col-span-2" : ""}
className={["note", "permissions"].includes(field.type) ? "sm:col-span-2" : ""} key={field.key}
key={field.key} >
> <AccessFieldControl
<AccessFieldControl createLabel={
createLabel={ field.key === "systemId" || field.key === "environmentId"
field.key === "systemId" || field.key === "environmentId" ? undefined
? undefined : t("quickAddEntry", { name: t(field.key) })
: t("quickAddEntry", { name: t(field.key) }) }
} disabled={
disabled={ Boolean(item && field.fixed) ||
Boolean(item && field.fixed) || lockedFields.includes(field.key) ||
lockedFields.includes(field.key) || mutation.isPending
mutation.isPending }
} field={field}
field={field} form={form}
form={form} id={`${prefix}-${field.key}`}
id={`${prefix}-${field.key}`} onCreate={quickAction(field)}
onCreate={quickAction(field)} onSelected={(value) => select(field, value)}
onSelected={(value) => select(field, value)} options={options(field)}
options={options(field)} value={watched[field.key]}
value={watched[field.key]} />
/> </div>
</div> ))}
))} </div>
</div> {validation && (
{validation && ( <p className="text-destructive" role="alert">
<p className="text-destructive" role="alert"> {validation}
{validation} </p>
</p> )}
)} {mutation.isError && (
{mutation.isError && ( <p className="text-destructive" role="alert">
<p className="text-destructive" role="alert"> {t(
{t( `errors.${mutation.error instanceof AccessApiError ? mutation.error.code : "INTERNAL_ERROR"}`
`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>}
{help[kind] && <p className="text-sm text-muted-foreground">{t(help[kind])}</p>} {["account_person", "membership", "grant"].includes(kind) && (
{["account_person", "membership", "grant"].includes(kind) && ( <p className="text-sm text-muted-foreground">{t("configurationHelp")}</p>
<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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,123 +45,121 @@ export function ApplicationConfigurationForm(props: ApplicationConfigurationForm
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="grid gap-4 sm:grid-cols-2">
<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}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<Textarea <SelectWithAction
id={`${prefix}-notes`} action={
maxLength={4000} fixed
onChange={(event) => form.setValue("notes", event.target.value || null)} ? undefined
rows={3} : { label: t("profiles.new"), onClick: () => setQuick("profile") }
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>
)} )}
{Object.keys(form.formState.errors).length > 0 && ( <div className="space-y-2">
<p className="text-sm text-destructive" role="alert"> <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
{t("applications.errors.INVALID_REQUEST")} <Textarea
</p> id={`${prefix}-notes`}
)} maxLength={4000}
<ApplicationFeedback onChange={(event) => form.setValue("notes", event.target.value || null)}
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error} rows={3}
/> value={values.notes ?? ""}
<ContextHelp />
description={t("applications.description")} </div>
examples={t("applications.examples")} </fieldset>
examplesLabel={t("mail.examplesLabel")} {configuration && (
guidance={t("applications.guidance")} <p className="text-sm text-muted-foreground">{t("applications.fixed")}</p>
scopeNote={t("applications.scope")} )}
title={t("applications.help")} {Object.keys(form.formState.errors).length > 0 && (
/> <p className="text-sm text-destructive" role="alert">
</FormBody> {t("applications.errors.INVALID_REQUEST")}
</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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,73 +21,69 @@ export function AssignmentForm(props: AssignmentFormProps) {
const id = useId(); const id = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<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}-department`}>{t("department")}</Label> <Label htmlFor={`${id}-start`}>{t("startDate")}</Label>
<SelectWithAction <Input id={`${id}-start`} required type="date" {...form.register("startDate")} />
action={ </div>
assignment <div className="space-y-2">
? undefined <Label htmlFor={`${id}-end`}>{t("endDate")}</Label>
: { <Input
label: t("newDepartment"), id={`${id}-end`}
onClick: () => setCreatingDepartment(true), min={form.watch("startDate")}
} 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 className="space-y-2"> </div>
<Label htmlFor={`${id}-position`}>{t("departmentPosition")}</Label> <SwitchField
<Input id={`${id}-position`} maxLength={100} {...form.register("position")} /> checked={!form.watch("endDate") && form.watch("isPrimary")}
</div> disabled={!!form.watch("endDate")}
<div className="grid gap-4 sm:grid-cols-2"> id={`${id}-primary`}
<div className="space-y-2"> label={t("primaryDepartment")}
<Label htmlFor={`${id}-start`}>{t("startDate")}</Label> onCheckedChange={(checked) => form.setValue("isPrimary", checked, { shouldDirty: true })}
<Input id={`${id}-start`} required type="date" {...form.register("startDate")} /> />
</div> <p className="text-xs text-muted-foreground">{t("assignmentHelp")}</p>
<div className="space-y-2"> <CompanyFeedback
<Label htmlFor={`${id}-end`}>{t("endDate")}</Label> error={mutation.error}
<Input invalid={Object.keys(form.formState.errors).length > 0}
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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,106 +44,104 @@ export function CertificateConfigurationForm(props: CertificateConfigurationForm
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="grid gap-4 sm:grid-cols-2">
<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}-location`}>{t("certificates.location")}</Label> <Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<Input <SelectWithAction
id={`${prefix}-location`} action={
maxLength={255} fixed
onChange={(event) => form.setValue("location", event.target.value || null)} ? undefined
value={values.location ?? ""} : { 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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-resource`}>{t("certificates.resource")}</Label>
<Textarea <SelectWithAction
id={`${prefix}-notes`} action={
maxLength={4000} configuration
onChange={(event) => form.setValue("notes", event.target.value || null)} ? undefined
rows={3} : {
value={values.notes ?? ""} 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>
</fieldset> <div className="space-y-2">
{configuration && ( <Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
<p className="text-sm text-muted-foreground">{t("certificates.fixed")}</p> <SelectWithAction
)} id={`${prefix}-state`}
{Object.keys(form.formState.errors).length > 0 && ( label={t("mail.state")}
<p className="text-sm text-destructive" role="alert"> onValueChange={changeState}
{t("certificates.errors.INVALID_REQUEST")} options={states.map((value) => ({ value, label: t(`mail.${value}`) }))}
</p> placeholder={t("mail.choose")}
)} required
<CertificateFeedback value={values.state}
error={mutation.error ?? profiles.error ?? resources.error ?? catalog.error} />
/> </div>
<ContextHelp </div>
description={t("certificates.description")} <div className="space-y-2">
examples={t("certificates.examples")} <Label htmlFor={`${prefix}-location`}>{t("certificates.location")}</Label>
examplesLabel={t("mail.examplesLabel")} <Input
guidance={t("certificates.guidance")} id={`${prefix}-location`}
scopeNote={t("certificates.scope")} maxLength={255}
title={t("certificates.help")} onChange={(event) => form.setValue("location", event.target.value || null)}
/> 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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,42 +20,40 @@ export function CompanyForm(props: CompanyFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> {showTitle && (
{showTitle && ( <h2 className="text-lg font-semibold">{t(company ? "editCompany" : "newCompany")}</h2>
<h2 className="text-lg font-semibold">{t(company ? "editCompany" : "newCompany")}</h2> )}
)} <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} /> </div>
</div> <SwitchField
<SwitchField checked={form.watch("active")}
checked={form.watch("active")} id={`${id}-active`}
id={`${id}-active`} label={t("activeCompany")}
label={t("activeCompany")} onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })} />
/> <CompanyLogoField
<CompanyLogoField companyId={savedCompanyId}
companyId={savedCompanyId} disabled={mutation.isPending}
disabled={mutation.isPending} draft={logoDraft}
draft={logoDraft} logoVersion={mutation.data ? mutation.data.company.logoVersion : company?.logoVersion}
logoVersion={mutation.data ? mutation.data.company.logoVersion : company?.logoVersion} name={form.watch("name")}
name={form.watch("name")} onChange={setLogoDraft}
onChange={setLogoDraft} />
/> {mutation.isError && partialSave && (
{mutation.isError && partialSave && ( <p className="text-sm text-muted-foreground" role="status">
<p className="text-sm text-muted-foreground" role="status"> {t("logo.partialSave")}
{t("logo.partialSave")} </p>
</p> )}
)} <CompanyFeedback
<CompanyFeedback error={mutation.error}
error={mutation.error} invalid={Object.keys(form.formState.errors).length > 0}
invalid={Object.keys(form.formState.errors).length > 0} success={mutation.isSuccess}
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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,29 +17,27 @@ export function DepartmentForm(props: DepartmentFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={id}>{t("name")}</Label>
<Label htmlFor={id}>{t("name")}</Label> <Input id={id} maxLength={100} required {...form.register("name")} />
<Input id={id} maxLength={100} required {...form.register("name")} /> </div>
</div> <SwitchField
<SwitchField checked={form.watch("active")}
checked={form.watch("active")} disabled={requireActive}
disabled={requireActive} id={`${id}-active`}
id={`${id}-active`} label={t("activeDepartment")}
label={t("activeDepartment")} onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })}
onCheckedChange={(checked) => form.setValue("active", checked, { shouldDirty: true })} />
/> {department && (
{department && ( <p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p>
<p className="text-sm text-muted-foreground">{t("departmentDeactivationHelp")}</p> )}
)} <CompanyFeedback
<CompanyFeedback error={mutation.error}
error={mutation.error} invalid={Object.keys(form.formState.errors).length > 0}
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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,95 +40,93 @@ export function FolderMappingForm(props: FolderMappingFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <FolderMappingTargets
<FolderMappingTargets company={company}
company={company} fixed={Boolean(mapping)}
fixed={Boolean(mapping)} form={form}
form={form} initialProfileId={initialProfileId}
initialProfileId={initialProfileId} items={items}
items={items} needs={needs}
needs={needs} onNeedChange={changeNeed}
onNeedChange={changeNeed} onQuickCreate={setQuick}
onQuickCreate={setQuick} prefix={prefix}
prefix={prefix} profile={profile}
profile={profile} profiles={profiles.data?.profiles ?? []}
profiles={profiles.data?.profiles ?? []} ready={ready}
ready={ready} />
/> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-path`}>{t("folders.path")}</Label>
<Label htmlFor={`${prefix}-path`}>{t("folders.path")}</Label> <Input id={`${prefix}-path`} readOnly value={values.path} />
<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>
{mapping && currentPath !== values.path && ( )}
<div className="rounded-md border p-3 text-sm space-y-2"> <div className="grid gap-4 sm:grid-cols-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"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-letter`}>{t("folders.letter")}</Label>
<Textarea <Input
id={`${prefix}-notes`} id={`${prefix}-letter`}
maxLength={4000} maxLength={2}
onChange={(event) => form.setValue("notes", event.target.value || null)} onChange={(event) => form.setValue("driveLetter", event.target.value || null)}
rows={3} placeholder="G:"
value={values.notes ?? ""} value={values.driveLetter ?? ""}
/> />
</div> </div>
</fieldset> <div className="space-y-2">
{Object.keys(form.formState.errors).length > 0 && ( <Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
<p role="alert">{t("folders.errors.INVALID_REQUEST")}</p> <SelectWithAction
)} id={`${prefix}-state`}
<FolderFeedback error={mutation.error ?? access.error ?? profiles.error} /> label={t("mail.state")}
<ContextHelp onValueChange={(state) => {
description={t("folders.description")} const parsed = FolderMappingInputSchema.shape.state.safeParse(state);
examples={t("folders.examples")} if (parsed.success) {
examplesLabel={t("mail.examplesLabel")} form.setValue("state", parsed.data);
guidance={t("folders.guidance")} }
scopeNote={t("folders.scope")} }}
title={t("folders.help")} options={states.map((state) => ({ value: state, label: t(`mail.${state}`) }))}
/> placeholder={t("mail.choose")}
</FormBody> 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")}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,57 +20,53 @@ export function InterventionTemplateForm(props: InterventionTemplateFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const prefix = useId(); const prefix = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> <Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-description`}>{t("interventionTemplates.description")}</Label>
<Label htmlFor={`${prefix}-description`}> <Textarea
{t("interventionTemplates.description")} id={`${prefix}-description`}
</Label> maxLength={4000}
<Textarea rows={3}
id={`${prefix}-description`} {...form.register("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 })}
/> />
<h3 className="font-medium">{t("interventionTemplates.steps")}</h3> </div>
{steps.fields.map((step, index) => ( <SwitchField
<TemplateStepFields checked={form.watch("active")}
count={steps.fields.length} id={`${prefix}-active`}
form={form} label={t("active")}
index={index} onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
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 /> <h3 className="font-medium">{t("interventionTemplates.steps")}</h3>
</FormBody> {steps.fields.map((step, index) => (
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,105 +52,103 @@ export function InterventionForm(props: InterventionFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <InterventionCompanyField
<InterventionCompanyField companies={data.companies}
companies={data.companies} companyId={companyId}
companyId={companyId} fixed={isCompanyFixed(scope, intervention, initialTicket)}
fixed={isCompanyFixed(scope, intervention, initialTicket)} onChange={changeCompany}
onChange={changeCompany} onNew={() => setQuick("company")}
onNew={() => setQuick("company")} prefix={prefix}
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}
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}
/> />
<InterventionHelp /> <div className="space-y-2">
</FormBody> <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}
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}
/>
<InterventionHelp />
<FormActions> <FormActions>
<Button <Button
disabled={mutation.isPending || !companyId || !lookups.data || lookups.isError} disabled={mutation.isPending || !companyId || !lookups.data || lookups.isError}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,73 +44,71 @@ export function MailConfigurationForm(props: MailConfigurationFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={disabled}>
<fieldset className="space-y-4" disabled={disabled}> {legacy && (
{legacy && ( <p className="rounded-md border p-3 text-sm">
<p className="rounded-md border p-3 text-sm"> {t("mail.legacyLink")} {legacy.address} · {personLabel(legacy.personId)} ·{" "}
{t("mail.legacyLink")} {legacy.address} · {personLabel(legacy.personId)} ·{" "} {assetLabel(legacy.assetId)}
{assetLabel(legacy.assetId)} </p>
</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}
states={states}
/>
<MailboxUsagePrompt
authorized={authorized ?? false}
mailbox={selectedMailbox}
onRegister={() => setQuick("use")}
profile={selectedProfile}
/>
<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>
{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 <MailConfigurationTargets
error={mutation.error ?? profiles.error ?? mailboxes.error ?? inventory.error} 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}
states={states}
/> />
<ContextHelp <MailboxUsagePrompt
description={t("mail.description")} authorized={authorized ?? false}
examples={t("mail.examples")} mailbox={selectedMailbox}
examplesLabel={t("mail.examplesLabel")} onRegister={() => setQuick("use")}
guidance={t("mail.guidance")} profile={selectedProfile}
scopeNote={t("mail.scope")}
title={t("mail.help")}
/> />
</FormBody> <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>
{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")}
/>
<FormActions> <FormActions>
<Button disabled={disabled || !ready} type="submit"> <Button disabled={disabled || !ready} type="submit">
{t(disabled ? "saving" : "save")} {t(disabled ? "saving" : "save")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,84 +31,82 @@ export function MailboxUseForm(props: MailboxUseFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<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}-mailbox`}>{t("mailbox.titleSingle")}</Label> <Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label>
<SelectWithAction <Input
action={ disabled={!!use}
canCreate(!!use, initialMailboxId, allowQuickCreate) id={`${prefix}-start`}
? { 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
value={values.mailboxId} type="date"
{...form.register("startDate")}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-person`}>{t("mail.person")}</Label> <Label htmlFor={`${prefix}-end`}>{t("mailbox.end")}</Label>
<SelectWithAction <Input
action={ id={`${prefix}-end`}
canCreate(!!use, initialPersonId, allowQuickCreate) onChange={(event) => form.setValue("endDate", event.target.value || null)}
? { label: t("newPerson"), onClick: () => setQuick("person") } required={!!use}
: undefined type="date"
} 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 className="grid gap-4 sm:grid-cols-2"> </div>
<div className="space-y-2"> </fieldset>
<Label htmlFor={`${prefix}-start`}>{t("mailbox.start")}</Label> {Object.keys(form.formState.errors).length > 0 && (
<Input <p role="alert">{t("mail.errors.INVALID_REQUEST")}</p>
disabled={!!use} )}
id={`${prefix}-start`} <MailFeedback error={mutation.error ?? query.error} />
required <ContextHelp
type="date" description={t("mailbox.useDescription")}
{...form.register("startDate")} examples={t("mailbox.useExamples")}
/> examplesLabel={t("mail.examplesLabel")}
</div> guidance={t("mailbox.useGuidance")}
<div className="space-y-2"> title={t("mailbox.useHelp")}
<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")}

View File

@ -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>

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,51 +17,49 @@ export function PersonForm(props: PersonFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="max-w-2xl space-y-6" onSubmit={submit}>
<FormBody> <div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-2"> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
<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>
<SwitchField <div className="space-y-2">
checked={form.watch("active")} <Label htmlFor={`${id}-position`}>{t("generalPosition")}</Label>
disabled={requireActive} <Input id={`${id}-position`} maxLength={100} {...form.register("position")} />
id={`${id}-active`} </div>
label={t("activePerson")} <div className="space-y-2">
onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })} <Label htmlFor={`${id}-email`}>{t("emailOptional")}</Label>
/> <Input id={`${id}-email`} maxLength={254} type="email" {...form.register("email")} />
<SwitchField </div>
checked={form.watch("isContact")} <div className="space-y-2">
id={`${id}-contact`} <Label htmlFor={`${id}-phone`}>{t("phoneOptional")}</Label>
label={t("isContact")} <Input id={`${id}-phone`} maxLength={50} type="tel" {...form.register("phone")} />
onCheckedChange={(isContact) => </div>
form.setValue("isContact", isContact, { shouldDirty: true }) </div>
} <SwitchField
/> checked={form.watch("active")}
{person?.active && !form.watch("active") && ( disabled={requireActive}
<p className="text-sm text-muted-foreground">{t("deactivationHelp")}</p> id={`${id}-active`}
)} label={t("activePerson")}
<CompanyFeedback onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
error={mutation.error} />
invalid={Object.keys(form.formState.errors).length > 0} <SwitchField
/> checked={form.watch("isContact")}
<p className="text-sm text-muted-foreground">{t("personHelp")}</p> id={`${id}-contact`}
<EntityHelp kind="person" /> label={t("isContact")}
</FormBody> onCheckedChange={(isContact) =>
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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,121 +47,118 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<SelectWithAction
action={
profileFixed
? 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);
}
}}
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>
<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"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-profile`}>{t("mail.profile")}</Label>
<Textarea <SelectWithAction
id={`${prefix}-notes`} action={
maxLength={4000} profileFixed
onChange={(event) => form.setValue("notes", event.target.value || null)} ? undefined
rows={3} : { label: t("profiles.new"), onClick: () => setQuick("profile") }
value={values.notes ?? ""} }
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>
</fieldset> <div className="space-y-2">
{configuration && <p className="text-sm text-muted-foreground">{t("printers.fixed")}</p>} <Label htmlFor={`${prefix}-printer`}>{t("printers.printer")}</Label>
{Object.keys(form.formState.errors).length > 0 && ( <SelectWithAction
<p className="text-sm text-destructive" role="alert"> action={
{t("printers.errors.INVALID_REQUEST")} configuration
</p> ? undefined
)} : { label: t("printers.newAsset"), onClick: () => setQuick("printer") }
<PrinterFeedback error={mutation.error ?? profiles.error ?? inventory.error} /> }
<ContextHelp disabled={!!configuration || !ready || !profile}
description={t("printers.description")} id={`${prefix}-printer`}
examples={t("printers.examples")} label={t("printers.printer")}
examplesLabel={t("mail.examplesLabel")} onValueChange={(id) => form.setValue("printerId", id)}
guidance={t("printers.guidance")} options={printerOptions(
scopeNote={t("printers.scope")} inventory.data?.assets ?? [],
title={t("printers.help")} 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>
<SwitchField
checked={values.isDefault}
disabled={!["pending", "configured"].includes(values.state)}
id={`${prefix}-default`}
label={t("printers.default")}
onCheckedChange={(isDefault) => form.setValue("isDefault", isDefault)}
/> />
</FormBody> <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("printers.fixed")}</p>}
{Object.keys(form.formState.errors).length > 0 && (
<p className="text-sm text-destructive" role="alert">
{t("printers.errors.INVALID_REQUEST")}
</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")}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,65 +18,58 @@ export function ProcessTemplateForm(props: ProcessTemplateFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const prefix = useId(); const prefix = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> <Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-kind`}>{t("processes.kind")}</Label>
<Label htmlFor={`${prefix}-kind`}>{t("processes.kind")}</Label> <SelectWithAction
<SelectWithAction id={`${prefix}-kind`}
id={`${prefix}-kind`} label={t("processes.kind")}
label={t("processes.kind")} onValueChange={(value) => {
onValueChange={(value) => { const parsed = ProcessTemplateInputSchema.shape.kind.safeParse(value);
const parsed = ProcessTemplateInputSchema.shape.kind.safeParse(value); if (parsed.success) {
if (parsed.success) { form.setValue("kind", parsed.data);
form.setValue("kind", parsed.data); }
} }}
}} options={["onboarding", "change", "offboarding"].map((value) => ({
options={["onboarding", "change", "offboarding"].map((value) => ({ value,
value, label: t(`processes.${value}`),
label: t(`processes.${value}`), }))}
}))} placeholder={t("mail.choose")}
placeholder={t("mail.choose")} value={form.watch("kind")}
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 className="space-y-2"> </div>
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <div className="space-y-2">
<Textarea <Label htmlFor={`${prefix}-instructions`}>{t("processes.instructions")}</Label>
id={`${prefix}-notes`} <Textarea
maxLength={4000} id={`${prefix}-instructions`}
rows={3} maxLength={16100}
{...form.register("notes")} required
/> rows={8}
</div> {...form.register("lines")}
</fieldset> />
<ProcessFeedback <p className="text-sm text-muted-foreground">{t("processes.instructionsHelp")}</p>
error={mutation.error} </div>
invalid={Object.keys(form.formState.errors).length > 0} <SwitchField
checked={form.watch("active")}
id={`${prefix}-active`}
label={t("active")}
onCheckedChange={(active) => form.setValue("active", active)}
/> />
<ProcessHelp template /> <div className="space-y-2">
</FormBody> <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}
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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,75 +20,73 @@ export function ProcessStartForm(props: ProcessStartFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> <Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} /> </div>
<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"> )}
<Label htmlFor={`${prefix}-person`}>{t("profiles.person")}</Label> <div className="space-y-2">
<SelectWithAction <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
action={{ label: t("newPerson"), onClick: () => setQuick("person") }} <Textarea
id={`${prefix}-person`} id={`${prefix}-notes`}
label={t("profiles.person")} maxLength={4000}
onValueChange={(id) => form.setValue("personId", id)} rows={3}
options={company.people.map((p) => ({ value: p.id, label: p.name }))} {...form.register("notes")}
placeholder={t("mail.choose")} />
required </div>
value={values.personId} </fieldset>
/> <ProcessFeedback
</div> error={mutation.error ?? query.error ?? review.error}
<div className="space-y-2"> invalid={Object.keys(form.formState.errors).length > 0}
<Label htmlFor={`${prefix}-template`}>{t("processes.template")}</Label> />
<SelectWithAction <ProcessHelp />
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={

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,52 +20,45 @@ export function ProcessEditForm(props: ProcessEditFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const prefix = useId(); const prefix = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> <Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} />
<Input id={`${prefix}-name`} maxLength={160} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label>
<Label htmlFor={`${prefix}-state`}>{t("mail.state")}</Label> <SelectWithAction
<SelectWithAction id={`${prefix}-state`}
id={`${prefix}-state`} label={t("mail.state")}
label={t("mail.state")} onValueChange={(value) => {
onValueChange={(value) => { const parsed = UpdateProcessInputSchema.shape.state.safeParse(value);
const parsed = UpdateProcessInputSchema.shape.state.safeParse(value); if (parsed.success) {
if (parsed.success) { form.setValue("state", parsed.data);
form.setValue("state", parsed.data); }
} }}
}} options={["open", "completed", "cancelled"].map((value) => ({
options={["open", "completed", "cancelled"].map((value) => ({ value,
value, label: t(`processes.${value}`),
label: t(`processes.${value}`), }))}
}))} placeholder={t("mail.choose")}
placeholder={t("mail.choose")} value={form.watch("state")}
value={form.watch("state")} />
/> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label>
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Textarea id={`${prefix}-notes`} maxLength={4000} rows={3} {...form.register("notes")} />
<Textarea </div>
id={`${prefix}-notes`} <p className="text-sm text-muted-foreground">{t("processes.resolutionHelp")}</p>
maxLength={4000} {process.values.tasks.map((task, index) => (
rows={3} <ProcessTaskField form={form} index={index} key={task.id} task={task} />
{...form.register("notes")} ))}
/> </fieldset>
</div> <ProcessFeedback
<p className="text-sm text-muted-foreground">{t("processes.resolutionHelp")}</p> error={mutation.error}
{process.values.tasks.map((task, index) => ( invalid={Object.keys(form.formState.errors).length > 0}
<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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,103 +40,101 @@ export function RemoteConnectionForm(props: RemoteConnectionFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-person`}>{t("remote.person")}</Label>
<Label htmlFor={`${prefix}-person`}>{t("remote.person")}</Label> <SelectWithAction
<SelectWithAction action={
action={ personFixed
personFixed ? undefined
? undefined : { label: t("newPerson"), onClick: () => setQuick("person") }
: { label: t("newPerson"), onClick: () => setQuick("person") } }
} disabled={personFixed}
disabled={personFixed} id={`${prefix}-person`}
id={`${prefix}-person`} label={t("remote.person")}
label={t("remote.person")} onValueChange={(id) => form.setValue("personId", id)}
onValueChange={(id) => form.setValue("personId", id)} options={company.people.map((p) => ({ value: p.id, label: p.name }))}
options={company.people.map((p) => ({ value: p.id, label: p.name }))} placeholder={t("mail.choose")}
placeholder={t("mail.choose")} required
required value={v.personId}
value={v.personId} />
/> </div>
</div> <div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-2"> <RemoteEndpointFields
<RemoteEndpointFields assetOptions={assetOptions}
assetOptions={assetOptions} company={company}
company={company} fixed={fixed}
fixed={fixed} form={form}
form={form} prefix={prefix}
prefix={prefix} ready={ready}
ready={ready} resourceOptions={resourceOptions}
resourceOptions={resourceOptions} setQuick={setQuick}
setQuick={setQuick}
/>
<div className="space-y-2">
<Label htmlFor={`${prefix}-method`}>{t("remote.method")}</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
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 className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("mail.notes")}</Label> <Label htmlFor={`${prefix}-method`}>{t("remote.method")}</Label>
<Textarea <Input
id={`${prefix}-notes`} id={`${prefix}-method`}
maxLength={4000} list={`${prefix}-methods`}
rows={3} maxLength={80}
{...form.register("notes")} 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>
</fieldset> </div>
{fixed && <p className="text-sm text-muted-foreground">{t("remote.fixed")}</p>} <SwitchField
{Object.keys(form.formState.errors).length > 0 && ( checked={v.requiresVpn}
<p className="text-sm text-destructive" role="alert"> id={`${prefix}-vpn`}
{t("remote.errors.INVALID_REQUEST")} label={t("remote.requiresVpn")}
</p> onCheckedChange={(requiresVpn) => form.setValue("requiresVpn", requiresVpn)}
)}
<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> <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>
{fixed && <p className="text-sm text-muted-foreground">{t("remote.fixed")}</p>}
{Object.keys(form.formState.errors).length > 0 && (
<p className="text-sm text-destructive" role="alert">
{t("remote.errors.INVALID_REQUEST")}
</p>
)}
<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")}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,32 +17,30 @@ export function SiteForm(props: SiteFormProps) {
const { t } = useTranslation("companies"); const { t } = useTranslation("companies");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> {showTitle && <h3 className="text-lg font-semibold">{t(site ? "editSite" : "newSite")}</h3>}
{showTitle && <h3 className="text-lg font-semibold">{t(site ? "editSite" : "newSite")}</h3>} <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-address`}>{t("addressOptional")}</Label>
<Label htmlFor={`${id}-address`}>{t("addressOptional")}</Label> <Textarea
<Textarea className="min-h-40"
className="min-h-40" id={`${id}-address`}
id={`${id}-address`} maxLength={10000}
maxLength={10000} rows={6}
rows={6} {...form.register("address")}
{...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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,83 +23,79 @@ export function TeleworkAuthorizationForm(props: TeleworkAuthorizationFormProps)
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${prefix}-person`}>{t("telework.person")}</Label>
<Label htmlFor={`${prefix}-person`}>{t("telework.person")}</Label> <SelectWithAction
<SelectWithAction action={fixed ? undefined : { label: t("newPerson"), onClick: () => setQuick(true) }}
action={ disabled={fixed}
fixed ? undefined : { label: t("newPerson"), onClick: () => setQuick(true) } id={`${prefix}-person`}
} label={t("telework.person")}
disabled={fixed} onValueChange={(value) => form.setValue("personId", value)}
id={`${prefix}-person`} options={company.people
label={t("telework.person")} .filter(
onValueChange={(value) => form.setValue("personId", value)} (person) =>
options={company.people person.id === values.personId || (person.active && !reserved.has(person.id))
.filter( )
(person) => .map((person) => ({ value: person.id, label: person.name }))}
person.id === values.personId || (person.active && !reserved.has(person.id)) placeholder={t("mail.choose")}
) required
.map((person) => ({ value: person.id, label: person.name }))} value={values.personId}
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 className="grid gap-4 sm:grid-cols-2"> </div>
<div className="space-y-2"> <SwitchField
<Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label> checked={values.active}
<Input id={`${prefix}-active`}
id={`${prefix}-start`} label={t("telework.active")}
onInput={(event) => form.setValue("startsOn", event.currentTarget.value || null)} onCheckedChange={(active) => form.setValue("active", active)}
type="date" />
value={values.startsOn ?? ""} <div className="grid gap-4 sm:grid-cols-2">
/>
</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"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("telework.notes")}</Label> <Label htmlFor={`${prefix}-start`}>{t("telework.startsOn")}</Label>
<Textarea <Input
id={`${prefix}-notes`} id={`${prefix}-start`}
maxLength={4000} onInput={(event) => form.setValue("startsOn", event.currentTarget.value || null)}
onChange={(event) => form.setValue("notes", event.target.value || null)} type="date"
rows={3} value={values.startsOn ?? ""}
value={values.notes ?? ""}
/> />
</div> </div>
</fieldset> <div className="space-y-2">
{authorization && <p className="text-sm text-muted-foreground">{t("telework.fixed")}</p>} <Label htmlFor={`${prefix}-end`}>{t("telework.expiresOn")}</Label>
{Object.keys(form.formState.errors).length > 0 && ( <Input
<p className="text-sm text-destructive" role="alert"> id={`${prefix}-end`}
{t("telework.errors.INVALID_REQUEST")} onInput={(event) => form.setValue("expiresOn", event.currentTarget.value || null)}
</p> type="date"
)} value={values.expiresOn ?? ""}
<TeleworkFeedback error={mutation.error} /> />
<ContextHelp </div>
description={t("telework.description")} </div>
examples={t("telework.examples")} <div className="space-y-2">
examplesLabel={t("mail.examplesLabel")} <Label htmlFor={`${prefix}-notes`}>{t("telework.notes")}</Label>
guidance={t("telework.guidance")} <Textarea
scopeNote={t("telework.scope")} id={`${prefix}-notes`}
title={t("telework.help")} maxLength={4000}
/> onChange={(event) => form.setValue("notes", event.target.value || null)}
</FormBody> 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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,110 +38,108 @@ export function TicketForm(props: TicketFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={mutation.isPending}>
<fieldset className="space-y-4" disabled={mutation.isPending}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={prefix}>{t("tickets.company")}</Label>
<Label htmlFor={prefix}>{t("tickets.company")}</Label> {scope || ticket ? (
{scope || ticket ? ( <p>{data.companies.find((item) => item.id === companyId)?.name ?? companyId}</p>
<p>{data.companies.find((item) => item.id === companyId)?.name ?? companyId}</p> ) : (
) : ( <>
<> <CompanySelect
<CompanySelect action={{ label: t("newCompany"), onClick: () => setQuick("company") }}
action={{ label: t("newCompany"), onClick: () => setQuick("company") }} id={prefix}
id={prefix} label={t("tickets.company")}
label={t("tickets.company")} onValueChange={changeCompany}
onValueChange={changeCompany} options={data.companies.map((item) => ({ value: item.id, label: item.name }))}
options={data.companies.map((item) => ({ value: item.id, label: item.name }))} placeholder={t("tickets.choose")}
placeholder={t("tickets.choose")} value={companyId}
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 ?? ""}
/> />
</div> <p className="text-xs text-muted-foreground">{t("tickets.companyNotice")}</p>
<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>
)} )}
{lookups.data && ( </div>
<TicketTargets <div className="space-y-2">
companyId={companyId} <Label htmlFor={`${prefix}-subject`}>{t("tickets.subject")}</Label>
csrfToken={csrfToken} <Input
lookups={lookups.data} id={`${prefix}-subject`}
onChange={setTargets} maxLength={200}
values={values.affected} 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 ?? ""}
/> />
)} </div>
</fieldset> <div className="space-y-2">
<TicketFeedback <Label htmlFor={`${prefix}-assignee`}>{t("tickets.assignee")}</Label>
error={mutation.error} <SelectWithAction
invalid={Object.keys(form.formState.errors).length > 0} id={`${prefix}-assignee`}
/> label={t("tickets.assignee")}
<TicketHelp /> onValueChange={(id) => form.setValue("assigneeId", id || null)}
</FormBody> 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>
)}
{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")}

View File

@ -1,5 +1,5 @@
import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { ContextHelp } from "@repo/rdx-ui/components/context-help";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,92 +35,90 @@ export function WorkProfileForm(props: WorkProfileFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-4" disabled={disabled}>
<fieldset className="space-y-4" disabled={disabled}> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={prefix}>{t("profiles.name")}</Label>
<Label htmlFor={prefix}>{t("profiles.name")}</Label> <Input id={prefix} maxLength={100} required {...form.register("name")} />
<Input id={prefix} maxLength={100} required {...form.register("name")} /> </div>
<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">
{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>
)}
<SwitchField
checked={values.active}
id={`${prefix}-active`}
label={t("profiles.active")}
onCheckedChange={(active) => form.setValue("active", active)}
/>
<div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
<Textarea
id={`${prefix}-notes`}
maxLength={4000}
onChange={(event) => form.setValue("notes", event.target.value || null)}
rows={3}
value={values.notes ?? ""}
/>
</div>
</fieldset>
{profile && <p className="text-sm text-muted-foreground">{t("profiles.fixed")}</p>}
{Object.keys(form.formState.errors).length > 0 && (
<p className="text-destructive" role="alert">
{t("profiles.errors.INVALID_REQUEST")}
</p>
)} )}
<ProfileFeedback error={mutation.error ?? access.error ?? inventory.error} /> <SwitchField
<ContextHelp checked={values.active}
description={t("profiles.description")} id={`${prefix}-active`}
examples={t("profiles.examples")} label={t("profiles.active")}
examplesLabel={t("profiles.examplesLabel")} onCheckedChange={(active) => form.setValue("active", active)}
guidance={t("profiles.guidance")}
scopeNote={t("profiles.scope")}
title={t("profiles.help")}
/> />
</FormBody> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("profiles.notes")}</Label>
<Textarea
id={`${prefix}-notes`}
maxLength={4000}
onChange={(event) => form.setValue("notes", event.target.value || null)}
rows={3}
value={values.notes ?? ""}
/>
</div>
</fieldset>
{profile && <p className="text-sm text-muted-foreground">{t("profiles.fixed")}</p>}
{Object.keys(form.formState.errors).length > 0 && (
<p className="text-destructive" role="alert">
{t("profiles.errors.INVALID_REQUEST")}
</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")}
/>
<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")}

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,49 +15,47 @@ export function PasswordForm(props: PasswordFormProps) {
const { t } = useTranslation("identity"); const { t } = useTranslation("identity");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> {showTitle && (
{showTitle && ( <h3 className="font-semibold">
<h3 className="font-semibold"> {t(target ? "resetPassword" : "changePassword")}
{t(target ? "resetPassword" : "changePassword")} {target ? ` · ${target.name}` : ""}
{target ? ` · ${target.name}` : ""} </h3>
</h3> )}
)} {!showTitle && target && <p className="text-muted-foreground">{target.name}</p>}
{!showTitle && target && <p className="text-muted-foreground">{target.name}</p>} {!target && (
{!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}-new`}>{t("newPassword")}</Label> <Label htmlFor={`${id}-current`}>{t("currentPassword")}</Label>
<Input <Input
autoComplete="new-password" autoComplete="current-password"
id={`${id}-new`} id={`${id}-current`}
maxLength={128} maxLength={128}
minLength={8}
required required
type="password" type="password"
{...form.register("password")} {...form.register("currentPassword")}
/> />
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
</div> </div>
{target && <p className="text-sm text-muted-foreground">{t("resetHelp")}</p>} )}
{Object.keys(form.formState.errors).length > 0 && ( <div className="space-y-2">
<p className="text-sm text-destructive" role="alert"> <Label htmlFor={`${id}-new`}>{t("newPassword")}</Label>
{t("invalidForm")} <Input
</p> autoComplete="new-password"
)} id={`${id}-new`}
<FormFeedback error={mutation.error} success={mutation.isSuccess} /> maxLength={128}
</FormBody> minLength={8}
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")}

View File

@ -24,7 +24,6 @@ export function UserEditorDialog(
} }
}} }}
open={props.open} open={props.open}
size="medium"
title={t( title={t(
props.mode === "create" props.mode === "create"
? "createUser" ? "createUser"

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,64 +16,62 @@ export function UserForm(props: UserFormProps) {
const { t } = useTranslation("identity"); const { t } = useTranslation("identity");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> {showTitle && <h3 className="font-semibold">{t(user ? "editUser" : "createUser")}</h3>}
{showTitle && <h3 className="font-semibold">{t(user ? "editUser" : "createUser")}</h3>} <div className="space-y-2">
<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}-name`}>{t("name")}</Label> <Label htmlFor={`${id}-password`}>{t("password")}</Label>
<Input <Input
autoComplete="name" autoComplete="new-password"
id={`${id}-name`} id={`${id}-password`}
maxLength={100} maxLength={128}
minLength={8}
required required
{...form.register("name")} type="password"
{...form.register("password")}
/> />
<p className="text-xs text-muted-foreground">{t("passwordHelp")}</p>
</div> </div>
<div className="space-y-2"> )}
<Label htmlFor={`${id}-email`}>{t("email")}</Label> {user && (
<Input <SwitchField
autoComplete="off" checked={form.watch("active")}
id={`${id}-email`} disabled={user.isInitialAdmin}
maxLength={254} id={`${id}-active`}
required label={t("active")}
type="email" onCheckedChange={(active) => form.setValue("active", active, { shouldDirty: true })}
{...form.register("email")} />
/> )}
</div> {user?.isInitialAdmin && (
{!user && ( <p className="text-xs text-muted-foreground">{t("adminProtected")}</p>
<div className="space-y-2"> )}
<Label htmlFor={`${id}-password`}>{t("password")}</Label> {Object.keys(form.formState.errors).length > 0 && (
<Input <p className="text-sm text-destructive" role="alert">
autoComplete="new-password" {t("invalidForm")}
id={`${id}-password`} </p>
maxLength={128} )}
minLength={8} <FormFeedback error={mutation.error} success={mutation.isSuccess} />
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")}

View File

@ -152,9 +152,5 @@
"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"
} }
} }

View File

@ -152,9 +152,5 @@
"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"
} }
} }

View File

@ -1,4 +1,4 @@
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,56 +19,54 @@ export function CatalogFieldForm(props: CatalogFieldFormProps) {
const { t } = useTranslation("resources"); const { t } = useTranslation("resources");
const id = useId(); const id = useId();
return ( return (
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<Label htmlFor={`${id}-name`}>{t("name")}</Label> <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
<Input id={`${id}-name`} maxLength={100} required {...form.register("name")} /> </div>
</div> <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={`${id}-type`}>{t("type")}</Label>
<Label htmlFor={`${id}-type`}>{t("type")}</Label> <SelectWithAction
<SelectWithAction disabled={!!item || mutation.isPending}
disabled={!!item || mutation.isPending} id={`${id}-type`}
id={`${id}-type`} label={t("type")}
label={t("type")} onValueChange={(value) => {
onValueChange={(value) => { const parsed = FieldTypeSchema.safeParse(value);
const parsed = FieldTypeSchema.safeParse(value); if (parsed.success) {
if (parsed.success) { form.setValue("type", parsed.data);
form.setValue("type", parsed.data); }
} }}
}} options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))}
options={FieldTypeSchema.options.map((value) => ({ value, label: t(value) }))} placeholder={t("chooseType")}
placeholder={t("chooseType")} required
required value={type}
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}
/> />
<CatalogHelp kind="field" scope={scope} /> </div>
</FormBody> <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}
/>
<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")}

View File

@ -1,6 +1,5 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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";
@ -24,112 +23,108 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
const id = useId(); const id = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-4" onSubmit={submit}>
<FormBody> <div className="grid gap-4 sm:grid-cols-2">
<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}-field`}>{t("selectField")}</Label> <Label htmlFor={`${id}-name`}>{t("name")}</Label>
<SelectWithAction <Input id={`${id}-name`} maxLength={100} required {...form.register("name")} />
action={ </div>
allowQuickCreate <div className="space-y-2">
? { label: t("addField"), onClick: () => setCreatingField(true) } <Label htmlFor={`${id}-description`}>{t("description")}</Label>
: undefined <Textarea
} id={`${id}-description`}
disabled={mutation.isPending} maxLength={1000}
id={`${id}-field`} rows={2}
label={t("selectField")} {...form.register("description")}
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>
{!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>} </div>
<TemplateSectionsEditor <div className="space-y-2">
createId={() => UniqueID.generateNewID().toPrimitive()} <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>
<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}
fieldNames={Object.fromEntries(fields.map((field) => [field.id, field.values.name]))} id={`${id}-field`}
fieldNotes={Object.fromEntries( label={t("selectField")}
fields onValueChange={add}
.filter((field) => !field.effectiveActive) options={fields
.map((field) => [field.id, t("disabledField")]) .filter(
)} (entry) =>
fields={selected} entry.effectiveActive && !selected.some((field) => field.fieldId === entry.id)
labels={{ )
title: t("sections.title"), .map((entry) => ({ value: entry.id, label: entry.values.name }))}
add: t("sections.add"), placeholder={t("choose")}
name: t("sections.name"), value=""
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") ?? []}
/> />
{form.formState.errors.sections && ( </div>
<p className="text-sm text-destructive" role="alert"> {!selected.length && <p className="text-sm text-muted-foreground">{t("noFields")}</p>}
{t("errors.INVALID_SECTIONS")} <TemplateSectionsEditor
</p> createId={() => UniqueID.generateNewID().toPrimitive()}
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")])
)} )}
<CatalogError fields={selected}
error={mutation.error} labels={{
invalid={Object.keys(form.formState.errors).length > 0} title: t("sections.title"),
/> add: t("sections.add"),
<CatalogHelp kind="template" scope={scope} /> name: t("sections.name"),
</FormBody> 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") ?? []}
/>
{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")}

View File

@ -1,7 +1,6 @@
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, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormActions } 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,115 +42,111 @@ export function ResourceForm(props: ResourceFormProps) {
const prefix = useId(); const prefix = useId();
return ( return (
<> <>
<form className="flex min-h-0 flex-1 flex-col overflow-hidden" onSubmit={submit}> <form className="space-y-5" onSubmit={submit}>
<FormBody> <fieldset className="space-y-5" disabled={mutation.isPending}>
<fieldset className="space-y-5" disabled={mutation.isPending}> <div className="space-y-2">
<FormSection title={t("formLayout.identity")}> <Label htmlFor={`${prefix}-name`}>{t("name")}</Label>
<div className="space-y-2"> <Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} />
<Label htmlFor={`${prefix}-name`}>{t("name")}</Label> </div>
<Input id={`${prefix}-name`} maxLength={100} required {...form.register("name")} /> <div className="grid gap-4 sm:grid-cols-2">
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
<SelectWithAction
action={
resource || !allowQuickCreate
? undefined
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
}
disabled={!!resource || mutation.isPending}
id={`${prefix}-template`}
label={t("template")}
onValueChange={selectTemplate}
options={
resource
? [
{
value: resource.values.templateId,
label: resource.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={["active", "inactive"].map((value) => ({ value, label: t(value) }))}
placeholder={t("chooseValue")}
required
value={values.active ? "active" : "inactive"}
/>
</div>
</div>
</FormSection>
<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]}
/>
)}
sections={sections}
ungroupedLabel={t("sections.ungrouped")}
/>
</section>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`${prefix}-notes`}>{t("notes")}</Label> <Label htmlFor={`${prefix}-template`}>{t("template")}</Label>
<Textarea <SelectWithAction
id={`${prefix}-notes`} action={
maxLength={ resource || !allowQuickCreate
values.integration || template?.builtinKey === "template:mailbox" ? 4000 : 10000 ? undefined
: { label: t("newTemplate"), onClick: () => setCreatingTemplate(true) }
} }
rows={3} disabled={!!resource || mutation.isPending}
{...form.register("notes")} id={`${prefix}-template`}
label={t("template")}
onValueChange={selectTemplate}
options={
resource
? [
{
value: resource.values.templateId,
label: resource.specification.templateName,
},
]
: templates.map((item) => ({ value: item.id, label: item.values.name }))
}
placeholder={t("chooseValue")}
required
value={values.templateId}
/> />
</div> </div>
</fieldset> <div className="space-y-2">
{Object.keys(form.formState.errors).length > 0 && ( <Label htmlFor={`${prefix}-status`}>{t("status")}</Label>
<p className="text-sm text-destructive" role="alert"> <SelectWithAction
{t("inventoryErrors.INVALID_REQUEST")} disabled={mutation.isPending}
</p> id={`${prefix}-status`}
)} label={t("status")}
<InventoryFeedback error={mutation.error ?? referenceQuery.error} /> onValueChange={changeStatus}
<ContextHelp options={["active", "inactive"].map((value) => ({ value, label: t(value) }))}
description={t(`${helpPrefix}Description`)} placeholder={t("chooseValue")}
examples={t(`${helpPrefix}Examples`)} required
examplesLabel={t("examplesLabel")} value={values.active ? "active" : "inactive"}
guidance={t(`${helpPrefix}Guidance`)} />
scopeNote={t(`${helpPrefix}Scope`)} </div>
title={t("helpTitle")} </div>
<ResourceIntegrationFields
fixed={!!resource}
integration={values.integration}
items={items}
onChange={(integration) =>
form.setValue("integration", integration, { shouldDirty: true })
}
references={referenceQuery.data?.references ?? []}
templateKey={template?.builtinKey}
/> />
</FormBody> {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]}
/>
)}
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")}
/>
<FormActions> <FormActions>
<Button disabled={mutation.isPending} type="submit"> <Button disabled={mutation.isPending} type="submit">
{t(mutation.isPending ? "saving" : "save")} {t(mutation.isPending ? "saving" : "save")}

View File

@ -29,13 +29,9 @@ 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 @min-[900px]:grid-cols-3"> <dl className="grid gap-4 sm:grid-cols-2">{fields.map(renderItem)}</dl>
{fields.map(renderItem)}
</dl>
) : ( ) : (
<div className="grid gap-4 sm:grid-cols-2 @min-[900px]:grid-cols-3"> <div className="grid gap-4 sm:grid-cols-2">{fields.map(renderItem)}</div>
{fields.map(renderItem)}
</div>
); );
if (!sections.length) { if (!sections.length) {
return content(items); return content(items);
@ -44,7 +40,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="grid items-start gap-4 @min-[900px]:grid-cols-2" className="space-y-3"
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) {
@ -62,7 +58,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
return null; return null;
} }
return ( return (
<details className="@container rounded-md border" key={section.id} open> <details className="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>
@ -76,7 +72,7 @@ export function FieldSections<T extends { sectionId?: string | undefined }>({
); );
})} })}
{ungrouped.length > 0 && ( {ungrouped.length > 0 && (
<section className="col-span-full space-y-3"> <section className="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>

View File

@ -18,7 +18,6 @@ export function FormDialog({
cancelLabel, cancelLabel,
children, children,
wide = false, wide = false,
size,
formActions = false, formActions = false,
}: { }: {
open: boolean; open: boolean;
@ -27,39 +26,28 @@ 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={`flex max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden p-0 ${widths[width]}`} className={`max-h-[calc(100dvh-2rem)] overflow-y-auto ${wide ? "sm:max-w-3xl" : "sm:max-w-xl"}`}
showCloseButton={true} showCloseButton={true}
> >
<DialogHeader className="shrink-0 border-b px-6 py-5 pr-14"> <DialogHeader>
<DialogTitle className="text-lg font-bold">{title}</DialogTitle> <DialogTitle className="text-lg font-bold">{title}</DialogTitle>
</DialogHeader> </DialogHeader>
<div <FormCancelContext.Provider value={formActions ? cancelLabel : undefined}>
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" {children}
data-slot="form-dialog-content" </FormCancelContext.Provider>
>
<FormCancelContext.Provider value={formActions ? cancelLabel : undefined}>
{children}
</FormCancelContext.Provider>
</div>
{!formActions && ( {!formActions && (
<DialogFooter className="shrink-0 border-t px-6 py-4"> <DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}> <DialogClose render={<Button type="button" variant="outline" />}>
{cancelLabel} {cancelLabel}
</DialogClose> </DialogClose>
</DialogFooter> </DialogFooter>
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
@ -68,7 +56,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="shrink-0 border-t bg-popover py-4"> <DialogFooter className="border-t pt-4">
{cancelLabel && ( {cancelLabel && (
<DialogClose render={<Button type="button" variant="outline" />}>{cancelLabel}</DialogClose> <DialogClose render={<Button type="button" variant="outline" />}>{cancelLabel}</DialogClose>
)} )}
@ -76,12 +64,3 @@ 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>
);
}

View File

@ -1,15 +0,0 @@
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>
);
}

View File

@ -87,82 +87,76 @@ 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>
<div className="grid items-start gap-4 @min-[900px]:grid-cols-2"> {sections.map((section, index) => (
{sections.map((section, index) => ( <fieldset className="space-y-3 rounded-md border p-3" disabled={disabled} key={section.id}>
<fieldset <legend className="sr-only">{section.name || labels.name}</legend>
className="space-y-3 rounded-md border p-3" <div className="flex items-end gap-2">
disabled={disabled} <div className="min-w-0 flex-1 space-y-2">
key={section.id} <Label htmlFor={`${prefix}-${section.id}-name`}>{labels.name}</Label>
> <Input
<legend className="sr-only">{section.name || labels.name}</legend> id={`${prefix}-${section.id}-name`}
<div className="flex items-end gap-2"> maxLength={100}
<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, description: event.target.value } : item item.id === section.id ? { ...item, name: event.target.value } : item
) )
) )
} }
rows={2} required
value={section.description} value={section.name}
/> />
</div> </div>
{fieldList(section.id)} <Button
</fieldset> aria-label={`${labels.moveUp}: ${section.name}`}
))} disabled={disabled || index === 0}
</div> 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) =>
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>
); );

View File

@ -5,111 +5,111 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
:root { :root {
--background: oklch(0.9911 0 0); --background: oklch(0.9916 0.0050 228.8162);
--foreground: oklch(0.1763 0.0140 258.3572); --foreground: oklch(0.2012 0.0307 210.2063);
--card: oklch(1.0000 0 0); --card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1763 0.0140 258.3572); --card-foreground: oklch(0.2012 0.0307 210.2063);
--popover: oklch(1.0000 0 0); --popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1763 0.0140 258.3572); --popover-foreground: oklch(0.2395 0.0455 282.4182);
--primary: oklch(0.5882 0.2309 28.2936); --primary: oklch(0.7040 0.1400 182.5030);
--primary-foreground: oklch(1.0000 0 0); --primary-foreground: oklch(0.9911 0 0);
--secondary: oklch(0.3303 0.0753 267.1046); --secondary: oklch(0.9507 0.0148 196.9072);
--secondary-foreground: oklch(1.0000 0 0); --secondary-foreground: oklch(0.4011 0.0399 210.3561);
--muted: oklch(0.9674 0.0013 286.3752); --muted: oklch(0.9604 0.0096 196.9971);
--muted-foreground: oklch(0.5517 0.0138 285.9385); --muted-foreground: oklch(0.4999 0.0296 211.7976);
--accent: oklch(0.8342 0.0950 7.8239); --accent: oklch(0.9104 0.0460 174.0259);
--accent-foreground: oklch(0.3155 0.1064 18.1157); --accent-foreground: oklch(0.4524 0.0772 194.7689);
--destructive: oklch(0.3999 0.1641 29.2339); --destructive: oklch(0.5993 0.1597 24.9573);
--destructive-foreground: oklch(1.0000 0 0); --destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.9197 0.0040 286.3202); --border: oklch(0.9093 0.0098 204.9097);
--input: oklch(0.9197 0.0040 286.3202); --input: oklch(0.9093 0.0098 204.9097);
--ring: oklch(0.5882 0.2309 28.2936); --ring: oklch(0.7040 0.1400 182.5030);
--chart-1: oklch(0.5882 0.2309 28.2936); --chart-1: oklch(0.7040 0.1400 182.5030);
--chart-2: oklch(0.3303 0.0753 267.1046); --chart-2: oklch(0.6700 0.1336 230.0356);
--chart-3: oklch(0.8342 0.0950 7.8239); --chart-3: oklch(0.7217 0.1638 297.0816);
--chart-4: oklch(0.8948 0.0509 64.2066); --chart-4: oklch(0.8614 0.1165 74.2273);
--chart-5: oklch(0.7118 0.0129 286.0665); --chart-5: oklch(0.8975 0.0740 131.2181);
--sidebar: oklch(1.0000 0 0); --sidebar: oklch(0.9794 0.0045 214.3282);
--sidebar-foreground: oklch(0.3703 0.0119 285.8054); --sidebar-foreground: oklch(0.2514 0.0307 209.6770);
--sidebar-primary: oklch(0.5882 0.2309 28.2936); --sidebar-primary: oklch(0.7040 0.1400 182.5030);
--sidebar-primary-foreground: oklch(1.0000 0 0); --sidebar-primary-foreground: oklch(0.9859 0.0040 286.3269);
--sidebar-accent: oklch(0.9674 0.0013 286.3752); --sidebar-accent: oklch(0.9104 0.0460 174.0259);
--sidebar-accent-foreground: oklch(0.1763 0.0140 258.3572); --sidebar-accent-foreground: oklch(0.2395 0.0455 282.4182);
--sidebar-border: oklch(0.9674 0.0013 286.3752); --sidebar-border: oklch(0.9202 0.0202 200.6310);
--sidebar-ring: oklch(0.5882 0.2309 28.2936); --sidebar-ring: oklch(0.5606 0.1650 288.1741);
--font-sans: 'Inter', sans-serif; --font-sans: Inter, sans-serif;
--font-serif: 'Georgia', serif; --font-serif: Georgia, serif;
--font-mono: 'JetBrains Mono', monospace; --font-mono: JetBrains Mono, monospace;
--radius: 0.75rem; --radius: 0.5rem;
--shadow-x: 0px; --shadow-x: 0px;
--shadow-y: 4px; --shadow-y: 4px;
--shadow-blur: 10px; --shadow-blur: 10px;
--shadow-spread: -2px; --shadow-spread: 0px;
--shadow-opacity: 0.1; --shadow-opacity: 0.12;
--shadow-color: 0 0% 0%; --shadow-color: #2d2d53;
--shadow-2xs: 0px 4px 10px -2px hsl(0 0% 0% / 0.05); --shadow-2xs: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.06);
--shadow-xs: 0px 4px 10px -2px hsl(0 0% 0% / 0.05); --shadow-xs: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.06);
--shadow-sm: 0px 4px 10px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10); --shadow-sm: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 1px 2px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow: 0px 4px 10px -2px hsl(0 0% 0% / 0.10), 0px 1px 2px -3px hsl(0 0% 0% / 0.10); --shadow: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 1px 2px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-md: 0px 4px 10px -2px hsl(0 0% 0% / 0.10), 0px 2px 4px -3px hsl(0 0% 0% / 0.10); --shadow-md: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 2px 4px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-lg: 0px 4px 10px -2px hsl(0 0% 0% / 0.10), 0px 4px 6px -3px hsl(0 0% 0% / 0.10); --shadow-lg: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 4px 6px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-xl: 0px 4px 10px -2px hsl(0 0% 0% / 0.10), 0px 8px 10px -3px hsl(0 0% 0% / 0.10); --shadow-xl: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 8px 10px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-2xl: 0px 4px 10px -2px hsl(0 0% 0% / 0.25); --shadow-2xl: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.30);
--tracking-normal: -0.02em; --tracking-normal: 0em;
--spacing: 0.25rem; --spacing: 0.25rem;
} }
.dark { .dark {
--background: oklch(0.1645 0.0086 274.3354); --background: oklch(0.1396 0.0155 195.5244);
--foreground: oklch(0.9911 0 0); --foreground: oklch(0.9396 0.0097 204.9127);
--card: oklch(0.2145 0.0184 270.4182); --card: oklch(0.1788 0.0202 201.3265);
--card-foreground: oklch(0.9911 0 0); --card-foreground: oklch(0.9396 0.0097 204.9127);
--popover: oklch(0.2145 0.0184 270.4182); --popover: oklch(0.2101 0.0257 280.5303);
--popover-foreground: oklch(0.9911 0 0); --popover-foreground: oklch(0.9294 0.0202 286.0004);
--primary: oklch(0.6453 0.2404 27.3106); --primary: oklch(0.7040 0.1400 182.5030);
--primary-foreground: oklch(1.0000 0 0); --primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.2604 0.0471 267.4902); --secondary: oklch(0.2413 0.0307 198.8697);
--secondary-foreground: oklch(1.0000 0 0); --secondary-foreground: oklch(0.8496 0.0299 199.2418);
--muted: oklch(0.2739 0.0055 286.0326); --muted: oklch(0.2185 0.0195 201.5676);
--muted-foreground: oklch(0.7118 0.0129 286.0665); --muted-foreground: oklch(0.6494 0.0304 199.2066);
--accent: oklch(0.6986 0.1954 14.1660); --accent: oklch(0.4370 0.0780 188.2160);
--accent-foreground: oklch(1.0000 0 0); --accent-foreground: oklch(0.7994 0.0596 196.0230);
--destructive: oklch(0.3958 0.1331 25.7230); --destructive: oklch(0.6593 0.2097 25.0107);
--destructive-foreground: oklch(1.0000 0 0); --destructive-foreground: oklch(0.9859 0.0040 286.3269);
--border: oklch(0.2739 0.0055 286.0326); --border: oklch(0.2397 0.0193 201.5913);
--input: oklch(0.2739 0.0055 286.0326); --input: oklch(0.2397 0.0193 201.5913);
--ring: oklch(0.6453 0.2404 27.3106); --ring: oklch(0.7040 0.1400 182.5030);
--chart-1: oklch(0.6453 0.2404 27.3106); --chart-1: oklch(0.7040 0.1400 182.5030);
--chart-2: oklch(0.3986 0.0946 266.8132); --chart-2: oklch(0.6700 0.1336 230.0356);
--chart-3: oklch(0.7570 0.1485 11.9623); --chart-3: oklch(0.7217 0.1638 297.0816);
--chart-4: oklch(0.7824 0.0829 60.0948); --chart-4: oklch(0.8614 0.1165 74.2273);
--chart-5: oklch(0.4419 0.0146 285.7864); --chart-5: oklch(0.8975 0.0740 131.2181);
--sidebar: oklch(0.1645 0.0086 274.3354); --sidebar: oklch(0.1604 0.0209 201.0807);
--sidebar-foreground: oklch(0.7118 0.0129 286.0665); --sidebar-foreground: oklch(0.9396 0.0097 204.9127);
--sidebar-primary: oklch(0.6453 0.2404 27.3106); --sidebar-primary: oklch(0.7040 0.1400 182.5030);
--sidebar-primary-foreground: oklch(1.0000 0 0); --sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.2145 0.0184 270.4182); --sidebar-accent: oklch(0.4370 0.0780 188.2160);
--sidebar-accent-foreground: oklch(0.9911 0 0); --sidebar-accent-foreground: oklch(0.9294 0.0202 286.0004);
--sidebar-border: oklch(0.2739 0.0055 286.0326); --sidebar-border: oklch(0.3008 0.0343 281.7840);
--sidebar-ring: oklch(0.6453 0.2404 27.3106); --sidebar-ring: oklch(0.7155 0.1583 288.7763);
--font-sans: 'Inter', sans-serif; --font-sans: Inter, sans-serif;
--font-serif: 'Georgia', serif; --font-serif: Georgia, serif;
--font-mono: 'JetBrains Mono', monospace; --font-mono: JetBrains Mono, monospace;
--radius: 0.75rem; --radius: 0.5rem;
--shadow-x: 0px; --shadow-x: 0px;
--shadow-y: 8px; --shadow-y: 4px;
--shadow-blur: 15px; --shadow-blur: 10px;
--shadow-spread: -3px; --shadow-spread: 0px;
--shadow-opacity: 0.4; --shadow-opacity: 0.12;
--shadow-color: 0 0% 0%; --shadow-color: #2d2d53;
--shadow-2xs: 0px 8px 15px -3px hsl(0 0% 0% / 0.20); --shadow-2xs: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.06);
--shadow-xs: 0px 8px 15px -3px hsl(0 0% 0% / 0.20); --shadow-xs: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.06);
--shadow-sm: 0px 8px 15px -3px hsl(0 0% 0% / 0.40), 0px 1px 2px -4px hsl(0 0% 0% / 0.40); --shadow-sm: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 1px 2px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow: 0px 8px 15px -3px hsl(0 0% 0% / 0.40), 0px 1px 2px -4px hsl(0 0% 0% / 0.40); --shadow: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 1px 2px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-md: 0px 8px 15px -3px hsl(0 0% 0% / 0.40), 0px 2px 4px -4px hsl(0 0% 0% / 0.40); --shadow-md: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 2px 4px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-lg: 0px 8px 15px -3px hsl(0 0% 0% / 0.40), 0px 4px 6px -4px hsl(0 0% 0% / 0.40); --shadow-lg: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 4px 6px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-xl: 0px 8px 15px -3px hsl(0 0% 0% / 0.40), 0px 8px 10px -4px hsl(0 0% 0% / 0.40); --shadow-xl: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.12), 0px 8px 10px -1px hsl(240 29.6875% 25.0980% / 0.12);
--shadow-2xl: 0px 8px 15px -3px hsl(0 0% 0% / 1.00); --shadow-2xl: 0px 4px 10px 0px hsl(240 29.6875% 25.0980% / 0.30);
} }
@theme inline { @theme inline {
@ -163,13 +163,6 @@
--shadow-lg: var(--shadow-lg); --shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl); --shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl); --shadow-2xl: var(--shadow-2xl);
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-normal: var(--tracking-normal);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
} }
@layer base { @layer base {
@ -178,6 +171,5 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
letter-spacing: var(--tracking-normal);
} }
} }