commit13
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { FormErrors, controlClass } from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
type Choice = { value: string; label: string };
|
||||
type Props = {
|
||||
route: string;
|
||||
record: { record_key: string; version: number };
|
||||
choices: Choice[];
|
||||
today?: string;
|
||||
data?: Record<string, string | number | boolean | null>;
|
||||
};
|
||||
|
||||
function Confirm({ props, choice }: { props: Props; choice: Choice }) {
|
||||
const form = useForm({
|
||||
operation_key: crypto.randomUUID(),
|
||||
record_key: props.record.record_key,
|
||||
version: props.record.version,
|
||||
action: choice.value,
|
||||
reason: '',
|
||||
data: {
|
||||
...props.data,
|
||||
...(choice.value === 'execute'
|
||||
? { event_on: props.today ?? '', evidence: '' }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
return (
|
||||
<form
|
||||
className="mt-3 grid gap-3 md:grid-cols-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post(props.route);
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
<label className="grid gap-1 text-sm">
|
||||
Motivo e evidência da conferência
|
||||
<textarea
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={4000}
|
||||
className={controlClass}
|
||||
value={form.data.reason}
|
||||
onChange={(e) => form.setData('reason', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
{choice.value === 'execute' && (
|
||||
<>
|
||||
<label className="grid gap-1 text-sm">
|
||||
Data efectiva do evento
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
max={props.today}
|
||||
className={controlClass}
|
||||
value={String(form.data.data.event_on ?? '')}
|
||||
onChange={(e) =>
|
||||
form.setData('data', {
|
||||
...form.data.data,
|
||||
event_on: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-1 text-sm">
|
||||
Comprovativo da execução física
|
||||
<input
|
||||
required
|
||||
minLength={10}
|
||||
maxLength={3000}
|
||||
className={controlClass}
|
||||
value={String(form.data.data.evidence ?? '')}
|
||||
onChange={(e) =>
|
||||
form.setData('data', {
|
||||
...form.data.data,
|
||||
evidence: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<Button disabled={form.processing}>{choice.label}</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function InventoryEventActions(props: Props) {
|
||||
const [selected, setSelected] = useState(
|
||||
props.choices.length === 1 ? props.choices[0].value : '',
|
||||
);
|
||||
if (props.choices.length === 0) return null;
|
||||
const choice = props.choices.find((option) => option.value === selected);
|
||||
return (
|
||||
<details className="mt-4">
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
{props.choices.length === 1
|
||||
? props.choices[0].label
|
||||
: 'Acções da minha responsabilidade'}
|
||||
</summary>
|
||||
{props.choices.length > 1 && (
|
||||
<label className="mt-3 grid gap-1 text-sm">
|
||||
Acção
|
||||
<select
|
||||
className={controlClass}
|
||||
value={selected}
|
||||
onChange={(e) => setSelected(e.target.value)}
|
||||
>
|
||||
<option value="">Seleccione a decisão</option>
|
||||
{props.choices.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{choice && (
|
||||
<Confirm key={choice.value} props={props} choice={choice} />
|
||||
)}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { quantity } from '@/types/inventory';
|
||||
import { controlClass } from '@/components/procurement';
|
||||
|
||||
export type CountInput = {
|
||||
lot_id: number | null;
|
||||
new_key: string | null;
|
||||
quantity: string;
|
||||
code: string;
|
||||
expires_on: string;
|
||||
no_expiry: boolean;
|
||||
};
|
||||
export type LotCountSource = {
|
||||
stock_item_id: number;
|
||||
warehouse_id: number;
|
||||
hash: string;
|
||||
lots: {
|
||||
lot_id: number;
|
||||
metadata_id: number;
|
||||
quantity_milli: number;
|
||||
code: string | null;
|
||||
expires_on: string | null;
|
||||
no_expiry: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
export function countTotal(rows: CountInput[]): string | null {
|
||||
let total = 0;
|
||||
for (const row of rows) {
|
||||
if (!/^\d{1,9}(\.\d{1,3})?$/.test(row.quantity)) return null;
|
||||
const [whole, fraction = ''] = row.quantity.split('.');
|
||||
total += Number(whole) * 1000 + Number(fraction.padEnd(3, '0'));
|
||||
}
|
||||
return `${Math.floor(total / 1000)}.${String(total % 1000).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
export function LotCountFields({
|
||||
source,
|
||||
rows,
|
||||
setRows,
|
||||
}: {
|
||||
source: LotCountSource;
|
||||
rows: CountInput[];
|
||||
setRows: (rows: CountInput[]) => void;
|
||||
}) {
|
||||
const change = (index: number, update: Partial<CountInput>) =>
|
||||
setRows(
|
||||
rows.map((row, i) => (i === index ? { ...row, ...update } : row)),
|
||||
);
|
||||
const exhausted = source.lots.filter(
|
||||
(lot) =>
|
||||
lot.quantity_milli === 0 &&
|
||||
!rows.some((row) => row.lot_id === lot.lot_id),
|
||||
);
|
||||
return (
|
||||
<fieldset className="space-y-3 rounded-lg border p-4 sm:col-span-2">
|
||||
<legend className="px-2 text-sm font-semibold">
|
||||
Conferência física por lote
|
||||
</legend>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Registe apenas o que contou. O total é calculado; identificação,
|
||||
validade e saldo de referência vêm do registo existente. A
|
||||
contagem não altera existências nem liberta qualidade.
|
||||
</p>
|
||||
{rows.map((row, index) => {
|
||||
const known = source.lots.find(
|
||||
(lot) => lot.lot_id === row.lot_id,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={row.lot_id ?? row.new_key}
|
||||
className="grid gap-3 rounded border p-3 md:grid-cols-2"
|
||||
>
|
||||
<p className="text-sm">
|
||||
{known
|
||||
? `${known.code ?? `Lote #${known.lot_id}`} · Saldo: ${quantity(known.quantity_milli)} · Validade: ${known.expires_on ?? (known.no_expiry ? 'Sem prazo confirmado' : 'Por confirmar')}`
|
||||
: 'Lote encontrado sem identidade anterior neste local'}
|
||||
</p>
|
||||
<label className="grid gap-1 text-sm">
|
||||
Quantidade contada
|
||||
<Input
|
||||
required
|
||||
inputMode="decimal"
|
||||
pattern="\d{1,9}(\.\d{1,3})?"
|
||||
value={row.quantity}
|
||||
onChange={(e) =>
|
||||
change(index, { quantity: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{!known && (
|
||||
<>
|
||||
<label className="grid gap-1 text-sm">
|
||||
Código, se conhecido
|
||||
<Input
|
||||
maxLength={100}
|
||||
value={row.code}
|
||||
onChange={(e) =>
|
||||
change(index, {
|
||||
code: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="grid gap-1 text-sm">
|
||||
Validade, se conhecida
|
||||
<Input
|
||||
type="date"
|
||||
value={row.expires_on}
|
||||
onChange={(e) =>
|
||||
change(index, {
|
||||
expires_on: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.no_expiry}
|
||||
onChange={(e) =>
|
||||
change(index, {
|
||||
no_expiry: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Confirmo artigo sem prazo de validade
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setRows(
|
||||
rows.filter((_, i) => i !== index),
|
||||
)
|
||||
}
|
||||
>
|
||||
Remover linha encontrada
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setRows([
|
||||
...rows,
|
||||
{
|
||||
lot_id: null,
|
||||
new_key: crypto.randomUUID(),
|
||||
quantity: '',
|
||||
code: '',
|
||||
expires_on: '',
|
||||
no_expiry: false,
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Acrescentar lote encontrado
|
||||
</Button>
|
||||
{exhausted.length > 0 && (
|
||||
<label className="grid gap-1 text-sm">
|
||||
Reutilizar identidade de lote sem saldo que voltou a
|
||||
encontrar
|
||||
<select
|
||||
className={controlClass}
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
const lot = exhausted.find(
|
||||
(l) => String(l.lot_id) === e.target.value,
|
||||
);
|
||||
if (lot)
|
||||
setRows([
|
||||
...rows,
|
||||
{
|
||||
lot_id: lot.lot_id,
|
||||
new_key: null,
|
||||
quantity: '',
|
||||
code: lot.code ?? '',
|
||||
expires_on: lot.expires_on ?? '',
|
||||
no_expiry: lot.no_expiry,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<option value="">
|
||||
Seleccione apenas se encontrou quantidade deste lote
|
||||
</option>
|
||||
{exhausted.map((lot) => (
|
||||
<option key={lot.lot_id} value={lot.lot_id}>
|
||||
{lot.code ?? `Lote #${lot.lot_id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<p className="text-sm font-medium">
|
||||
Total contado: {countTotal(rows) ?? 'Complete as quantidades'}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Quantidades encontradas a mais ficam em quarentena após o
|
||||
ajuste, mesmo num lote anteriormente libertado. A aplicação
|
||||
exige conferência independente da contagem.
|
||||
</p>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -47,6 +47,23 @@ export function ReceiptProgressTable({
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{line.unit}
|
||||
</p>
|
||||
{(line.supplier_returned_milli ?? 0) >
|
||||
0 && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Devolvido ao fornecedor:{' '}
|
||||
{quantity(
|
||||
line.supplier_returned_milli ??
|
||||
0,
|
||||
)}{' '}
|
||||
· Aceitação efectiva:{' '}
|
||||
{quantity(
|
||||
line.effective_accepted_milli ??
|
||||
0,
|
||||
)}
|
||||
. Reposição exige autorização
|
||||
própria.
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
{[
|
||||
line.ordered_milli,
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
procurementNavigation,
|
||||
} from '@/lib/procurement-navigation';
|
||||
import { roleLabels } from '@/types/procurement';
|
||||
import type { WorkContextState } from '@/types/work-context';
|
||||
import { WorkContextChange } from '@/components/work-context-change';
|
||||
|
||||
export function SgcWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
const page = usePage();
|
||||
@@ -44,11 +46,31 @@ export function SgcWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
active: boolean;
|
||||
permissions: string[];
|
||||
} | null;
|
||||
const context = page.props.workContext as WorkContextState | null;
|
||||
const scopedEntries = new Set(
|
||||
procurementNavigation(
|
||||
context?.permissions ?? [],
|
||||
page.props.payablesAvailable === true,
|
||||
).flatMap((group) => group.entries.map((entry) => entry.id)),
|
||||
);
|
||||
const groups = procurementNavigation(
|
||||
access?.active ? access.permissions : (user?.purchasing_role ?? ''),
|
||||
page.props.payablesAvailable === true,
|
||||
publicNavigation?.paths,
|
||||
);
|
||||
)
|
||||
.map((group) => ({
|
||||
...group,
|
||||
entries: group.entries.filter(
|
||||
(entry) =>
|
||||
!context?.enforced ||
|
||||
(context.paths.includes(entry.path.split('?')[0]) &&
|
||||
(context.sharedPaths.includes(
|
||||
entry.path.split('?')[0],
|
||||
) ||
|
||||
scopedEntries.has(entry.id))),
|
||||
),
|
||||
}))
|
||||
.filter((group) => group.entries.length);
|
||||
const activeId = activeProcurementEntry(groups, page.url);
|
||||
const activeGroup = groups.find((group) =>
|
||||
group.entries.some((entry) => entry.id === activeId),
|
||||
@@ -278,6 +300,20 @@ export function SgcWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
);
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-foreground dark:bg-background print:block">
|
||||
{context?.enforced && <WorkContextChange />}
|
||||
{context?.enforced && (
|
||||
<div
|
||||
className="border-b bg-background px-5 py-2 text-sm"
|
||||
role="status"
|
||||
>
|
||||
{context.active
|
||||
? `${context.options[context.active].areaLabel} · ${context.options[context.active].label}`
|
||||
: 'Sem contexto seleccionado'}{' '}
|
||||
<Link className="ml-3 underline" href="/dashboard">
|
||||
Mudar contexto
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{publicNavigation && (
|
||||
<p className="border-b bg-background p-3 text-center text-sm">
|
||||
Consulta pública de desenvolvimento · Sem organização
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { router, usePage } from '@inertiajs/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
export function WorkContextChange() {
|
||||
const page = usePage();
|
||||
const dirty = useRef(new Set<HTMLFormElement>());
|
||||
const pending = useRef(false);
|
||||
const savingForm = useRef<HTMLFormElement | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
useEffect(() => {
|
||||
dirty.current.clear();
|
||||
}, [page.url]);
|
||||
useEffect(() => {
|
||||
const formChanged = (event: Event) => {
|
||||
if (
|
||||
!(event instanceof CustomEvent) ||
|
||||
!(event.detail.form instanceof HTMLFormElement)
|
||||
)
|
||||
return;
|
||||
if (event.detail.dirty) dirty.current.add(event.detail.form);
|
||||
else dirty.current.delete(event.detail.form);
|
||||
};
|
||||
const changed = (event: Event) => {
|
||||
const input = event.target;
|
||||
if (
|
||||
input instanceof HTMLInputElement ||
|
||||
input instanceof HTMLTextAreaElement ||
|
||||
input instanceof HTMLSelectElement
|
||||
) {
|
||||
if (input.form) dirty.current.add(input.form);
|
||||
}
|
||||
};
|
||||
const click = (event: MouseEvent) => {
|
||||
if (!(event.target instanceof Element)) return;
|
||||
const link = event.target.closest('a');
|
||||
if (
|
||||
!link ||
|
||||
new URL(link.href).pathname !== '/dashboard' ||
|
||||
!dirty.current.size
|
||||
)
|
||||
return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setMessage('');
|
||||
setOpen(true);
|
||||
};
|
||||
const unload = (event: BeforeUnloadEvent) => {
|
||||
if (dirty.current.size) {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
};
|
||||
const saved = (event: Event) => {
|
||||
if (!(event instanceof CustomEvent)) return;
|
||||
if (event.detail.success) dirty.current.delete(event.detail.form);
|
||||
if (!pending.current || event.detail.form !== savingForm.current)
|
||||
return;
|
||||
if (event.detail.success) {
|
||||
pending.current = false;
|
||||
dirty.current.clear();
|
||||
setOpen(false);
|
||||
router.visit('/dashboard');
|
||||
} else {
|
||||
pending.current = false;
|
||||
setOpen(true);
|
||||
setMessage(
|
||||
'A gravação falhou. O contexto actual foi mantido. Corrija os campos antes de mudar.',
|
||||
);
|
||||
}
|
||||
};
|
||||
const failure = () => {
|
||||
if (pending.current) {
|
||||
pending.current = false;
|
||||
setOpen(true);
|
||||
setMessage(
|
||||
'Não foi possível concluir a gravação. O contexto actual foi mantido.',
|
||||
);
|
||||
}
|
||||
};
|
||||
const offException = router.on('httpException', failure);
|
||||
const offNetwork = router.on('networkError', failure);
|
||||
document.addEventListener('input', changed);
|
||||
document.addEventListener('change', changed);
|
||||
document.addEventListener('click', click, true);
|
||||
window.addEventListener('beforeunload', unload);
|
||||
window.addEventListener('sgc:context-save-result', saved);
|
||||
window.addEventListener('sgc:context-dirty', formChanged);
|
||||
return () => {
|
||||
document.removeEventListener('input', changed);
|
||||
document.removeEventListener('change', changed);
|
||||
document.removeEventListener('click', click, true);
|
||||
window.removeEventListener('beforeunload', unload);
|
||||
window.removeEventListener('sgc:context-save-result', saved);
|
||||
window.removeEventListener('sgc:context-dirty', formChanged);
|
||||
offException();
|
||||
offNetwork();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Alterações por guardar</DialogTitle>
|
||||
<DialogDescription>
|
||||
Guarde as alterações antes de escolher outro contexto, ou
|
||||
descarte-as.
|
||||
</DialogDescription>
|
||||
{message && <p role="alert">{message}</p>}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const forms = [...dirty.current].filter(
|
||||
(form) => form.isConnected,
|
||||
);
|
||||
if (forms.length !== 1) {
|
||||
setMessage(
|
||||
'Existem alterações em vários formulários. Cancele a mudança e guarde cada formulário antes de continuar.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!forms[0].hasAttribute('data-context-save')) {
|
||||
setMessage(
|
||||
'Cancele a mudança e conclua este formulário pela sua acção própria. A mudança de contexto não executa aprovações ou outras decisões.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!forms[0].reportValidity()) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
pending.current = true;
|
||||
savingForm.current = forms[0];
|
||||
setOpen(false);
|
||||
forms[0].requestSubmit();
|
||||
}}
|
||||
>
|
||||
Guardar
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
dirty.current.clear();
|
||||
pending.current = false;
|
||||
setOpen(false);
|
||||
router.visit('/dashboard');
|
||||
}}
|
||||
>
|
||||
Descartar
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
pending.current = false;
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useForm } from '@inertiajs/react';
|
||||
import { FormErrors, Panel, controlClass } from '@/components/procurement';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { WorkContextState } from '@/types/work-context';
|
||||
|
||||
export function WorkContextSettings({
|
||||
context,
|
||||
enabled,
|
||||
version,
|
||||
}: {
|
||||
context: WorkContextState;
|
||||
enabled: string[];
|
||||
version: number;
|
||||
}) {
|
||||
const form = useForm({ version, enabled, reason: '', confirm: false });
|
||||
return (
|
||||
<Panel title="Áreas e opções do produto">
|
||||
<p className="mb-4 text-sm">
|
||||
A configuração limita as opções contratadas; não atribui
|
||||
permissões. A alteração de acesso é imediata, sem modificar
|
||||
processos existentes. Os perfis precisam de associação explícita
|
||||
às opções. Percursos ainda pendentes ficam identificados no
|
||||
Painel Inicial.
|
||||
</p>
|
||||
{context.purchased.length === 0 && (
|
||||
<p>
|
||||
Não existem opções contratadas registadas para esta empresa
|
||||
na instalação.
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.post('/administracao/parametrizacao/work_contexts', {
|
||||
preserveState: 'errors',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormErrors errors={form.errors} />
|
||||
{context.purchased.map((key) => (
|
||||
<label className="flex items-center gap-2" key={key}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.data.enabled.includes(key)}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'enabled',
|
||||
e.target.checked
|
||||
? [...form.data.enabled, key]
|
||||
: form.data.enabled.filter(
|
||||
(value) => value !== key,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{context.options[key].areaLabel} ·{' '}
|
||||
{context.options[key].label}
|
||||
</label>
|
||||
))}
|
||||
<label className="block">
|
||||
Motivo
|
||||
<textarea
|
||||
required
|
||||
minLength={10}
|
||||
className={controlClass}
|
||||
value={form.data.reason}
|
||||
onChange={(e) => form.setData('reason', e.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
required
|
||||
type="checkbox"
|
||||
checked={form.data.confirm}
|
||||
onChange={(e) =>
|
||||
form.setData('confirm', e.target.checked)
|
||||
}
|
||||
/>
|
||||
Confirmo o efeito imediato sobre o acesso, preservando os
|
||||
processos.
|
||||
</label>
|
||||
<Button disabled={form.processing}>Guardar opções</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user