feat(silo attached detach): added in silo attach detach setup in the silo card
This commit is contained in:
164
frontend/src/components/logistics/siloAdjustments/AttachSilo.tsx
Normal file
164
frontend/src/components/logistics/siloAdjustments/AttachSilo.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { useAppForm } from "@/utils/formStuff";
|
||||||
|
import { getMachineConnected } from "@/utils/querys/logistics/machineConnected";
|
||||||
|
import { getMachineNotConnected } from "@/utils/querys/logistics/notConnected";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function AttachSilo(props: any) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data, isError, isLoading, refetch } = useQuery(
|
||||||
|
getMachineNotConnected({
|
||||||
|
siloID: props.silo.LocationID,
|
||||||
|
connectionType: "detached",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const { refetch: attached } = useQuery(
|
||||||
|
getMachineConnected({
|
||||||
|
siloID: props.silo.LocationID,
|
||||||
|
connectionType: "connected",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
laneId: props.silo.LocationID,
|
||||||
|
productionLotId: "",
|
||||||
|
machineId: "",
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
console.log(value);
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
"/api/logistics/attachsilo",
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
toast.success(res.data.message);
|
||||||
|
refetch();
|
||||||
|
attached();
|
||||||
|
form.reset();
|
||||||
|
setOpen(!open);
|
||||||
|
} else {
|
||||||
|
console.log(res.data);
|
||||||
|
toast.error(res.data.message);
|
||||||
|
refetch();
|
||||||
|
form.reset();
|
||||||
|
setOpen(!open);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(
|
||||||
|
"There was an error attaching the silo please try again, if persist please enter a helpdesk ticket."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isError)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>There was an error loading data</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Loading....</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// convert the array that comes over to label and value
|
||||||
|
const tranMachine = data.map((i: any) => ({
|
||||||
|
value: i.machineId.toString(),
|
||||||
|
label: i.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(!open)}>
|
||||||
|
Attach Silo
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Attach silo for: {props.silo.Description}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Enter the new lotnumber, select the machine you
|
||||||
|
would like to attach.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm">
|
||||||
|
NOTE: If the machine you are trying to attach is not
|
||||||
|
showing in the drop down this means it is already
|
||||||
|
attached to this silo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<form.AppField
|
||||||
|
name="productionLotId"
|
||||||
|
children={(field) => (
|
||||||
|
<field.InputField
|
||||||
|
label="Lot Number"
|
||||||
|
inputType="number"
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<form.AppField
|
||||||
|
name="machineId"
|
||||||
|
children={(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Machine"
|
||||||
|
options={tranMachine}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Attach</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
frontend/src/components/logistics/siloAdjustments/DetachSilo.tsx
Normal file
145
frontend/src/components/logistics/siloAdjustments/DetachSilo.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { useAppForm } from "@/utils/formStuff";
|
||||||
|
import { getMachineConnected } from "@/utils/querys/logistics/machineConnected";
|
||||||
|
import { getMachineNotConnected } from "@/utils/querys/logistics/notConnected";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function DetachSilo(props: any) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data, isError, isLoading, refetch } = useQuery(
|
||||||
|
getMachineConnected({
|
||||||
|
siloID: props.silo.LocationID,
|
||||||
|
connectionType: "connected",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { refetch: notConnected } = useQuery(
|
||||||
|
getMachineNotConnected({
|
||||||
|
siloID: props.silo.LocationID,
|
||||||
|
connectionType: "detached",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useAppForm({
|
||||||
|
defaultValues: {
|
||||||
|
laneId: props.silo.LocationID,
|
||||||
|
machineId: 0,
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
"/api/logistics/detachsilo",
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success(res.data.message);
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
notConnected();
|
||||||
|
form.reset();
|
||||||
|
setOpen(!open);
|
||||||
|
} else {
|
||||||
|
console.log(res.data);
|
||||||
|
toast.error(res.data.message);
|
||||||
|
refetch();
|
||||||
|
form.reset();
|
||||||
|
setOpen(!open);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
toast.error(
|
||||||
|
"There was an error detaching the silo please try again, if persist please enter a helpdesk ticket."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isError)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>There was an error loading data</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>Loading....</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// convert the array that comes over to label and value
|
||||||
|
const tranMachine = data.map((i: any) => ({
|
||||||
|
value: i.machineId.toString(),
|
||||||
|
label: i.name,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<Dialog open={open}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
disabled={data.length === 0}
|
||||||
|
>
|
||||||
|
Detach Silo
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Attach silo for: {props.silo.Description}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Select the machine you would like to detach.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="mt-2">
|
||||||
|
<form.AppField
|
||||||
|
name="machineId"
|
||||||
|
children={(field) => (
|
||||||
|
<field.SelectField
|
||||||
|
label="Select Machine"
|
||||||
|
options={tranMachine}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="outline">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<form.AppForm>
|
||||||
|
<form.SubmitButton>Detach</form.SubmitButton>
|
||||||
|
</form.AppForm>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ import { CircleAlert } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import ChartData from "./ChartData";
|
import ChartData from "./ChartData";
|
||||||
|
import { AttachSilo } from "./AttachSilo";
|
||||||
|
import { DetachSilo } from "./DetachSilo";
|
||||||
|
|
||||||
export default function SiloCard(data: any) {
|
export default function SiloCard(data: any) {
|
||||||
const token = localStorage.getItem("auth_token");
|
const token = localStorage.getItem("auth_token");
|
||||||
@@ -151,6 +153,7 @@ export default function SiloCard(data: any) {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
|
variant="outline"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={
|
onClick={
|
||||||
form.handleSubmit
|
form.handleSubmit
|
||||||
@@ -188,13 +191,17 @@ export default function SiloCard(data: any) {
|
|||||||
<div className="grow max-w-[600px]">
|
<div className="grow max-w-[600px]">
|
||||||
<ChartData laneId={silo.LocationID} />
|
<ChartData laneId={silo.LocationID} />
|
||||||
|
|
||||||
<div className="flex justify-end m-1">
|
<div className="flex justify-end m-1 gap-3">
|
||||||
<Link
|
<AttachSilo silo={silo} />
|
||||||
to={"/siloAdjustments/$hist"}
|
<DetachSilo silo={silo} />
|
||||||
params={{ hist: silo.LocationID }}
|
<Button variant="outline">
|
||||||
>
|
<Link
|
||||||
Historical Data
|
to={"/siloAdjustments/$hist"}
|
||||||
</Link>
|
params={{ hist: silo.LocationID }}
|
||||||
|
>
|
||||||
|
Historical Data
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import ConsumeMaterial from "@/components/logistics/materialHelper/consumption/ConsumeMaterial";
|
import ConsumeMaterial from "@/components/logistics/materialHelper/consumption/ConsumeMaterial";
|
||||||
import PreformReturn from "@/components/logistics/materialHelper/consumption/PreformReturn";
|
import PreformReturn from "@/components/logistics/materialHelper/consumption/MaterialReturn";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
|
|||||||
14
frontend/src/utils/formStuff/index.tsx
Normal file
14
frontend/src/utils/formStuff/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createFormHook, createFormHookContexts } from "@tanstack/react-form";
|
||||||
|
import { InputField } from "./options/InputField";
|
||||||
|
import { SubmitButton } from "./options/submitButton";
|
||||||
|
import { SelectField } from "./options/selectorField";
|
||||||
|
|
||||||
|
export const { fieldContext, useFieldContext, formContext, useFormContext } =
|
||||||
|
createFormHookContexts();
|
||||||
|
|
||||||
|
export const { useAppForm } = createFormHook({
|
||||||
|
fieldComponents: { InputField, SelectField },
|
||||||
|
formComponents: { SubmitButton },
|
||||||
|
fieldContext,
|
||||||
|
formContext,
|
||||||
|
});
|
||||||
16
frontend/src/utils/formStuff/options/FieldErrors.tsx
Normal file
16
frontend/src/utils/formStuff/options/FieldErrors.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { AnyFieldMeta } from "@tanstack/react-form";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
|
type FieldErrorsProps = {
|
||||||
|
meta: AnyFieldMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FieldErrors = ({ meta }: FieldErrorsProps) => {
|
||||||
|
if (!meta.isTouched) return null;
|
||||||
|
|
||||||
|
return meta.errors.map(({ message }: ZodError, index) => (
|
||||||
|
<p key={index} className="text-sm font-medium text-destructive">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
));
|
||||||
|
};
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
//import { Input } from "@/components/ui/input";
|
import { Label } from "@/components/ui/label";
|
||||||
//import { Label } from "@radix-ui/react-dropdown-menu";
|
import { useFieldContext } from "..";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { FieldErrors } from "./FieldErrors";
|
||||||
|
|
||||||
// export const FormInput = (form: any, label: string) => {
|
type InputFieldProps = {
|
||||||
// // <form.Field
|
label: string;
|
||||||
// // name="username"
|
inputType: string;
|
||||||
// // validators={{
|
required: boolean;
|
||||||
// // // We can choose between form-wide and field-specific validators
|
};
|
||||||
// // onChange: ({ value }) =>
|
export const InputField = ({ label, inputType, required }: InputFieldProps) => {
|
||||||
// // value.length > 3
|
const field = useFieldContext<any>();
|
||||||
// // ? undefined
|
|
||||||
// // : "Username must be longer than 3 letters",
|
return (
|
||||||
// // }}
|
<div className="grid gap-3">
|
||||||
// // children={(field) => {
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
// // return (
|
<Input
|
||||||
// // <div className="m-2 min-w-48 max-w-96 p-2">
|
id={field.name}
|
||||||
// // <Label htmlFor="username">{label}</Label>
|
value={field.state.value}
|
||||||
// // <Input
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
// // name={field.name}
|
onBlur={field.handleBlur}
|
||||||
// // value={field.state.value}
|
type={inputType}
|
||||||
// // onBlur={field.handleBlur}
|
required={required}
|
||||||
// // //type="number"
|
/>
|
||||||
// // onChange={(e) => field.handleChange(e.target.value)}
|
<FieldErrors meta={field.state.meta} />
|
||||||
// // />
|
</div>
|
||||||
// // {field.state.meta.errors.length ? (
|
);
|
||||||
// // <em>{field.state.meta.errors.join(",")}</em>
|
};
|
||||||
// // ) : null}
|
|
||||||
// // </div>
|
|
||||||
// // );
|
|
||||||
// // }}
|
|
||||||
// // />;
|
|
||||||
// };
|
|
||||||
|
|||||||
57
frontend/src/utils/formStuff/options/selectorField.tsx
Normal file
57
frontend/src/utils/formStuff/options/selectorField.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { useFieldContext } from "..";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { FieldErrors } from "./FieldErrors";
|
||||||
|
|
||||||
|
type SelectOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectFieldProps = {
|
||||||
|
label: string;
|
||||||
|
options: SelectOption[];
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectField = ({
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
placeholder,
|
||||||
|
}: SelectFieldProps) => {
|
||||||
|
const field = useFieldContext<string>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor={field.name}>{label}</Label>
|
||||||
|
<Select
|
||||||
|
value={field.state.value}
|
||||||
|
onValueChange={(value) => field.handleChange(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
className="w-[380px]"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{options.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<FieldErrors meta={field.state.meta} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
frontend/src/utils/formStuff/options/submitButton.tsx
Normal file
24
frontend/src/utils/formStuff/options/submitButton.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useStore } from "@tanstack/react-form";
|
||||||
|
import { useFormContext } from "..";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
type SubmitButtonProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SubmitButton = ({ children }: SubmitButtonProps) => {
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const [isSubmitting] = useStore(form.store, (state) => [
|
||||||
|
state.isSubmitting,
|
||||||
|
state.canSubmit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
frontend/src/utils/querys/logistics/machineConnected.tsx
Normal file
23
frontend/src/utils/querys/logistics/machineConnected.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getMachineConnected(siloCon: any) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: [`siloConnectionAttached-${siloCon.siloID}`],
|
||||||
|
queryFn: () => fetchStockSilo(siloCon),
|
||||||
|
//enabled:
|
||||||
|
//staleTime: 1000,
|
||||||
|
//refetchInterval: 60 * 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStockSilo = async (siloCon: any) => {
|
||||||
|
const { data } = await axios.post(`/api/logistics/siloconnection`, {
|
||||||
|
siloID: siloCon.siloID,
|
||||||
|
connectionType: siloCon.connectionType,
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
//const url: string = window.location.host.split(":")[0];
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
23
frontend/src/utils/querys/logistics/notConnected.tsx
Normal file
23
frontend/src/utils/querys/logistics/notConnected.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getMachineNotConnected(siloCon: any) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: [`siloConnectionNotConnected-${siloCon.siloID}`],
|
||||||
|
queryFn: () => fetchStockSilo(siloCon),
|
||||||
|
//enabled:
|
||||||
|
//staleTime: 1000,
|
||||||
|
//refetchInterval: 60 * 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStockSilo = async (siloCon: any) => {
|
||||||
|
const { data } = await axios.post(`/api/logistics/siloconnection`, {
|
||||||
|
siloID: siloCon.siloID,
|
||||||
|
connectionType: siloCon.connectionType,
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
//const url: string = window.location.host.split(":")[0];
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
92
server/globalUtils/runProdApi.ts
Normal file
92
server/globalUtils/runProdApi.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { prodEndpointCreation } from "./createUrl.js";
|
||||||
|
import { tryCatch } from "./tryCatch.js";
|
||||||
|
import { createLog } from "../services/logger/logger.js";
|
||||||
|
|
||||||
|
type bodyData = any;
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
endpoint: string;
|
||||||
|
data: bodyData[];
|
||||||
|
};
|
||||||
|
export const runProdApi = async (data: Data) => {
|
||||||
|
/**
|
||||||
|
* Detachs a silo
|
||||||
|
*/
|
||||||
|
|
||||||
|
let url = await prodEndpointCreation(data.endpoint);
|
||||||
|
|
||||||
|
const { data: d, error } = await tryCatch(
|
||||||
|
axios.post(url, data.data[0], {
|
||||||
|
headers: {
|
||||||
|
"X-API-Key": process.env.TEC_API_KEY || "",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let e = error as any;
|
||||||
|
if (e) {
|
||||||
|
//console.log(e.response);
|
||||||
|
if (e.status === 401) {
|
||||||
|
createLog(
|
||||||
|
"error",
|
||||||
|
"lst",
|
||||||
|
"logistics",
|
||||||
|
`Not autorized: ${JSON.stringify(e.response?.data)}`
|
||||||
|
);
|
||||||
|
const data = {
|
||||||
|
success: false,
|
||||||
|
message: `Not autorized: ${JSON.stringify(e.response?.data)}`,
|
||||||
|
data: {
|
||||||
|
status: e.response?.status,
|
||||||
|
statusText: e.response?.statusText,
|
||||||
|
data: e.response?.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
createLog(
|
||||||
|
"error",
|
||||||
|
"lst",
|
||||||
|
"logistics",
|
||||||
|
`There was an error processing the endpoint: ${JSON.stringify(
|
||||||
|
e.response?.data
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `There was an error processing the endpoint: ${JSON.stringify(
|
||||||
|
e.response?.data
|
||||||
|
)}`,
|
||||||
|
data: {
|
||||||
|
status: e.response?.status,
|
||||||
|
statusText: e.response?.statusText,
|
||||||
|
data: e.response?.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d?.status !== 200) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error processing endpoint",
|
||||||
|
data: {
|
||||||
|
status: d?.status,
|
||||||
|
statusText: d?.statusText,
|
||||||
|
data: d?.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Endpoint was processed",
|
||||||
|
data: {
|
||||||
|
status: d.status,
|
||||||
|
statusText: d.statusText,
|
||||||
|
data: d.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -41,15 +41,21 @@ export const postAdjustment = async (data: any, prod: any) => {
|
|||||||
|
|
||||||
const { data: silo, error } = await tryCatch(
|
const { data: silo, error } = await tryCatch(
|
||||||
axios.post(url, siloAdjustment, {
|
axios.post(url, siloAdjustment, {
|
||||||
headers: { Authorization: `Basic ${prod}` },
|
headers: {
|
||||||
|
"X-API-Key": process.env.TEC_API_KEY || "",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
let e = error as any;
|
let e = error as any;
|
||||||
if (e) {
|
if (e) {
|
||||||
|
console.log(e.response);
|
||||||
if (e.status === 401) {
|
if (e.status === 401) {
|
||||||
const data = {
|
const data = {
|
||||||
success: false,
|
success: false,
|
||||||
message: "Incorrect alpla prod password.",
|
message: `There was error posting the data: ${JSON.stringify(
|
||||||
|
e.response?.data
|
||||||
|
)}`,
|
||||||
data: {
|
data: {
|
||||||
status: e.response?.status,
|
status: e.response?.status,
|
||||||
statusText: e.response?.statusText,
|
statusText: e.response?.statusText,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { runProdApi } from "../../../../globalUtils/runProdApi.js";
|
||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
|
||||||
|
export const attachSilo = async (data: any) => {
|
||||||
|
/**
|
||||||
|
* Detachs a silo
|
||||||
|
*/
|
||||||
|
|
||||||
|
const detachData = {
|
||||||
|
endpoint: "/public/v1.0/IssueMaterial/AssignSiloToMachine",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
laneId: data.laneId,
|
||||||
|
machineId: data.machineId,
|
||||||
|
productionLotId: data.productionLotId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: d, error } = await tryCatch(runProdApi(detachData));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error processing attachingSilo data",
|
||||||
|
data: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error processing silo attach data",
|
||||||
|
data: d.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "silo attach was completed",
|
||||||
|
data: d.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { runProdApi } from "../../../../globalUtils/runProdApi.js";
|
||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
|
||||||
|
export const detachSilo = async (data: any) => {
|
||||||
|
/**
|
||||||
|
* Detachs a silo
|
||||||
|
*/
|
||||||
|
|
||||||
|
const detachData = {
|
||||||
|
endpoint: "/public/v1.0/IssueMaterial/DetachSiloFromMachine",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
laneId: data.laneId,
|
||||||
|
machineId: data.machineId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: d, error } = await tryCatch(runProdApi(detachData));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error processing detach data",
|
||||||
|
data: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error processing detach data",
|
||||||
|
data: d.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Detach was completed",
|
||||||
|
data: d.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
import { query } from "../../../sqlServer/prodSqlServer.js";
|
||||||
|
import {
|
||||||
|
connectedToMachine,
|
||||||
|
notconnectedToMachine,
|
||||||
|
} from "../../../sqlServer/querys/silo/connectionCheck.js";
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
siloID: string;
|
||||||
|
connectionType: string;
|
||||||
|
};
|
||||||
|
export const siloConnectionType = async (data: Data) => {
|
||||||
|
/**
|
||||||
|
* Will return the machines that are attached or detached based on the silo and connection type
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Missing mandatory data",
|
||||||
|
data: [{ error: "Missing siloId or ConnectionType" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the silo id to the correct one
|
||||||
|
let newQuery = "";
|
||||||
|
|
||||||
|
if (data.connectionType === "connected") {
|
||||||
|
newQuery = connectedToMachine.replace("[siloID]", data.siloID);
|
||||||
|
} else {
|
||||||
|
newQuery = notconnectedToMachine.replace("[siloID]", data.siloID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the silo data
|
||||||
|
*/
|
||||||
|
const { data: s, error } = (await tryCatch(
|
||||||
|
query(newQuery, "Silo connection check")
|
||||||
|
)) as any;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
createLog(
|
||||||
|
"error",
|
||||||
|
"lst",
|
||||||
|
"logistics",
|
||||||
|
`There was an error getting the silo connection data: ${JSON.stringify(
|
||||||
|
error
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "There was an error getting the silo connection data.",
|
||||||
|
data: error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `silo ${data.connectionType} data`,
|
||||||
|
data: s.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -21,6 +21,9 @@ import intervalChecks from "./route/getActiveLogistics.js";
|
|||||||
import getActiveLanes from "./route/getActiveLanes.js";
|
import getActiveLanes from "./route/getActiveLanes.js";
|
||||||
import removeAsNonReable from "./route/removeAsNonReusable.js";
|
import removeAsNonReable from "./route/removeAsNonReusable.js";
|
||||||
import getSSCC from "./route/getSSCCNumber.js";
|
import getSSCC from "./route/getSSCCNumber.js";
|
||||||
|
import getConnectionType from "./route/getSiloConnectionData.js";
|
||||||
|
import detachSilo from "./route/detachSilo.js";
|
||||||
|
import attachSilo from "./route/attachSilo.js";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -33,6 +36,9 @@ const routes = [
|
|||||||
postComment,
|
postComment,
|
||||||
getStockSilo,
|
getStockSilo,
|
||||||
getSiloAdjustments,
|
getSiloAdjustments,
|
||||||
|
getConnectionType,
|
||||||
|
detachSilo,
|
||||||
|
attachSilo,
|
||||||
//lanes
|
//lanes
|
||||||
getCycleCountCheck,
|
getCycleCountCheck,
|
||||||
//warehouse
|
//warehouse
|
||||||
|
|||||||
65
server/services/logistics/route/attachSilo.ts
Normal file
65
server/services/logistics/route/attachSilo.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
|
import { responses } from "../../../globalUtils/routeDefs/responses.js";
|
||||||
|
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
||||||
|
import { apiHit } from "../../../globalUtils/apiHits.js";
|
||||||
|
import { attachSilo } from "../controller/siloAttachments/attachSilo.js";
|
||||||
|
|
||||||
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
// const Body = z
|
||||||
|
// .object({
|
||||||
|
// age: z.number().optional().openapi({ example: 90 }),
|
||||||
|
// //email: z.string().optional().openapi({example: "s.smith@example.com"}),
|
||||||
|
// type: z.string().optional().openapi({ example: "fg" }),
|
||||||
|
// })
|
||||||
|
// .openapi("User");
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
tags: ["logistics"],
|
||||||
|
summary: "Returns all the silo connection based on connection type",
|
||||||
|
method: "post",
|
||||||
|
path: "/attachsilo",
|
||||||
|
// request: {
|
||||||
|
// body: {
|
||||||
|
// content: {
|
||||||
|
// "application/json": { schema: Body },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// description:
|
||||||
|
// "Provided a running number and lot number you can consume material.",
|
||||||
|
responses: responses(),
|
||||||
|
}),
|
||||||
|
async (c: any) => {
|
||||||
|
apiHit(c, { endpoint: "/attachSilo" });
|
||||||
|
|
||||||
|
const { data: body, error: bodyError } = await tryCatch(c.req.json());
|
||||||
|
|
||||||
|
if (bodyError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Missing mandatory data",
|
||||||
|
data: [{ error: "Missing Data" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = body as any;
|
||||||
|
|
||||||
|
const { data: silo, error } = await tryCatch(attachSilo(b));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
message: "Error detaching silo.",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: silo.success,
|
||||||
|
message: silo.message,
|
||||||
|
data: silo.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default app;
|
||||||
67
server/services/logistics/route/detachSilo.ts
Normal file
67
server/services/logistics/route/detachSilo.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
|
import { responses } from "../../../globalUtils/routeDefs/responses.js";
|
||||||
|
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
||||||
|
import { apiHit } from "../../../globalUtils/apiHits.js";
|
||||||
|
|
||||||
|
import { siloConnectionType } from "../controller/siloAttachments/siloConnectionData.js";
|
||||||
|
import { detachSilo } from "../controller/siloAttachments/detachSilo.js";
|
||||||
|
|
||||||
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
// const Body = z
|
||||||
|
// .object({
|
||||||
|
// age: z.number().optional().openapi({ example: 90 }),
|
||||||
|
// //email: z.string().optional().openapi({example: "s.smith@example.com"}),
|
||||||
|
// type: z.string().optional().openapi({ example: "fg" }),
|
||||||
|
// })
|
||||||
|
// .openapi("User");
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
tags: ["logistics"],
|
||||||
|
summary: "Returns all the silo connection based on connection type",
|
||||||
|
method: "post",
|
||||||
|
path: "/detachsilo",
|
||||||
|
// request: {
|
||||||
|
// body: {
|
||||||
|
// content: {
|
||||||
|
// "application/json": { schema: Body },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// description:
|
||||||
|
// "Provided a running number and lot number you can consume material.",
|
||||||
|
responses: responses(),
|
||||||
|
}),
|
||||||
|
async (c: any) => {
|
||||||
|
apiHit(c, { endpoint: "/attachSilo" });
|
||||||
|
|
||||||
|
const { data: body, error: bodyError } = await tryCatch(c.req.json());
|
||||||
|
|
||||||
|
if (bodyError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Missing mandatory data",
|
||||||
|
data: [{ error: "Missing Data" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = body as any;
|
||||||
|
|
||||||
|
const { data: silo, error } = await tryCatch(detachSilo(b));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
message: "Error detaching silo.",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: silo.success,
|
||||||
|
message: silo.message,
|
||||||
|
data: silo.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default app;
|
||||||
66
server/services/logistics/route/getSiloConnectionData.ts
Normal file
66
server/services/logistics/route/getSiloConnectionData.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
|
import { responses } from "../../../globalUtils/routeDefs/responses.js";
|
||||||
|
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
||||||
|
import { apiHit } from "../../../globalUtils/apiHits.js";
|
||||||
|
|
||||||
|
import { siloConnectionType } from "../controller/siloAttachments/siloConnectionData.js";
|
||||||
|
|
||||||
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
// const Body = z
|
||||||
|
// .object({
|
||||||
|
// age: z.number().optional().openapi({ example: 90 }),
|
||||||
|
// //email: z.string().optional().openapi({example: "s.smith@example.com"}),
|
||||||
|
// type: z.string().optional().openapi({ example: "fg" }),
|
||||||
|
// })
|
||||||
|
// .openapi("User");
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
tags: ["logistics"],
|
||||||
|
summary: "Returns all the silo connection based on connection type",
|
||||||
|
method: "post",
|
||||||
|
path: "/siloconnection",
|
||||||
|
// request: {
|
||||||
|
// body: {
|
||||||
|
// content: {
|
||||||
|
// "application/json": { schema: Body },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// description:
|
||||||
|
// "Provided a running number and lot number you can consume material.",
|
||||||
|
responses: responses(),
|
||||||
|
}),
|
||||||
|
async (c: any) => {
|
||||||
|
apiHit(c, { endpoint: "/siloconnection" });
|
||||||
|
|
||||||
|
const { data: body, error: bodyError } = await tryCatch(c.req.json());
|
||||||
|
|
||||||
|
if (bodyError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Missing mandatory data",
|
||||||
|
data: [{ error: "Missing Data" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let b = body as any;
|
||||||
|
|
||||||
|
const { data: silo, error } = await tryCatch(siloConnectionType(b));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return c.json({
|
||||||
|
success: false,
|
||||||
|
message: "Error getting silo connection data.",
|
||||||
|
data: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: silo.success,
|
||||||
|
message: silo.message,
|
||||||
|
data: silo.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
export default app;
|
||||||
42
server/services/sqlServer/querys/silo/connectionCheck.ts
Normal file
42
server/services/sqlServer/querys/silo/connectionCheck.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export const notconnectedToMachine = `
|
||||||
|
select distinct HumanReadableId as machineId
|
||||||
|
,Location as location
|
||||||
|
,name
|
||||||
|
--,[SiloHumanReadableId]
|
||||||
|
from [test1_AlplaPROD2.0_Read].[masterData].[Machine] (nolock) m
|
||||||
|
|
||||||
|
left join
|
||||||
|
[test1_AlplaPROD2.0_Read].[issueMaterial].[SiloAssignment] (nolock) s
|
||||||
|
on s.MachineId = m.id
|
||||||
|
|
||||||
|
|
||||||
|
where m.id not in (
|
||||||
|
SELECT
|
||||||
|
[MachineId]
|
||||||
|
|
||||||
|
FROM [test1_AlplaPROD2.0_Read].[issueMaterial].[SiloAssignment]
|
||||||
|
|
||||||
|
where [SiloHumanReadableId] = [siloID]
|
||||||
|
)
|
||||||
|
|
||||||
|
and name not like '%REWORK%'
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const connectedToMachine = `
|
||||||
|
SELECT
|
||||||
|
[SiloHumanReadableId]
|
||||||
|
,[SiloDescription]
|
||||||
|
,[MaterialHumanReadableId]
|
||||||
|
,[MaterialDescription]
|
||||||
|
,[ConnectionDate]
|
||||||
|
,m.HumanReadableId as machineId
|
||||||
|
,m.Location as location
|
||||||
|
,m.Name as name
|
||||||
|
FROM [test1_AlplaPROD2.0_Read].[issueMaterial].[SiloAssignment] s
|
||||||
|
|
||||||
|
left join
|
||||||
|
[test1_AlplaPROD2.0_Read].[masterData].[Machine] m
|
||||||
|
on m.id = s.MachineId
|
||||||
|
|
||||||
|
where [SiloHumanReadableId] = [siloID]
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user