Compare commits

..

No commits in common. "f945e9a996c991e5c54f4f4ce6c000cb83d79fc9" and "d7eb91ee68cfb87028ce812b5e9092bf4f688d92" have entirely different histories.

42 changed files with 41 additions and 309 deletions

View File

@ -58,49 +58,3 @@
--spacing-shell-avatar: 35px; --spacing-shell-avatar: 35px;
--shadow-shell-popover: 0 3px 12px rgb(12 40 80 / 8%); --shadow-shell-popover: 0 3px 12px rgb(12 40 80 / 8%);
} }
/* Enlaces de contenido y controles que navegan: criterio común en toda la aplicación. */
@layer components {
.app-link {
color: var(--primary);
cursor: pointer;
text-underline-offset: 4px;
}
.app-link:hover {
text-decoration-line: underline;
}
.app-link:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 3px;
}
.app-link:disabled,
.app-link[data-disabled],
.app-link[aria-disabled="true"] {
cursor: default;
text-decoration-line: none;
}
}
/* Los diálogos necesitan separarse visualmente del fondo y de la capa de
superposición sin depender de una variante concreta de formulario. */
[data-slot="dialog-content"] {
box-shadow:
0 18px 45px rgb(12 40 80 / 16%),
0 4px 12px rgb(12 40 80 / 10%);
}
/* El diálogo activo necesita un límite inequívoco cuando aparece sobre otro. */
[data-slot="dialog-content"] {
border: 1px solid color-mix(in oklch, var(--foreground) 1%, var(--border));
box-shadow:
0 24px 60px rgb(12 40 80 / 24%),
0 8px 18px rgb(12 40 80 / 14%),
0 0 0 1px rgb(12 40 80 / 8%);
}
[data-slot="dialog-content"] [data-slot="dialog-header"] {
background-color: var(--popover);
}

View File

@ -309,11 +309,3 @@ Las secciones de consulta evitan recuadros anidados y rellenos laterales: usan t
`DetailSection` ofrece agrupación plegable nativa (`details/summary`), abierta inicialmente, accesible por teclado. `DetailField` devuelve una fila semántica `dt/dd` para incluir en un `dl`: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto. `DetailSection` ofrece agrupación plegable nativa (`details/summary`), abierta inicialmente, accesible por teclado. `DetailField` devuelve una fila semántica `dt/dd` para incluir en un `dl`: etiqueta y valor se alinean horizontalmente cuando el contenedor dispone de al menos 240 px y se apilan en espacios menores. El consumidor aporta los enlaces; el componente común les aplica cursor de enlace, color semántico y subrayado al pasar el puntero. Los valores ausentes permanecen como texto.
La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: persona y activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas. La ficha lateral de activos utiliza un ancho adaptable entre 320 y 400 px, acciones debajo del título y bloques de identificación, ubicación/asignación, características, observaciones, relaciones y asistencia. Conserva las secciones capturadas de la plantilla, incluyendo la compatibilidad con especificaciones antiguas sin agrupación; no modifica valores ni instantáneas. Las relaciones llevan a destinos existentes: persona y activo relacionado a su ficha; sede, puesto y plantilla a sus listados/catálogos actuales. La vista completa e historial mantienen sus funciones. Las fichas de recursos reutilizan las filas de características. No se crean nuevas fichas laterales ni nuevas rutas.
## Enlaces y controles de navegación
Los enlaces de contenido usan `app-link`: color semántico `primary`, cursor de mano, subrayado al pasar el puntero y foco visible por teclado. `BrowserLink` lo incorpora de forma predeterminada, conservando la navegación interna, los clics modificados, destinos externos y descargas. Los botones que presentan nombres navegables o «Ver todos» comparten este estilo sin alterar sus callbacks. Los controles deshabilitados no muestran cursor de enlace ni subrayado. La navegación lateral y los enlaces presentados como botones conservan los colores de su contexto para mantener contraste; las acciones de guardar, editar o cancelar mantienen su presentación de botón.
En el menú de secciones de una compañía, los elementos utilizan el color normal de texto y solo destacan su texto al pasar el puntero. Se mantienen el cursor de mano, el foco por teclado y el indicador de la sección actual. Esta excepción no afecta a enlaces dentro de listados, fichas o bloques del dashboard.
El cuerpo desplazable `FormBody` reserva 4 px interiores en ambos laterales y al inicio para el anillo exterior de foco (3 px). No debe eliminarse ese espacio: `overflow-y-auto` también recorta horizontalmente y produciría un foco asimétrico en campos pegados al borde. Se conserva el scroll y las acciones fijas.

View File

@ -22,7 +22,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
<p className="font-medium"> <p className="font-medium">
{source ? link.values.name : link.values.inverseName} ·{" "} {source ? link.values.name : link.values.inverseName} ·{" "}
<BrowserLink <BrowserLink
className="cursor-pointer text-primary underline-offset-4 app-link text-primary hover:text-primary" className="cursor-pointer text-primary underline-offset-4 hover:underline"
href={`/companies/${companyId}/assets/${source ? link.values.targetId : link.values.sourceId}`} href={`/companies/${companyId}/assets/${source ? link.values.targetId : link.values.sourceId}`}
> >
{nameOf(source ? link.values.targetId : link.values.sourceId)} {nameOf(source ? link.values.targetId : link.values.sourceId)}

View File

@ -100,7 +100,7 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
render: (asset) => ( render: (asset) => (
<BrowserLink <BrowserLink
aria-pressed={selected?.id === asset.id && detailOpen} aria-pressed={selected?.id === asset.id && detailOpen}
className="cursor-pointer text-left font-medium [overflow-wrap:anywhere] app-link text-primary hover:text-primary focus-visible:rounded-[4px] focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-4 focus-visible:outline-ring" className="cursor-pointer text-left font-medium [overflow-wrap:anywhere] hover:underline focus-visible:rounded-[4px] focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-4 focus-visible:outline-ring"
href={entityHref(asset.id)} href={entityHref(asset.id)}
onClick={() => setDetailOpen(true)} onClick={() => setDetailOpen(true)}
> >

View File

@ -21,7 +21,7 @@ export function CatalogName({
return ( return (
<> <>
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={onView} onClick={onView}
type="button" type="button"
> >

View File

@ -68,7 +68,7 @@ export function WorkstationsPage(props: WorkstationsPageProps) {
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -12,10 +12,4 @@ export interface ICompanyRepository {
id: UniqueID, id: UniqueID,
change: (company: Company) => Result<Company, CompanyError> change: (company: Company) => Result<Company, CompanyError>
): Promise<Result<Company, CompanyError>>; ): Promise<Result<Company, CompanyError>>;
removePerson(id: UniqueID, personId: UniqueID): Promise<Result<Company, CompanyError>>;
removeAssignment(
id: UniqueID,
personId: UniqueID,
assignmentId: UniqueID
): Promise<Result<Company, CompanyError>>;
} }

View File

@ -97,14 +97,6 @@ export class CompanyUseCases {
company.updatePerson(child.data, props.data) company.updatePerson(child.data, props.data)
); );
} }
async removePerson(companyId: string, id: string) {
const parent = this.input.id(companyId);
const child = this.input.id(id);
if (!(parent.isSuccess && child.isSuccess)) {
return Result.fail(new CompanyError("INVALID_REQUEST"));
}
return this.companies.removePerson(parent.data, child.data);
}
async addDepartment(id: string, dto: DepartmentRequestDTO) { async addDepartment(id: string, dto: DepartmentRequestDTO) {
const parsed = this.input.id(id); const parsed = this.input.id(id);
const props = this.input.department(dto); const props = this.input.department(dto);
@ -156,15 +148,6 @@ export class CompanyUseCases {
company.updateAssignment(person.data, child.data, values.data, new Date().toISOString()) company.updateAssignment(person.data, child.data, values.data, new Date().toISOString())
); );
} }
async removeAssignment(companyId: string, personId: string, id: string) {
const parent = this.input.id(companyId);
const person = this.input.id(personId);
const assignment = this.input.id(id);
if (!(parent.isSuccess && person.isSuccess && assignment.isSuccess)) {
return Result.fail(new CompanyError("INVALID_REQUEST"));
}
return this.companies.removeAssignment(parent.data, person.data, assignment.data);
}
async addSite(id: string, dto: SiteRequestDTO) { async addSite(id: string, dto: SiteRequestDTO) {
const parsed = this.input.id(id); const parsed = this.input.id(id);
const props = this.input.site(dto); const props = this.input.site(dto);

View File

@ -4,11 +4,9 @@ export type CompanyErrorCode =
| "CONTACT_NOT_FOUND" | "CONTACT_NOT_FOUND"
| "SITE_NOT_FOUND" | "SITE_NOT_FOUND"
| "PERSON_NOT_FOUND" | "PERSON_NOT_FOUND"
| "PERSON_IN_USE"
| "DEPARTMENT_NOT_FOUND" | "DEPARTMENT_NOT_FOUND"
| "DEPARTMENT_NAME_EXISTS" | "DEPARTMENT_NAME_EXISTS"
| "ASSIGNMENT_NOT_FOUND" | "ASSIGNMENT_NOT_FOUND"
| "ASSIGNMENT_IN_USE"
| "ASSIGNMENT_CONFLICT" | "ASSIGNMENT_CONFLICT"
| "INACTIVE_ASSIGNMENT_TARGET"; | "INACTIVE_ASSIGNMENT_TARGET";
export class CompanyError extends Error { export class CompanyError extends Error {

View File

@ -109,20 +109,6 @@ export class Company extends AggregateRoot<CompanyProps> {
) )
); );
} }
removePerson(id: UniqueID): Result<Company, CompanyError> {
if (!this.people.some((person) => person.id.equals(id))) {
return Result.fail(new CompanyError("PERSON_NOT_FOUND"));
}
if (this.assignments.some((assignment) => assignment.personId.equals(id))) {
return Result.fail(new CompanyError("PERSON_IN_USE"));
}
return Result.ok(
new Company(
{ ...this.props, people: this.people.filter((person) => !person.id.equals(id)) },
this.id
)
);
}
private departmentNameExists(name: string, except?: UniqueID): boolean { private departmentNameExists(name: string, except?: UniqueID): boolean {
const key = name.normalize("NFC").toLowerCase(); const key = name.normalize("NFC").toLowerCase();
return this.departments.some( return this.departments.some(
@ -270,20 +256,6 @@ export class Company extends AggregateRoot<CompanyProps> {
) )
); );
} }
removeAssignment(personId: UniqueID, id: UniqueID): Result<Company, CompanyError> {
const assignment = this.assignments.find(
(item) => item.id.equals(id) && item.personId.equals(personId)
);
if (!assignment) {
return Result.fail(new CompanyError("ASSIGNMENT_NOT_FOUND"));
}
return Result.ok(
new Company(
{ ...this.props, assignments: this.assignments.filter((item) => !item.id.equals(id)) },
this.id
)
);
}
addSite(site: Site): Result<Company, CompanyError> { addSite(site: Site): Result<Company, CompanyError> {
if (!site.companyId.equals(this.id)) { if (!site.companyId.equals(this.id)) {
return Result.fail(new CompanyError("INVALID_REQUEST")); return Result.fail(new CompanyError("INVALID_REQUEST"));

View File

@ -126,17 +126,6 @@ export function registerCompanyRoutes(
post("/:companyId/people/:id/update", PersonRequestSchema, ChildParamsSchema, (dto, params) => post("/:companyId/people/:id/update", PersonRequestSchema, ChildParamsSchema, (dto, params) =>
useCases.updatePerson(params.companyId, params.id, dto) useCases.updatePerson(params.companyId, params.id, dto)
); );
router.post("/:companyId/people/:id/delete", (request, response, next) => {
const params = ChildParamsSchema.safeParse(request.params);
if (!(params.success && z.strictObject({}).safeParse(request.body).success)) {
sendError(response, "INVALID_REQUEST", 400);
return;
}
useCases
.removePerson(params.data.companyId, params.data.id)
.then((result) => sendResult(response, result))
.catch(next);
});
post( post(
"/:companyId/departments", "/:companyId/departments",
DepartmentRequestSchema, DepartmentRequestSchema,
@ -157,17 +146,6 @@ export function registerCompanyRoutes(
(dto, params) => useCases.addAssignment(params.companyId, params.personId, dto), (dto, params) => useCases.addAssignment(params.companyId, params.personId, dto),
201 201
); );
router.post("/:companyId/people/:personId/assignments/:id/delete", (request, response, next) => {
const params = AssignmentParamsSchema.safeParse(request.params);
if (!(params.success && z.strictObject({}).safeParse(request.body).success)) {
sendError(response, "INVALID_REQUEST", 400);
return;
}
useCases
.removeAssignment(params.data.companyId, params.data.personId, params.data.id)
.then((result) => sendResult(response, result))
.catch(next);
});
post( post(
"/:companyId/people/:personId/assignments/:id/update", "/:companyId/people/:personId/assignments/:id/update",
AssignmentUpdateRequestSchema, AssignmentUpdateRequestSchema,

View File

@ -102,55 +102,6 @@ export class SequelizeCompanyRepository implements ICompanyRepository {
await CompanyModel.create(this.mapper.toPersistence(company).company); await CompanyModel.create(this.mapper.toPersistence(company).company);
return company; return company;
} }
async removePerson(
id: UniqueID,
personId: UniqueID
): Promise<DomainResult<Company, CompanyError>> {
return this.database.transaction(async (transaction) => {
const current = (await this.load(id, transaction, true)).getOrUndefined();
if (!current) {
return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
}
const changed = current.removePerson(personId);
if (!changed.isSuccess) {
return changed;
}
try {
await PersonModel.destroy({
where: { id: personId.toPrimitive(), companyId: id.toPrimitive() },
transaction,
});
} catch {
return Result.fail(new CompanyError("PERSON_IN_USE"));
}
return changed;
});
}
async removeAssignment(
id: UniqueID,
personId: UniqueID,
assignmentId: UniqueID
): Promise<DomainResult<Company, CompanyError>> {
return this.database.transaction(async (transaction) => {
const current = (await this.load(id, transaction, true)).getOrUndefined();
if (!current) {
return Result.fail(new CompanyError("COMPANY_NOT_FOUND"));
}
const changed = current.removeAssignment(personId, assignmentId);
if (!changed.isSuccess) {
return changed;
}
await AssignmentRevisionModel.destroy({
where: { assignmentId: assignmentId.toPrimitive() },
transaction,
});
await AssignmentModel.destroy({
where: { id: assignmentId.toPrimitive(), companyId: id.toPrimitive() },
transaction,
});
return changed;
});
}
async update( async update(
id: UniqueID, id: UniqueID,
change: (company: Company) => DomainResult<Company, CompanyError> change: (company: Company) => DomainResult<Company, CompanyError>

View File

@ -909,15 +909,7 @@
"removed": "The logo will be removed on save.", "removed": "The logo will be removed on save.",
"remove": "Remove logo", "remove": "Remove logo",
"undo": "Undo logo change" "undo": "Undo logo change"
}, }
"deletePerson": "Delete person",
"deletingPerson": "Deleting…",
"deletePersonConfirm": "Delete {{name}}? It can only be deleted when it has no registered relationships.",
"deletePersonError": "This person cannot be deleted because it has registered relationships.",
"deleteAssignment": "Delete assignment",
"deletingAssignment": "Deleting…",
"deleteAssignmentConfirm": "Delete the assignment to {{name}}?",
"deleteAssignmentError": "The assignment could not be deleted."
}, },
"access": { "access": {
"helpTitle": "What is it and what is it for?", "helpTitle": "What is it and what is it for?",

View File

@ -909,15 +909,7 @@
"removed": "Se quitará el logotipo al guardar.", "removed": "Se quitará el logotipo al guardar.",
"remove": "Quitar logotipo", "remove": "Quitar logotipo",
"undo": "Deshacer cambio de logotipo" "undo": "Deshacer cambio de logotipo"
}, }
"deletePerson": "Eliminar persona",
"deletingPerson": "Eliminando…",
"deletePersonConfirm": "¿Eliminar a {{name}}? Solo se puede eliminar si no tiene relaciones registradas.",
"deletePersonError": "No se puede eliminar esta persona porque tiene relaciones registradas.",
"deleteAssignment": "Eliminar asignación",
"deletingAssignment": "Eliminando…",
"deleteAssignmentConfirm": "¿Eliminar la asignación al departamento {{name}}?",
"deleteAssignmentError": "No se puede eliminar la asignación."
}, },
"access": { "access": {
"helpTitle": "¿Qué es y para qué sirve?", "helpTitle": "¿Qué es y para qué sirve?",

View File

@ -80,7 +80,7 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -1 +1 @@
export { deleteAssignment, saveAssignment } from "./save-assignment.api"; export { saveAssignment } from "./save-assignment.api";

View File

@ -1,14 +1,6 @@
import { CompanyResponseSchema } from "../../../../common"; import { CompanyResponseSchema } from "../../../../common";
import { companyRequest } from "../../../companies/shared/api/company-request"; import { companyRequest } from "../../../companies/shared/api/company-request";
export const deleteAssignment = (companyId: string, personId: string, id: string, token: string) =>
companyRequest(
`/${companyId}/people/${personId}/assignments/${id}/delete`,
CompanyResponseSchema,
{},
token
);
export const saveAssignment = ( export const saveAssignment = (
companyId: string, companyId: string,
personId: string, personId: string,

View File

@ -80,7 +80,7 @@ export function CertificateConfigurationsPage(props: CertificateConfigurationsPa
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -158,8 +158,8 @@ export function CompanyDetail(props: CompanyDetailProps) {
aria-pressed={tab === section} aria-pressed={tab === section}
className={ className={
embedded embedded
? "cursor-pointer text-foreground hover:text-primary h-auto min-h-10 w-full justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] aria-pressed:bg-accent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent aria-pressed:text-accent-foreground shell:text-sm" ? "h-auto min-h-10 w-full justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] aria-pressed:bg-accent aria-pressed:hover:bg-accent dark:aria-pressed:hover:bg-accent aria-pressed:text-accent-foreground shell:text-sm"
: "cursor-pointer text-foreground hover:text-primary" : undefined
} }
{...(props.onSectionChange {...(props.onSectionChange
? { ? {

View File

@ -85,7 +85,7 @@ export function CompaniesPage(props: CompaniesPageProps) {
hideable: false, hideable: false,
render: (company) => ( render: (company) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={companySectionHref(company.id, "overview")} href={companySectionHref(company.id, "overview")}
> >
<CompanyIdentity <CompanyIdentity

View File

@ -9,7 +9,7 @@ export function DashboardNavigationButton({
<button <button
{...props} {...props}
className={cn( className={cn(
"text-left font-semibold app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-3 focus-visible:outline-ring", "text-left font-semibold text-primary hover:underline focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-3 focus-visible:outline-ring",
className className
)} )}
type="button" type="button"

View File

@ -30,12 +30,7 @@ export function DashboardPanel({
{title} {title}
</h2> </h2>
{action && ( {action && (
<Button <Button className="text-primary shrink-0" onClick={action} size="sm" variant="ghost">
className="app-link text-primary hover:text-primary shrink-0"
onClick={action}
size="sm"
variant="ghost"
>
{t("dashboard.viewAll")} {t("dashboard.viewAll")}
<ChevronRightIcon aria-hidden="true" className="size-4" /> <ChevronRightIcon aria-hidden="true" className="size-4" />
</Button> </Button>

View File

@ -73,7 +73,7 @@ export function FolderMappingsPage(props: FolderMappingsPageProps) {
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -84,7 +84,7 @@ export function InterventionsPage(props: InterventionsPageProps) {
hideable: false, hideable: false,
render: (intervention) => ( render: (intervention) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${intervention.companyId}/interventions/${intervention.id}`} href={`/companies/${intervention.companyId}/interventions/${intervention.id}`}
> >
{intervention.reference} {intervention.reference}
@ -98,7 +98,7 @@ export function InterventionsPage(props: InterventionsPageProps) {
hideable: false, hideable: false,
render: (intervention) => ( render: (intervention) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${intervention.companyId}/interventions/${intervention.id}`} href={`/companies/${intervention.companyId}/interventions/${intervention.id}`}
> >
{intervention.values.subject} {intervention.values.subject}

View File

@ -79,7 +79,7 @@ export function MailConfigurationsPage(props: MailConfigurationsPageProps) {
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -11,8 +11,6 @@ import { useTranslation } from "react-i18next";
import type { CompanyDTO, PersonDTO } from "../../../../common"; import type { CompanyDTO, PersonDTO } from "../../../../common";
import { AssignmentEditorDialog, AssignmentHistoryDialog } from "../../../assignments"; import { AssignmentEditorDialog, AssignmentHistoryDialog } from "../../../assignments";
import { deleteAssignment } from "../../../assignments/shared/api";
import { deletePerson } from "../../shared/api";
export function PersonDetails({ export function PersonDetails({
company, company,
@ -30,8 +28,6 @@ export function PersonDetails({
const { t, i18n } = useTranslation("companies"); const { t, i18n } = useTranslation("companies");
const [editor, setEditor] = useState<string | undefined>(); const [editor, setEditor] = useState<string | undefined>();
const [historyId, setHistoryId] = useState<string | undefined>(); const [historyId, setHistoryId] = useState<string | undefined>();
const [deleting, setDeleting] = useState(false);
const [deletingAssignment, setDeletingAssignment] = useState<string | undefined>();
const assignments = company.assignments.filter((item) => item.personId === person.id); const assignments = company.assignments.filter((item) => item.personId === person.id);
const selected = assignments.find((item) => item.id === editor); const selected = assignments.find((item) => item.id === editor);
const history = assignments.find((item) => item.id === historyId); const history = assignments.find((item) => item.id === historyId);
@ -50,31 +46,9 @@ export function PersonDetails({
{person.isContact ? ` · ${t("isContact")}` : ""} {person.isContact ? ` · ${t("isContact")}` : ""}
</p> </p>
</div> </div>
<div className="flex flex-wrap gap-2">
<Button onClick={onEdit} variant="outline"> <Button onClick={onEdit} variant="outline">
{t("editPerson")} {t("editPerson")}
</Button> </Button>
<Button
disabled={deleting}
onClick={async () => {
if (!window.confirm(t("deletePersonConfirm", { name: person.name }))) {
return;
}
setDeleting(true);
try {
await deletePerson(company.id, person.id, csrfToken);
onBack();
} catch {
window.alert(t("deletePersonError"));
} finally {
setDeleting(false);
}
}}
variant="destructive"
>
{t(deleting ? "deletingPerson" : "deletePerson")}
</Button>
</div>
</div> </div>
<Card> <Card>
<CardContent className="grid gap-4 pt-6 sm:grid-cols-3"> <CardContent className="grid gap-4 pt-6 sm:grid-cols-3">
@ -158,32 +132,6 @@ export function PersonDetails({
<EntityAction onClick={() => setHistoryId(item.id)}> <EntityAction onClick={() => setHistoryId(item.id)}>
{t("history")} {t("history")}
</EntityAction> </EntityAction>
<EntityAction
onClick={async () => {
if (
!window.confirm(
t("deleteAssignmentConfirm", {
name: departmentName(item.departmentId),
})
)
) {
return;
}
setDeletingAssignment(item.id);
try {
await deleteAssignment(company.id, person.id, item.id, csrfToken);
window.location.reload();
} catch {
window.alert(t("deleteAssignmentError"));
} finally {
setDeletingAssignment(undefined);
}
}}
>
{t(
deletingAssignment === item.id ? "deletingAssignment" : "deleteAssignment"
)}
</EntityAction>
</EntityRowActions> </EntityRowActions>
), ),
}, },

View File

@ -104,7 +104,7 @@ export function PeopleSection({
render: (person) => ( render: (person) => (
<> <>
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={entityHref(person.id)} href={entityHref(person.id)}
> >
{person.name} {person.name}

View File

@ -1 +1 @@
export { deletePerson, savePerson } from "./save-person.api"; export { savePerson } from "./save-person.api";

View File

@ -13,6 +13,3 @@ export const savePerson = (
body, body,
token token
); );
export const deletePerson = (companyId: string, id: string, token: string) =>
companyRequest(`/${companyId}/people/${id}/delete`, CompanyResponseSchema, {}, token);

View File

@ -74,7 +74,7 @@ export function PrinterConfigurationsPage(props: PrinterConfigurationsPageProps)
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -96,7 +96,7 @@ export function ProcessesPage(props: ProcessesPageProps) {
hideable: false, hideable: false,
render: (p) => ( render: (p) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${company.id}/processes/${p.id}`} href={`/companies/${company.id}/processes/${p.id}`}
> >
{p.values.name} {p.values.name}

View File

@ -77,7 +77,7 @@ export function RemoteConnectionsPage(props: RemoteConnectionsPageProps) {
hideable: false, hideable: false,
render: (c) => ( render: (c) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(c.id)} onClick={() => setView(c.id)}
type="button" type="button"
> >

View File

@ -77,7 +77,7 @@ export function TeleworkAuthorizationsPage(props: TeleworkAuthorizationsPageProp
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={() => setView(item.id)} onClick={() => setView(item.id)}
type="button" type="button"
> >

View File

@ -87,7 +87,7 @@ export function TicketsPage(props: TicketsPageProps) {
hideable: false, hideable: false,
render: (ticket) => ( render: (ticket) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${ticket.companyId}/tickets/${ticket.id}`} href={`/companies/${ticket.companyId}/tickets/${ticket.id}`}
> >
{ticket.reference} {ticket.reference}
@ -101,7 +101,7 @@ export function TicketsPage(props: TicketsPageProps) {
hideable: false, hideable: false,
render: (ticket) => ( render: (ticket) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${ticket.companyId}/tickets/${ticket.id}`} href={`/companies/${ticket.companyId}/tickets/${ticket.id}`}
> >
{ticket.values.subject} {ticket.values.subject}

View File

@ -98,7 +98,7 @@ export function WorkProfilesPage(props: WorkProfilesPageProps) {
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={`/companies/${company.id}/work-profiles/${item.id}`} href={`/companies/${company.id}/work-profiles/${item.id}`}
> >
{item.values.name} {item.values.name}

View File

@ -9,7 +9,9 @@ export function RouteFeedback({ invalid = false }: { invalid?: boolean }) {
{t(invalid ? "navigation.invalidRoute" : "navigation.notFound")} {t(invalid ? "navigation.invalidRoute" : "navigation.notFound")}
</h1> </h1>
<p className="text-muted-foreground">{t("navigation.routeHelp")}</p> <p className="text-muted-foreground">{t("navigation.routeHelp")}</p>
<BrowserLink href="/dashboard">{t("dashboard.title")}</BrowserLink> <BrowserLink className="text-primary underline underline-offset-4" href="/dashboard">
{t("dashboard.title")}
</BrowserLink>
</section> </section>
); );
} }

View File

@ -2,16 +2,10 @@ import type { ComponentProps } from "react";
import { isInternalPath, navigateTo } from "../helpers/browser-navigation"; import { isInternalPath, navigateTo } from "../helpers/browser-navigation";
export function BrowserLink({ export function BrowserLink({ href, onClick, ...props }: ComponentProps<"a"> & { href: string }) {
href,
onClick,
className,
...props
}: ComponentProps<"a"> & { href: string }) {
return ( return (
<a <a
{...props} {...props}
className={`app-link ${className ?? ""}`}
href={href} href={href}
onClick={(event) => { onClick={(event) => {
onClick?.(event); onClick?.(event);

View File

@ -33,7 +33,9 @@ export function CustomWorkspace({
return ( return (
<section className="space-y-4 p-6" role="alert"> <section className="space-y-4 p-6" role="alert">
<p>{t("errors.FORBIDDEN")}</p> <p>{t("errors.FORBIDDEN")}</p>
<BrowserLink href="/dashboard">{t("workspace")}</BrowserLink> <BrowserLink className="underline" href="/dashboard">
{t("workspace")}
</BrowserLink>
</section> </section>
); );
} }

View File

@ -21,7 +21,7 @@ export function CatalogName({
return ( return (
<> <>
<button <button
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
onClick={onView} onClick={onView}
type="button" type="button"
> >

View File

@ -72,7 +72,7 @@ export function ResourcesPage(props: ResourcesPageProps) {
hideable: false, hideable: false,
render: (item) => ( render: (item) => (
<BrowserLink <BrowserLink
className="font-medium text-left underline-offset-4 app-link text-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-ring" className="font-medium text-left underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-ring"
href={entityHref(item.id)} href={entityHref(item.id)}
> >
{item.values.name} {item.values.name}

View File

@ -77,10 +77,10 @@ export function FormActions({ children }: { children: ReactNode }) {
); );
} }
/** El cuerpo reserva espacio simétrico para que el scroll no recorte el anillo de foco. */ /** El cuerpo se desplaza dentro del formulario; las acciones conservan su submit nativo. */
export function FormBody({ children }: { children: ReactNode }) { export function FormBody({ children }: { children: ReactNode }) {
return ( return (
<div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-1 pt-1 pb-5" data-slot="form-body"> <div className="min-h-0 flex-1 space-y-5 overflow-y-auto pb-5 pr-1" data-slot="form-body">
{children} {children}
</div> </div>
); );

View File

@ -180,8 +180,4 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
letter-spacing: var(--tracking-normal); letter-spacing: var(--tracking-normal);
} }
button:not(:disabled), [role="button"]:not(:disabled) {
cursor: pointer;
}
} }