add i18n, products, productors and forms as tables

This commit is contained in:
2026-02-12 00:30:28 +01:00
parent 1813e2893e
commit 025b78d5dd
43 changed files with 1623 additions and 527 deletions

View File

@@ -1,4 +1,4 @@
import { Grid, NumberInput, Paper, Select, Stack, TextInput } from "@mantine/core";
import { Grid, NumberInput, Select, Stack, TextInput } from "@mantine/core";
import { t } from "../../config/i18n";
export type CreateProductProps = {
@@ -11,22 +11,22 @@ export default function CreateProduct({form}: CreateProductProps) {
<Grid>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<TextInput
label={t("product name")}
placeholder={t("product name")}
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<NumberInput
label={t("product price")}
placeholder={t("product price")}
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('price')}
/>
<TextInput
label={t("product weight")}
placeholder={t("product weight")}
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight')}
@@ -34,32 +34,32 @@ export default function CreateProduct({form}: CreateProductProps) {
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 6 }}>
<Select
label={t("product type")}
placeholder={t("product type")}
label={t("product type", {capfirst: true})}
placeholder={t("product type", {capfirst: true})}
radius="sm"
data={[
{value: "1", label: t("planned")},
{value: "2", label: t("reccurent")}
{value: "1", label: t("planned", {capfirst: true})},
{value: "2", label: t("reccurent", {capfirst: true})}
]}
defaultValue={"1"}
clearable
{...form.getInputProps('type')}
/>
<NumberInput
label={t("product price kg")}
placeholder={t("product price kg")}
label={t("product price kg", {capfirst: true})}
placeholder={t("product price kg", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('pricekg')}
{...form.getInputProps('pricekg', {capfirst: true})}
/>
<Select
label={t("product unit")}
placeholder={t("product unit")}
label={t("product unit", {capfirst: true})}
placeholder={t("product unit", {capfirst: true})}
radius="sm"
data={[
{value: "1", label: t("grams")},
{value: "2", label: t("kilo")},
{value: "3", label: t("piece")}
{value: "1", label: t("grams", {capfirst: true})},
{value: "2", label: t("kilo", {capfirst: true})},
{value: "3", label: t("piece", {capfirst: true})}
]}
defaultValue={"2"}
clearable

View File

@@ -1,62 +0,0 @@
import { Button, Group, Loader, Modal, Text, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "../../config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel, IconPlus } from "@tabler/icons-react";
import { createProductor, type Productor } from "../../services/api";
import { useEffect } from "react";
export function CreateProductorModal({opened, onClose}: ModalBaseProps) {
const form = useForm<Productor>();
const mutation = createProductor();
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create productor")}
>
<Title order={4}>{t("Informations")}</Title>
<TextInput
label={t("productor name")}
placeholder={t("productor name")}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("productor address")}
placeholder={t("productor address")}
radius="sm"
withAsterisk
{...form.getInputProps('address')}
/>
<TextInput
label={t("productor payment")}
placeholder={t("productor payment")}
radius="sm"
withAsterisk
{...form.getInputProps('payment')}
/>
{mutation.isError ? <Text>{t("an error occured")}:{mutation.error.message}</Text> : null}
{mutation.isSuccess ? <Text>{t("success")}</Text> : null}
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel")}
leftSection={<IconCancel/>}
onClick={onClose}
>{t("cancel")}</Button>
<Button
variant="filled"
aria-label={t("create productor")}
leftSection={mutation.isPending ? <Loader/> : <IconPlus/>}
onClick={() => {
mutation.mutate(form.getValues());
}}
>{t("create productor")}</Button>
</Group>
</Modal>
);
}

View File

@@ -1,5 +1,5 @@
import { Group, MultiSelect } from "@mantine/core";
import { t } from "../../../config/i18n";
import { t } from "@/config/i18n";
import { useMemo } from "react";
export type FilterFormsProps = {
@@ -9,7 +9,12 @@ export type FilterFormsProps = {
onFilterChange: (values: string[], filter: string) => void;
}
export function FilterForms({seasons, productors, filters, onFilterChange}: FilterFormsProps) {
export default function FilterForms({
seasons,
productors,
filters,
onFilterChange
}: FilterFormsProps) {
const defaultProductors = useMemo(() => {
return filters.getAll("productors")
}, [filters]);
@@ -20,8 +25,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
return (
<Group>
<MultiSelect
aria-label={t("filter by season")}
placeholder={t("filter by season")}
aria-label={t("filter by season", {capfirst: true})}
placeholder={t("filter by season", {capfirst: true})}
data={seasons}
defaultValue={defaultSeasons}
onChange={(values: string[]) => {
@@ -30,8 +35,8 @@ export function FilterForms({seasons, productors, filters, onFilterChange}: Filt
clearable
/>
<MultiSelect
aria-label={t("filter by productor")}
placeholder={t("filter by productor")}
aria-label={t("filter by productor", {capfirst: true})}
placeholder={t("filter by productor", {capfirst: true})}
data={productors}
defaultValue={defaultProductors}
onChange={(values: string[]) => {

View File

@@ -1,77 +0,0 @@
import { ActionIcon, Badge, Box, Group, Paper, Text, Title } from "@mantine/core";
import { Link, useNavigate } from "react-router";
import { deleteForm, getForm, type Form, type Shipment } from "../../../services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "../../../config/i18n";
import FormModal, { type FormInputs } from "../FormModal";
export type FormCardProps = {
form: Form;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (form: FormInputs, id?: number) => void;
}
export default function FormCard({form, isEdit, closeModal, handleSubmit}: FormCardProps) {
const deleteMutation = deleteForm();
const navigate = useNavigate();
const {data: currentForm, isPending} = getForm(form.id);
return (
<Paper
key={form.id}
shadow="xl"
p="xl"
miw={{base: "100vw", md: "25vw", lg:"20vw"}}
>
{/* TODO: Show only to logged users */}
<FormModal
opened={isEdit}
onClose={closeModal}
currentForm={currentForm}
handleSubmit={handleSubmit}
/>
<Group justify="space-between" mb="sm">
<ActionIcon
size={"sm"}
aria-label={t("edit form")}
onClick={(e) => {
e.stopPropagation();
navigate(`/form/${form.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
<ActionIcon
size={"sm"}
aria-label={t("delete form")}
color="red"
onClick={() => {
deleteMutation.mutate(form.id);
}}
>
<IconX/>
</ActionIcon>
</Group>
<Box
component={Link}
to={`/form/${form.id}`}
>
<Group justify="space-between" wrap="nowrap">
<Title
order={3}
textWrap="wrap"
lineClamp={1}
>
{form.name}
</Title>
<Badge>{form.season}</Badge>
</Group>
<Group justify="space-between">
<Text>{form.productor.name}</Text>
<Text>{form.referer.name}</Text>
</Group>
</Box>
</Paper>
);
}

View File

@@ -1,36 +1,26 @@
import { ActionIcon, Button, Collapse, Group, Modal, NumberInput, Select, TextInput, type ModalBaseProps } from "@mantine/core";
import { t } from "../../../config/i18n";
import { t } from "@/config/i18n";
import { DatePickerInput } from "@mantine/dates";
import { IconCancel, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { getProductors, getUsers, type Form, type Shipment } from "../../../services/api";
import { getProductors, getUsers } from "@/services/api";
import { useForm } from "@mantine/form";
import { useCallback, useEffect, useMemo } from "react";
import { useDisclosure } from "@mantine/hooks";
import ShipmentForm from "../../ShipmentForm";
export type FormInputs = {
name: string;
season: string;
start: string | null;
end: string | null;
productor_id: string;
referer_id: string;
shipments: ShipmentInputs[];
}
export type ShipmentInputs = {
name: string | null;
date: string | null;
id: number | null;
form_id: number | null;
}
import type { Form, FormInputs } from "@/services/resources/forms";
import type { ShipmentInputs } from "@/services/resources/shipments";
import ShipmentForm from "@/components/Shipments/Form";
export type FormModalProps = ModalBaseProps & {
currentForm?: Form;
handleSubmit: (form: FormInputs, id?: number) => void;
}
export default function FormModal({opened, onClose, currentForm, handleSubmit}: FormModalProps) {
export default function FormModal({
opened,
onClose,
currentForm,
handleSubmit
}: FormModalProps) {
const {data: productors} = getProductors();
const {data: users} = getUsers();
const form = useForm<FormInputs>({
@@ -42,6 +32,20 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
productor_id: "",
referer_id: "",
shipments: [],
},
validate: {
name: (value) =>
!value ? `${t("a name", {capfirst: true})} ${t('is required')}` : null,
season: (value) =>
!value ? `${t("a season", {capfirst: true})} ${t('is required')}` : null,
start: (value) =>
!value ? `${t("a start date", {capfirst: true})} ${t('is required')}` : null,
end: (value) =>
!value ? `${t("a end date", {capfirst: true})} ${t('is required')}` : null,
productor_id: (value) =>
!value ? `${t("a productor", {capfirst: true})} ${t('is required')}` : null,
referer_id: (value) =>
!value ? `${t("a referer", {capfirst: true})} ${t('is required')}` : null
}
});
@@ -68,7 +72,10 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
const [openedShipents, { toggle: toggleShipments }] = useDisclosure(true);
const editShipmentElement = useCallback((index: number, shipment: ShipmentInputs) => {
const editShipmentElement = useCallback((
index: number,
shipment: ShipmentInputs
) => {
form.setFieldValue('shipments', (prev) => {
return prev.map((elem, id) => {
if (id === index)
@@ -91,35 +98,35 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
title={currentForm ? t("edit form") : t('create form')}
>
<TextInput
label={t("form name")}
placeholder={t("form name")}
label={t("form name", {capfirst: true})}
placeholder={t("form name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("contact season")}
placeholder={t("contact season")}
label={t("contact season", {capfirst: true})}
placeholder={t("contact season", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('season')}
/>
<DatePickerInput
label={t("start date")}
placeholder={t("start date")}
label={t("start date", {capfirst: true})}
placeholder={t("start date", {capfirst: true})}
withAsterisk
{...form.getInputProps('start')}
/>
<DatePickerInput
label={t("end date")}
placeholder={t("end date")}
label={t("end date", {capfirst: true})}
placeholder={t("end date", {capfirst: true})}
withAsterisk
{...form.getInputProps('end')}
/>
<Select
label={t("referer")}
placeholder={t("referer")}
nothingFoundMessage={t("nothing found")}
label={t("referer", {capfirst: true})}
placeholder={t("referer", {capfirst: true})}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
@@ -128,9 +135,9 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
{...form.getInputProps('referer_id')}
/>
<Select
label={t("productor")}
placeholder={t("productor")}
nothingFoundMessage={t("nothing found")}
label={t("productor", {capfirst: true})}
placeholder={t("productor", {capfirst: true})}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
@@ -140,8 +147,8 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
/>
<Group align="end">
<NumberInput
label={t("number of shipment")}
placeholder={t("number of shipment")}
label={t("number of shipment", {capfirst: true})}
placeholder={t("number of shipment", {capfirst: true})}
radius="sm"
withAsterisk
flex="2"
@@ -162,14 +169,17 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
})
}}
/>
<ActionIcon onClick={toggleShipments} disabled={form.getValues().shipments.length === 0}>
<ActionIcon
onClick={toggleShipments}
disabled={form.getValues().shipments.length === 0}
>
{openedShipents ? <IconChevronUp/> : <IconChevronDown/>}
</ActionIcon>
</Group>
<Collapse in={openedShipents}>
{
form.getValues().shipments.map((value, index) =>
<ShipmentForm
<ShipmentForm
key={index}
index={index}
setShipmentElement={editShipmentElement}
@@ -183,18 +193,23 @@ export default function FormModal({opened, onClose, currentForm, handleSubmit}:
<Button
variant="filled"
color="red"
aria-label={t("cancel")}
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel")}</Button>
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentForm ? t("edit form") : t('create form')}
onClick={() => handleSubmit(form.getValues(), currentForm?.id)}
>{currentForm ? t("edit form") : t('create form')}</Button>
aria-label={currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid())
handleSubmit(form.getValues(), currentForm?.id)
}}
>{currentForm ? t("edit form", {capfirst: true}) : t('create form', {capfirst: true})}</Button>
</Group>
</Modal>
);

View File

@@ -0,0 +1,123 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { useNavigate } from "react-router";
import { deleteForm, getForm} from "@/services/api";
import { IconEdit, IconX } from "@tabler/icons-react";
import { t } from "@/config/i18n";
import type { Form, FormInputs } from "@/services/resources/forms";
import FormModal from "@/components/Forms/Modal";
export type FormRowProps = {
form: Form;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (form: FormInputs, id?: number) => void;
}
export default function FormRow({
form,
isEdit,
closeModal,
handleSubmit
}: FormRowProps) {
const deleteMutation = deleteForm();
const navigate = useNavigate();
const {data: currentForm, isPending} = getForm(form.id);
return (
<Table.Tr key={form.id}>
<FormModal
opened={isEdit}
onClose={closeModal}
currentForm={currentForm}
handleSubmit={handleSubmit}
/>
<Table.Td>{form.name}</Table.Td>
<Table.Td>{form.season}</Table.Td>
<Table.Td>{form.start}</Table.Td>
<Table.Td>{form.end}</Table.Td>
<Table.Td>{form.productor.name}</Table.Td>
<Table.Td>{form.referer.name}</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/forms/${form.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(form.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
// <Paper
// key={form.id}
// shadow="xl"
// p="xl"
// miw={{base: "100vw", md: "25vw", lg:"20vw"}}
// >
// {/* TODO: Show only to logged users */}
// <FormModal
// opened={isEdit}
// onClose={closeModal}
// currentForm={currentForm}
// handleSubmit={handleSubmit}
// />
// <Group justify="space-between" mb="sm">
// <ActionIcon
// size={"sm"}
// aria-label={t("edit form", {capfirst: true})}
// onClick={(e) => {
// e.stopPropagation();
// navigate(`/dashboard/forms/${form.id}/edit`);
// }}
// >
// <IconEdit/>
// </ActionIcon>
// <ActionIcon
// size={"sm"}
// aria-label={t("delete form", {capfirst: true})}
// color="red"
// onClick={() => {
// deleteMutation.mutate(form.id);
// }}
// >
// <IconX/>
// </ActionIcon>
// </Group>
// <Box
// component={Link}
// to={`/form/${form.id}`}
// >
// <Group justify="space-between" wrap="nowrap">
// <Title
// order={3}
// textWrap="wrap"
// lineClamp={1}
// >
// {form.name}
// </Title>
// <Badge>{form.season}</Badge>
// </Group>
// <Group justify="space-between">
// <Text>{form.productor.name}</Text>
// <Text>{form.referer.name}</Text>
// </Group>
// </Box>
// </Paper>
);
}

View File

@@ -1,12 +1,12 @@
import { NavLink } from "react-router";
import { t } from "../../config/i18n";
import { t } from "@/config/i18n";
import "./index.css";
export function Navbar() {
return (
<nav>
<NavLink to="/">{t("home")}</NavLink>
<NavLink to="/dashboard">{t("dashboard")}</NavLink>
<NavLink to="/forms">{t("forms")}</NavLink>
<NavLink to="/">{t("home", {capfirst: true})}</NavLink>
<NavLink to="/dashboard">{t("dashboard", {capfirst: true})}</NavLink>
</nav>
);
}

View File

@@ -0,0 +1,48 @@
import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react";
import { t } from "@/config/i18n";
export type ProductorsFiltersProps = {
names: string[];
types: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
export default function ProductorsFilter({
names,
types,
filters,
onFilterChange
}: ProductorsFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("name")
}, [filters]);
const defaultTypes = useMemo(() => {
return filters.getAll("type")
}, [filters]);
return (
<Group>
<MultiSelect
aria-label={t("filter by name", {capfirst: true})}
placeholder={t("filter by name", {capfirst: true})}
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
}}
clearable
/>
<MultiSelect
aria-label={t("filter by type", {capfirst: true})}
placeholder={t("filter by type", {capfirst: true})}
data={types}
defaultValue={defaultTypes}
onChange={(values: string[]) => {
onFilterChange(values, 'types')
}}
clearable
/>
</Group>
);
}

View File

@@ -0,0 +1,108 @@
import { Button, Group, Modal, Text, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import { useEffect } from "react";
export type ProductorModalProps = ModalBaseProps & {
currentProductor?: Productor;
handleSubmit: (productor: ProductorInputs, id?: number) => void;
}
export function ProductorModal({
opened,
onClose,
currentProductor,
handleSubmit
}: ProductorModalProps) {
const form = useForm<ProductorInputs>({
initialValues: {
name: "",
address: "",
payment: "",
type: "",
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
address: (value) =>
!value ? `${t("address", {capfirst: true})} ${t('is required')}` : null,
payment: (value) =>
!value ? `${t("payment", {capfirst: true})} ${t('is required')}` : null,
type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null
}
});
useEffect(() => {
if (currentProductor) {
form.initialize({
...currentProductor,
});
}
}, [currentProductor]);
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create productor", {capfirst: true})}
>
<Title order={4}>{t("Informations", {capfirst: true})}</Title>
<TextInput
label={t("productor name", {capfirst: true})}
placeholder={t("productor name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("productor type", {capfirst: true})}
placeholder={t("productor type", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('type')}
/>
<TextInput
label={t("productor address", {capfirst: true})}
placeholder={t("productor address", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('address')}
/>
<TextInput
label={t("productor payment", {capfirst: true})}
placeholder={t("productor payment", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('payment')}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid()) {
handleSubmit(form.getValues(), currentProductor?.id)
}
}}
>{currentProductor ? t("edit productor", {capfirst: true}) : t('create productor', {capfirst: true})}</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,67 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react";
import type { Productor, ProductorInputs } from "@/services/resources/productors";
import { ProductorModal } from "@/components/Productors/Modal";
import { deleteProductor, getProductor } from "@/services/api";
import { useNavigate } from "react-router";
export type ProductorRowProps = {
productor: Productor;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (productor: ProductorInputs, id?: number) => void;
}
export default function ProductorRow({
productor,
isEdit,
closeModal,
handleSubmit
}: ProductorRowProps) {
const deleteMutation = deleteProductor();
const navigate = useNavigate();
const {data: currentProductor, isPending} = getProductor(productor.id);
return (
<Table.Tr key={productor.id}>
<ProductorModal
opened={isEdit}
onClose={closeModal}
currentProductor={currentProductor}
handleSubmit={handleSubmit}
/>
<Table.Td>{productor.name}</Table.Td>
<Table.Td>{productor.type}</Table.Td>
<Table.Td>{productor.address}</Table.Td>
<Table.Td>{productor.payment}</Table.Td>
<Table.Td>
<Tooltip label={t("edit productor", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/productors/${productor.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove productor", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(productor.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -0,0 +1,48 @@
import { Group, MultiSelect } from "@mantine/core";
import { useMemo } from "react";
import { t } from "@/config/i18n";
export type ProductsFiltersProps = {
names: string[];
types: string[];
filters: URLSearchParams;
onFilterChange: (values: string[], filter: string) => void;
}
export default function ProductsFilters({
names,
types,
filters,
onFilterChange
}: ProductsFiltersProps) {
const defaultNames = useMemo(() => {
return filters.getAll("name")
}, [filters]);
const defaultTypes = useMemo(() => {
return filters.getAll("type")
}, [filters]);
return (
<Group>
<MultiSelect
aria-label={t("filter by name", {capfirst: true})}
placeholder={t("filter by name", {capfirst: true})}
data={names}
defaultValue={defaultNames}
onChange={(values: string[]) => {
onFilterChange(values, 'names')
}}
clearable
/>
<MultiSelect
aria-label={t("filter by type", {capfirst: true})}
placeholder={t("filter by type", {capfirst: true})}
data={types}
defaultValue={defaultTypes}
onChange={(values: string[]) => {
onFilterChange(values, 'types')
}}
clearable
/>
</Group>
);
}

View File

@@ -0,0 +1,139 @@
import { Button, Group, Modal, NumberInput, Select, TextInput, Title, type ModalBaseProps } from "@mantine/core";
import { t } from "@/config/i18n";
import { useForm } from "@mantine/form";
import { IconCancel } from "@tabler/icons-react";
import type { Product, ProductInputs } from "@/services/resources/products";
import { useEffect, useMemo } from "react";
import { getProductors } from "@/services/api";
export type ProductModalProps = ModalBaseProps & {
currentProduct?: Product;
handleSubmit: (product: ProductInputs, id?: number) => void;
}
export function ProductModal({
opened,
onClose,
currentProduct,
handleSubmit
}: ProductModalProps) {
const {data: productors} = getProductors();
const form = useForm<ProductInputs>({
initialValues: {
name: "",
unit: null,
price: null,
priceKg: null,
weight: null,
type: "",
productor_id: null,
},
validate: {
name: (value) =>
!value ? `${t("name", {capfirst: true})} ${t('is required')}` : null,
unit: (value) =>
!value ? `${t("unit", {capfirst: true})} ${t('is required')}` : null,
price: (value) =>
!value ? `${t("price", {capfirst: true})} ${t('is required')}` : null,
priceKg: (value) =>
!value ? `${t("priceKg", {capfirst: true})} ${t('is required')}` : null,
weight: (value) =>
!value ? `${t("weight", {capfirst: true})} ${t('is required')}` : null,
type: (value) =>
!value ? `${t("type", {capfirst: true})} ${t('is required')}` : null,
productor_id: (value) =>
!value ? `${t("productor", {capfirst: true})} ${t('is required')}` : null
}
});
useEffect(() => {
if (currentProduct) {
form.initialize({
...currentProduct,
});
}
}, [currentProduct]);
const productorsSelect = useMemo(() => {
return productors?.map(prod => ({value: String(prod.id), label: `${prod.name}`}))
}, [productors])
return (
<Modal
size="50%"
opened={opened}
onClose={onClose}
title={t("create product", {capfirst: true})}
>
<Title order={4}>{t("informations", {capfirst: true})}</Title>
<TextInput
label={t("product name", {capfirst: true})}
placeholder={t("product name", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('name')}
/>
<TextInput
label={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
placeholder={t("product type", {capfirst: true}, {capfirst: true}, {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('type')}
/>
<NumberInput
label={t("product price", {capfirst: true})}
placeholder={t("product price", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('price')}
/>
<NumberInput
label={t("product priceKg", {capfirst: true}, {capfirst: true})}
placeholder={t("product priceKg", {capfirst: true}, {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('priceKg')}
/>
<NumberInput
label={t("product weight", {capfirst: true})}
placeholder={t("product weight", {capfirst: true})}
radius="sm"
withAsterisk
{...form.getInputProps('weight', {capfirst: true})}
/>
<Select
label={t("productor", {capfirst: true})}
placeholder={t("productor")}
nothingFoundMessage={t("nothing found", {capfirst: true})}
withAsterisk
clearable
allowDeselect
searchable
data={productorsSelect || []}
{...form.getInputProps('productor_id')}
/>
<Group mt="sm" justify="space-between">
<Button
variant="filled"
color="red"
aria-label={t("cancel", {capfirst: true})}
leftSection={<IconCancel/>}
onClick={() => {
form.reset();
form.clearErrors();
onClose();
}}
>{t("cancel", {capfirst: true})}</Button>
<Button
variant="filled"
aria-label={currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}
onClick={() => {
form.validate();
if (form.isValid())
handleSubmit(form.getValues(), currentProduct?.id)
}}
>{currentProduct ? t("edit product", {capfirst: true}) : t('create product', {capfirst: true})}</Button>
</Group>
</Modal>
);
}

View File

@@ -0,0 +1,69 @@
import { ActionIcon, Table, Tooltip } from "@mantine/core";
import { t } from "@/config/i18n";
import { IconEdit, IconX } from "@tabler/icons-react";
import type { Product, ProductInputs } from "@/services/resources/products";
import { ProductModal } from "@/components/Products/Modal";
import { deleteProduct, getProduct } from "@/services/api";
import { useNavigate } from "react-router";
export type ProductRowProps = {
product: Product;
isEdit: boolean;
closeModal: () => void;
handleSubmit: (product: ProductInputs, id?: number) => void;
}
export default function ProductRow({
product,
isEdit,
closeModal,
handleSubmit
}: ProductRowProps) {
const deleteMutation = deleteProduct();
const navigate = useNavigate();
const {data: currentProduct, isPending} = getProduct(product.id);
return (
<Table.Tr key={product.id}>
<ProductModal
opened={isEdit}
onClose={closeModal}
currentProduct={currentProduct}
handleSubmit={handleSubmit}
/>
<Table.Td>{product.name}</Table.Td>
<Table.Td>{product.type}</Table.Td>
<Table.Td>{product.price}</Table.Td>
<Table.Td>{product.priceKg}</Table.Td>
<Table.Td>{product.weight}</Table.Td>
<Table.Td>{product.unit}</Table.Td>
<Table.Td>
<Tooltip label={t("edit product", {capfirst: true})}>
<ActionIcon
size="sm"
mr="5"
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/products/${product.id}/edit`);
}}
>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label={t("remove product", {capfirst: true})}>
<ActionIcon
color="red"
size="sm"
mr="5"
onClick={() => {
deleteMutation.mutate(product.id);
}}
>
<IconX/>
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
);
}

View File

@@ -1,50 +0,0 @@
import { Group, NumberInput, Paper, Stack, Text } from "@mantine/core";
import { useCallback, useState } from "react";
type Product = {
name: string;
price: number;
priceKg: number;
unit: string;
}
export type ShipmentCardProps = {
title: string;
date: string;
product: Product;
}
export default function ShipmentCard({title, date, product}: ShipmentCardProps) {
const [ price, setPrice ] = useState<number>(0)
const calculatePrice = useCallback((value: number | string ) => {
const numberValue = Number(value)
const price = numberValue * product.price
setPrice(price)
}, [])
return (
<Paper shadow="xs" p="xl">
<Group justify="space-between">
<Text>{title}</Text>
<Text>{date}</Text>
</Group>
<Text>{product.name}</Text>
<Group>
<Stack align="flex-start">
<Text>{product.price} / piece</Text>
<Text>{product.priceKg} / kilo</Text>
</Stack>
<NumberInput
aria-label="select quantity"
description={`Indiquez le nombre de ${product.unit}`}
placeholder={`Quantité en ${product.unit}`}
allowNegative={false}
onChange={calculatePrice}
/>
<Text size="xs">
{new Intl.NumberFormat('en-us', {minimumFractionDigits: 2}).format(price)}
</Text>
</Group>
</Paper>
);
}

View File

@@ -1,9 +1,8 @@
import { ActionIcon, Group, TextInput, Tooltip } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import { t } from "../../config/i18n";
import type { Shipment } from "../../services/api";
import { IconX } from "@tabler/icons-react";
import type { ShipmentInputs } from "../Forms/FormModal";
import { t } from "@/config/i18n";
import type { ShipmentInputs } from "@/services/resources/shipments";
export type ShipmentFormProps = {
index: number;
@@ -22,8 +21,8 @@ export default function ShipmentForm({
<Group justify="space-between" key={`shipment_${index}`}>
<Group grow maw="80%">
<TextInput
label={t("shipment name")}
placeholder={t("shipment name")}
label={t("shipment name", {capfirst: true})}
placeholder={t("shipment name", {capfirst: true})}
radius="sm"
withAsterisk
value={shipment.name || ""}
@@ -33,23 +32,23 @@ export default function ShipmentForm({
}}
/>
<DatePickerInput
label={t("shipment date")}
placeholder={t("shipment date")}
label={t("shipment date", {capfirst: true})}
placeholder={t("shipment date", {capfirst: true})}
radius="sm"
withAsterisk
value={shipment.date}
value={shipment.date || null}
onChange={(event) => {
const value = event || "";
setShipmentElement(index, {...shipment, date: value})
}}
/>
</Group>
<Tooltip label={t("remove shipment")}>
<Tooltip label={t("remove shipment", {capfirst: true})}>
<ActionIcon
flex={{base: "1", md: "0"}}
style={{alignSelf: "flex-end"}}
color="red"
aria-label={t("remove shipment")}
aria-label={t("remove shipment", {capfirst: true})}
onClick={() => {
deleteShipmentElement(index)
}}