feat: Add edit pages with cache

This commit is contained in:
2025-06-08 08:33:43 +03:00
parent e37f9e14bc
commit b09c1b3214
37 changed files with 2223 additions and 772 deletions

View File

@ -25,8 +25,14 @@ import {
SnapshotCreatePage,
CountryCreatePage,
CityCreatePage,
// CarrierCreatePage,
CarrierCreatePage,
VehicleCreatePage,
CountryEditPage,
CityEditPage,
UserCreatePage,
UserEditPage,
VehicleEditPage,
CarrierEditPage,
} from "@pages";
import { authStore, createSightStore, editSightStore } from "@shared";
import { Layout } from "@widgets";
@ -121,25 +127,28 @@ const router = createBrowserRouter([
{ path: "country", element: <CountryListPage /> },
{ path: "country/create", element: <CountryCreatePage /> },
{ path: "country/:id", element: <CountryPreviewPage /> },
{ path: "country/:id/edit", element: <CountryEditPage /> },
// City
{ path: "city", element: <CityListPage /> },
{ path: "city/create", element: <CityCreatePage /> },
{ path: "city/:id", element: <CityPreviewPage /> },
{ path: "city/:id/edit", element: <CityEditPage /> },
// Route
{ path: "route", element: <RouteListPage /> },
// User
{ path: "user", element: <UserListPage /> },
{ path: "user/create", element: <UserCreatePage /> },
{ path: "user/:id/edit", element: <UserEditPage /> },
// Snapshot
{ path: "snapshot", element: <SnapshotListPage /> },
{ path: "snapshot/create", element: <SnapshotCreatePage /> },
// Carrier
{ path: "carrier", element: <CarrierListPage /> },
// { path: "carrier/create", element: <CarrierCreatePage /> },
{ path: "carrier/create", element: <CarrierCreatePage /> },
{ path: "carrier/:id", element: <CarrierPreviewPage /> },
{ path: "carrier/:id/edit", element: <CarrierEditPage /> },
// Station
{ path: "station", element: <StationListPage /> },
@ -147,7 +156,7 @@ const router = createBrowserRouter([
{ path: "vehicle", element: <VehicleListPage /> },
{ path: "vehicle/create", element: <VehicleCreatePage /> },
{ path: "vehicle/:id", element: <VehiclePreviewPage /> },
{ path: "vehicle/:id/edit", element: <VehicleEditPage /> },
// Article
{ path: "article", element: <ArticleListPage /> },

View File

@ -4,7 +4,9 @@ export interface NavigationItem {
id: string;
label: string;
icon: LucideIcon;
path: string;
path?: string;
onClick?: () => void;
nestedItems?: NavigationItem[];
}
export type NavigationSection = "primary" | "secondary";

View File

@ -3,6 +3,10 @@ import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Collapse from "@mui/material/Collapse";
import List from "@mui/material/List";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import type { NavigationItem } from "../model";
import { useNavigate } from "react-router-dom";
@ -10,73 +14,100 @@ interface NavigationItemProps {
item: NavigationItem;
open: boolean;
onClick?: () => void;
isNested?: boolean;
}
export const NavigationItemComponent: React.FC<NavigationItemProps> = ({
item,
open,
onClick,
isNested = false,
}) => {
const Icon = item.icon;
const navigate = useNavigate();
const [isExpanded, setIsExpanded] = React.useState(false);
const handleClick = () => {
if (item.nestedItems) {
setIsExpanded(!isExpanded);
} else if (onClick) {
onClick();
} else if (item.path) {
navigate(item.path);
}
};
return (
<ListItem
onClick={() => {
if (onClick) {
onClick();
} else {
navigate(item.path);
}
}}
disablePadding
sx={{ display: "block" }}
>
<ListItemButton
sx={[
{
minHeight: 48,
px: 2.5,
},
open
? {
justifyContent: "initial",
}
: {
justifyContent: "center",
},
]}
>
<ListItemIcon
<>
<ListItem disablePadding sx={{ display: "block" }}>
<ListItemButton
onClick={handleClick}
sx={[
{
minWidth: 0,
justifyContent: "center",
minHeight: 48,
px: 2.5,
},
open
? {
mr: 3,
justifyContent: "initial",
}
: {
mr: "auto",
justifyContent: "center",
},
isNested && {
pl: 4,
},
]}
>
<Icon />
</ListItemIcon>
<ListItemText
primary={item.label}
sx={[
open
? {
opacity: 1,
}
: {
opacity: 0,
},
]}
/>
</ListItemButton>
</ListItem>
<ListItemIcon
sx={[
{
minWidth: 0,
justifyContent: "center",
},
open
? {
mr: 3,
}
: {
mr: "auto",
},
]}
>
<Icon />
</ListItemIcon>
<ListItemText
primary={item.label}
sx={[
open
? {
opacity: 1,
}
: {
opacity: 0,
},
]}
/>
{item.nestedItems &&
open &&
(isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />)}
</ListItemButton>
</ListItem>
{item.nestedItems && (
<Collapse in={isExpanded && open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.nestedItems.map((nestedItem) => (
<NavigationItemComponent
key={nestedItem.id}
item={nestedItem}
open={open}
onClick={nestedItem.onClick}
isNested={true}
/>
))}
</List>
</Collapse>
)}
</>
);
};

View File

@ -23,15 +23,15 @@ export const CarrierCreatePage = observer(() => {
const [fullName, setFullName] = useState("");
const [shortName, setShortName] = useState("");
const [cityId, setCityId] = useState<number | null>(null);
const [primaryColor, setPrimaryColor] = useState("#000000");
const [secondaryColor, setSecondaryColor] = useState("#ffffff");
const [accentColor, setAccentColor] = useState("#ff0000");
const [main_color, setMainColor] = useState("#000000");
const [left_color, setLeftColor] = useState("#ffffff");
const [right_color, setRightColor] = useState("#ff0000");
const [slogan, setSlogan] = useState("");
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
cityStore.getCities();
cityStore.getCities("ru");
mediaStore.getMedia();
}, []);
@ -41,11 +41,11 @@ export const CarrierCreatePage = observer(() => {
await carrierStore.createCarrier(
fullName,
shortName,
cityStore.cities.find((c) => c.id === cityId)?.name!,
cityStore.cities.ru.find((c) => c.id === cityId)?.name!,
cityId!,
primaryColor,
secondaryColor,
accentColor,
main_color,
left_color,
right_color,
slogan,
selectedMediaId!
);
@ -60,7 +60,6 @@ export const CarrierCreatePage = observer(() => {
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
@ -80,7 +79,7 @@ export const CarrierCreatePage = observer(() => {
required
onChange={(e) => setCityId(e.target.value as number)}
>
{cityStore.cities.map((city) => (
{cityStore.cities.ru.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
@ -104,51 +103,55 @@ export const CarrierCreatePage = observer(() => {
onChange={(e) => setShortName(e.target.value)}
/>
<div className="w-full flex flex-col gap-4">
<div className="flex items-center gap-4">
<span className="w-32">Основной цвет:</span>
<Box
sx={{
width: 40,
height: 40,
backgroundColor: primaryColor,
border: "1px solid #ccc",
<div className="flex gap-4 w-full ">
<TextField
fullWidth
label="Основной цвет"
value={main_color}
className="flex-1 w-full"
onChange={(e) => setMainColor(e.target.value)}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
}}
/>
<HexColorPicker color={primaryColor} onChange={setPrimaryColor} />
</div>
<div className="flex items-center gap-4">
<span className="w-32">Вторичный цвет:</span>
<Box
sx={{
width: 40,
height: 40,
backgroundColor: secondaryColor,
border: "1px solid #ccc",
},
}}
/>
<TextField
fullWidth
label="Цвет левого виджета"
value={left_color}
className="flex-1 w-full"
onChange={(e) => setLeftColor(e.target.value)}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
}}
/>
<HexColorPicker
color={secondaryColor}
onChange={setSecondaryColor}
/>
</div>
<div className="flex items-center gap-4">
<span className="w-32">Акцентный цвет:</span>
<Box
sx={{
width: 40,
height: 40,
backgroundColor: accentColor,
border: "1px solid #ccc",
},
}}
/>
<TextField
fullWidth
label="Цвет правого виджета"
value={right_color}
className="flex-1 w-full"
onChange={(e) => setRightColor(e.target.value)}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
}}
/>
<HexColorPicker color={accentColor} onChange={setAccentColor} />
</div>
},
}}
/>
</div>
<TextField
@ -167,11 +170,13 @@ export const CarrierCreatePage = observer(() => {
required
onChange={(e) => setSelectedMediaId(e.target.value as string)}
>
{mediaStore.media.map((media) => (
<MenuItem key={media.id} value={media.id}>
{media.media_name || media.filename}
</MenuItem>
))}
{mediaStore.media
.filter((media) => media.media_type === 3)
.map((media) => (
<MenuItem key={media.id} value={media.id}>
{media.media_name || media.filename}
</MenuItem>
))}
</Select>
</FormControl>
{selectedMediaId && (

View File

@ -0,0 +1,307 @@
import {
Button,
Paper,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { carrierStore, cityStore, mediaStore } from "@shared";
import { useState, useEffect } from "react";
import { MediaViewer } from "@widgets";
export const CarrierEditPage = observer(() => {
const navigate = useNavigate();
const { id } = useParams();
const { carrier, getCarrier, setEditCarrierData, editCarrierData } =
carrierStore;
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
(async () => {
await getCarrier(Number(id));
setEditCarrierData(
carrier?.[Number(id)]?.full_name as string,
carrier?.[Number(id)]?.short_name as string,
carrier?.[Number(id)]?.city as string,
carrier?.[Number(id)]?.city_id as number,
carrier?.[Number(id)]?.main_color as string,
carrier?.[Number(id)]?.left_color as string,
carrier?.[Number(id)]?.right_color as string,
carrier?.[Number(id)]?.slogan as string,
carrier?.[Number(id)]?.logo as string
);
cityStore.getCities("ru");
mediaStore.getMedia();
})();
}, [id]);
const handleEdit = async () => {
try {
setIsLoading(true);
await carrierStore.editCarrier(Number(id));
toast.success("Перевозчик успешно обновлен");
navigate("/carrier");
} catch (error) {
toast.error("Ошибка при обновлении перевозчика");
} finally {
setIsLoading(false);
}
};
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/carrier")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<FormControl fullWidth>
<InputLabel>Город</InputLabel>
<Select
value={editCarrierData.city_id || ""}
label="Город"
required
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
Number(e.target.value),
editCarrierData.main_color,
editCarrierData.left_color,
editCarrierData.right_color,
editCarrierData.slogan,
editCarrierData.logo
)
}
>
{cityStore.cities.ru.map((city) => (
<MenuItem key={city.id} value={city.id}>
{city.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
fullWidth
label="Полное название"
value={editCarrierData.full_name}
required
onChange={(e) =>
setEditCarrierData(
e.target.value,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
editCarrierData.left_color,
editCarrierData.right_color,
editCarrierData.slogan,
editCarrierData.logo
)
}
/>
<TextField
fullWidth
label="Короткое название"
value={editCarrierData.short_name}
required
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
e.target.value,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
editCarrierData.left_color,
editCarrierData.right_color,
editCarrierData.slogan,
editCarrierData.logo
)
}
/>
<div className="flex gap-4 w-full">
<TextField
fullWidth
label="Основной цвет"
value={editCarrierData.main_color}
className="flex-1 w-full"
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
e.target.value,
editCarrierData.left_color,
editCarrierData.right_color,
editCarrierData.slogan,
editCarrierData.logo
)
}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
fullWidth
label="Цвет левого виджета"
value={editCarrierData.left_color}
className="flex-1 w-full"
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
e.target.value,
editCarrierData.right_color,
editCarrierData.slogan,
editCarrierData.logo
)
}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
<TextField
fullWidth
label="Цвет правого виджета"
value={editCarrierData.right_color}
className="flex-1 w-full"
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
editCarrierData.left_color,
e.target.value,
editCarrierData.slogan,
editCarrierData.logo
)
}
type="color"
sx={{
"& input": {
height: "50px",
paddingBlock: "14px",
paddingInline: "14px",
cursor: "pointer",
},
}}
/>
</div>
<TextField
fullWidth
label="Слоган"
value={editCarrierData.slogan}
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
editCarrierData.left_color,
editCarrierData.right_color,
e.target.value,
editCarrierData.logo
)
}
/>
<div className="w-full flex flex-col gap-4">
<FormControl fullWidth>
<InputLabel>Логотип</InputLabel>
<Select
value={editCarrierData.logo || ""}
label="Логотип"
required
onChange={(e) =>
setEditCarrierData(
editCarrierData.full_name,
editCarrierData.short_name,
editCarrierData.city,
editCarrierData.city_id,
editCarrierData.main_color,
editCarrierData.left_color,
editCarrierData.right_color,
editCarrierData.slogan,
e.target.value as string
)
}
>
{mediaStore.media
.filter((media) => media.media_type === 3)
.map((media) => (
<MenuItem key={media.id} value={media.id}>
{media.media_name || media.filename}
</MenuItem>
))}
</Select>
</FormControl>
{editCarrierData.logo && (
<div className="w-32 h-32">
<MediaViewer
media={{ id: editCarrierData.logo, media_type: 1 }}
/>
</div>
)}
</div>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={
isLoading ||
!editCarrierData.full_name ||
!editCarrierData.short_name ||
!editCarrierData.city_id ||
!editCarrierData.logo
}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Обновить"
)}
</Button>
</div>
</Paper>
);
});

View File

@ -1,21 +1,20 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { carrierStore, languageStore } from "@shared";
import { carrierStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { DeleteModal, LanguageSwitcher } from "@widgets";
import { CreateButton, DeleteModal } from "@widgets";
export const CarrierListPage = observer(() => {
const { carriers, getCarriers, deleteCarrier } = carrierStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
const { language } = languageStore;
useEffect(() => {
getCarriers();
}, [language]);
}, []);
const columns: GridColDef[] = [
{
@ -37,10 +36,15 @@ export const CarrierListPage = observer(() => {
{
field: "actions",
headerName: "Действия",
headerAlign: "center",
width: 200,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button onClick={() => navigate(`/carrier/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/carrier/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button>
@ -67,12 +71,10 @@ export const CarrierListPage = observer(() => {
return (
<>
<LanguageSwitcher />
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Перевозчики</h1>
{/* <CreateButton label="Создать перевозчика" path="/carrier/create" /> */}
<CreateButton label="Создать перевозчика" path="/carrier/create" />
</div>
<DataGrid
rows={rows}
@ -86,7 +88,7 @@ export const CarrierListPage = observer(() => {
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
deleteCarrier(rowId);
await deleteCarrier(rowId);
}
setIsDeleteModalOpen(false);
setRowId(null);

View File

@ -1,5 +1,5 @@
import { Paper } from "@mui/material";
import { carrierStore, mediaStore } from "@shared";
import { carrierStore, languageStore, mediaStore } from "@shared";
import { MediaViewer } from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
@ -8,13 +8,25 @@ import { useNavigate, useParams } from "react-router-dom";
export const CarrierPreviewPage = observer(() => {
const { id } = useParams();
const { getCarrier, carrier } = carrierStore;
const { getCarrier, carrier, setEditCarrierData } = carrierStore;
const { oneMedia, getOneMedia } = mediaStore;
const navigate = useNavigate();
useEffect(() => {
(async () => {
const carrierResponse = await getCarrier(Number(id));
setEditCarrierData(
carrierResponse?.full_name as string,
carrierResponse?.short_name as string,
carrierResponse?.city as string,
carrierResponse?.city_id as number,
carrierResponse?.main_color as string,
carrierResponse?.left_color as string,
carrierResponse?.right_color as string,
carrierResponse?.slogan as string,
carrierResponse?.logo as string
);
console.log(carrierResponse);
await getOneMedia(carrierResponse?.logo as string);
})();
}, [id]);
@ -31,48 +43,30 @@ export const CarrierPreviewPage = observer(() => {
<ArrowLeft size={20} />
Назад
</button>
{/* <div className="flex gap-2">
<Button
variant="contained"
color="primary"
onClick={() => navigate(`/carrier/${id}/edit`)}
startIcon={<Pencil size={20} />}
>
Редактировать
</Button>
<Button
variant="contained"
color="error"
onClick={() => navigate(`/carrier/${id}/delete`)}
startIcon={<Trash2 size={20} />}
>
Удалить
</Button>
</div> */}
</div>
<div className="flex flex-col gap-10 w-full">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Полное имя</h1>
<p>{carrier?.full_name}</p>
<p>{carrier[Number(id)]?.full_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Полное имя</h1>
<p>{carrier?.full_name}</p>
<p>{carrier[Number(id)]?.full_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Город</h1>
<p>{carrier?.city}</p>
<p>{carrier[Number(id)]?.city}</p>
</div>
<div className="flex flex-col gap-2 ">
<h1 className="text-lg font-bold">Основной цвет</h1>
<div
className="w-min"
style={{
backgroundColor: `${carrier?.main_color}90`,
backgroundColor: `${carrier[Number(id)]?.main_color}90`,
}}
>
{carrier?.main_color}
{carrier[Number(id)]?.main_color}
</div>
</div>
<div className="flex flex-col gap-2">
@ -80,10 +74,10 @@ export const CarrierPreviewPage = observer(() => {
<div
className="w-min"
style={{
backgroundColor: `${carrier?.left_color}90`,
backgroundColor: `${carrier[Number(id)]?.left_color}90`,
}}
>
{carrier?.left_color}
{carrier[Number(id)]?.left_color}
</div>
</div>
<div className="flex flex-col gap-2">
@ -91,15 +85,15 @@ export const CarrierPreviewPage = observer(() => {
<div
className="w-min"
style={{
backgroundColor: `${carrier?.right_color}90`,
backgroundColor: `${carrier[Number(id)]?.right_color}90`,
}}
>
{carrier?.right_color}
{carrier[Number(id)]?.right_color}
</div>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Краткое имя</h1>
<p>{carrier?.short_name}</p>
<p>{carrier[Number(id)]?.short_name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Логотип</h1>

View File

@ -1,3 +1,4 @@
export * from "./CarrierListPage";
export * from "./CarrierPreviewPage";
export * from "./CarrierCreatePage";
export * from "./CarrierEditPage";

View File

@ -13,33 +13,31 @@ import { ArrowLeft, Save, ImagePlus } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { cityStore, countryStore, mediaStore } from "@shared";
import { cityStore, countryStore, languageStore, mediaStore } from "@shared";
import { useState, useEffect } from "react";
import { LanguageSwitcher, MediaViewer } from "@widgets";
import { SelectMediaDialog } from "@shared";
export const CityCreatePage = observer(() => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [countryCode, setCountryCode] = useState("");
const [selectedMediaId, setSelectedMediaId] = useState<string | null>(null);
const { language } = languageStore;
const { createCityData, setCreateCityData } = cityStore;
const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const { getCountries } = countryStore;
const { getMedia } = mediaStore;
useEffect(() => {
countryStore.getCountries();
mediaStore.getMedia();
}, []);
(async () => {
await getCountries(language);
await getMedia();
})();
}, [language]);
const handleCreate = async () => {
try {
setIsLoading(true);
await cityStore.createCity(
name,
countryStore.countries.find((c) => c.code === countryCode)?.name!,
countryCode,
selectedMediaId!
);
await cityStore.createCity();
toast.success("Город успешно создан");
navigate("/city");
} catch (error) {
@ -55,11 +53,17 @@ export const CityCreatePage = observer(() => {
media_name?: string;
media_type: number;
}) => {
setSelectedMediaId(media.id);
setCreateCityData(
createCityData[language].name,
createCityData.country,
createCityData.country_code,
media.id,
language
);
};
const selectedMedia = selectedMediaId
? mediaStore.media.find((m) => m.id === selectedMediaId)
const selectedMedia = createCityData.arms
? mediaStore.media.find((m) => m.id === createCityData.arms)
: null;
return (
@ -79,20 +83,39 @@ export const CityCreatePage = observer(() => {
<TextField
fullWidth
label="Название города"
value={name}
value={createCityData[language]?.name || ""}
required
onChange={(e) => setName(e.target.value)}
onChange={(e) =>
setCreateCityData(
e.target.value,
createCityData.country,
createCityData.country_code,
createCityData.arms,
language
)
}
/>
<FormControl fullWidth>
<InputLabel>Страна</InputLabel>
<Select
value={countryCode}
value={createCityData.country_code || ""}
label="Страна"
required
onChange={(e) => setCountryCode(e.target.value)}
onChange={(e) => {
const selectedCountry = countryStore.countries[language]?.find(
(country) => country.code === e.target.value
);
setCreateCityData(
createCityData[language].name,
selectedCountry?.name || "",
e.target.value,
createCityData.arms,
language
);
}}
>
{countryStore.countries.map((country) => (
{countryStore.countries[language].map((country) => (
<MenuItem key={country.code} value={country.code}>
{country.name}
</MenuItem>
@ -145,7 +168,7 @@ export const CityCreatePage = observer(() => {
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={isLoading || !name || !countryCode}
disabled={isLoading || !createCityData[language]?.name}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />

View File

@ -0,0 +1,207 @@
import {
Button,
Paper,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Box,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save, ImagePlus } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import {
cityStore,
countryStore,
languageStore,
mediaStore,
CashedCities,
} from "@shared";
import { useEffect, useState } from "react";
import { LanguageSwitcher, MediaViewer } from "@widgets";
import { SelectMediaDialog } from "@shared";
export const CityEditPage = observer(() => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [isSelectMediaOpen, setIsSelectMediaOpen] = useState(false);
const { language } = languageStore;
const { id } = useParams();
const { editCityData, editCity, getCity, setEditCityData } = cityStore;
const { getCountries } = countryStore;
const { getMedia, getOneMedia, oneMedia } = mediaStore;
const handleEdit = async () => {
try {
setIsLoading(true);
await editCity(id as string);
toast.success("Город успешно обновлен");
} catch (error) {
toast.error("Ошибка при обновлении города");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
(async () => {
if (id) {
const data = await getCity(id as string, language);
setEditCityData(
data.name,
data.country,
data.country_code,
data.arms,
language
);
await getOneMedia(data.arms as string);
await getCountries(language);
await getMedia();
}
})();
}, [id, language]);
const handleMediaSelect = (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
setEditCityData(
editCityData[language].name,
editCityData.country,
editCityData.country_code,
media.id,
language
);
};
const selectedMedia = editCityData.arms
? mediaStore.media.find((m) => m.id === editCityData.arms)
: null;
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/city")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
fullWidth
label="Название"
value={editCityData[language].name}
required
onChange={(e) =>
setEditCityData(
e.target.value,
editCityData.country,
editCityData.country_code,
editCityData.arms,
language
)
}
/>
<FormControl fullWidth>
<InputLabel>Страна</InputLabel>
<Select
value={editCityData.country_code || ""}
label="Страна"
required
onChange={(e) => {
const selectedCountry = countryStore.countries[language]?.find(
(country) => country.code === e.target.value
);
setEditCityData(
editCityData[language as keyof CashedCities]?.name || "",
selectedCountry?.name || "",
e.target.value,
editCityData.arms,
language
);
}}
>
{countryStore.countries[language].map((country) => (
<MenuItem key={country.code} value={country.code}>
{country.name}
</MenuItem>
))}
</Select>
</FormControl>
<div className="w-full flex flex-col gap-4">
<label className="text-sm text-gray-600">Герб города</label>
<div className="flex items-center gap-4">
<Button
variant="outlined"
onClick={() => setIsSelectMediaOpen(true)}
startIcon={<ImagePlus size={20} />}
>
Выбрать герб
</Button>
{selectedMedia && (
<span className="text-sm text-gray-600">
{selectedMedia.media_name || selectedMedia.filename}
</span>
)}
</div>
{selectedMedia && (
<Box
sx={{
width: "200px",
height: "200px",
border: "1px solid #e0e0e0",
borderRadius: "8px",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<MediaViewer
media={{
id: selectedMedia.id,
media_type: selectedMedia.media_type,
filename: selectedMedia.filename,
}}
/>
</Box>
)}
</div>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={
isLoading || !editCityData[language as keyof CashedCities]?.name
}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Обновить"
)}
</Button>
</div>
<SelectMediaDialog
open={isSelectMediaOpen}
onClose={() => setIsSelectMediaOpen(false)}
onSelectMedia={handleMediaSelect}
/>
</Paper>
);
});

View File

@ -1,8 +1,8 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { cityStore, languageStore } from "@shared";
import { languageStore, cityStore, CashedCities } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
@ -14,7 +14,7 @@ export const CityListPage = observer(() => {
const { language } = languageStore;
useEffect(() => {
getCities();
getCities(language);
}, [language]);
const columns: GridColDef[] = [
@ -33,10 +33,14 @@ export const CityListPage = observer(() => {
headerName: "Действия",
align: "center",
headerAlign: "center",
width: 200,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button onClick={() => navigate(`/city/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/city/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button>
@ -54,7 +58,7 @@ export const CityListPage = observer(() => {
},
];
const rows = cities.map((city) => ({
const rows = cities[language].map((city) => ({
id: city.id,
name: city.name,
country: city.country,
@ -81,7 +85,7 @@ export const CityListPage = observer(() => {
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
deleteCity(rowId);
deleteCity(rowId.toString(), language as keyof CashedCities);
}
setIsDeleteModalOpen(false);
setRowId(null);

View File

@ -1,6 +1,6 @@
import { Paper } from "@mui/material";
import { cityStore, mediaStore } from "@shared";
import { MediaViewer } from "@widgets";
import { cityStore, languageStore, mediaStore } from "@shared";
import { LanguageSwitcher, MediaViewer } from "@widgets";
import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
import { useEffect } from "react";
@ -8,19 +8,30 @@ import { useNavigate, useParams } from "react-router-dom";
export const CityPreviewPage = observer(() => {
const { id } = useParams();
const { getCity, city } = cityStore;
const { getCity, city, setEditCityData } = cityStore;
const { oneMedia, getOneMedia } = mediaStore;
const navigate = useNavigate();
const { language } = languageStore;
useEffect(() => {
(async () => {
const cityResponse = await getCity(id as string);
await getOneMedia(cityResponse.arms as string);
if (id) {
const cityResponse = await getCity(id as string, language);
setEditCityData(
cityResponse.name,
cityResponse.country,
cityResponse.country_code,
cityResponse.arms,
language
);
await getOneMedia(cityResponse.arms as string);
}
})();
}, [id]);
}, [id, language]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
@ -29,36 +40,18 @@ export const CityPreviewPage = observer(() => {
<ArrowLeft size={20} />
Назад
</button>
{/* <div className="flex gap-2">
<Button
variant="contained"
color="primary"
onClick={() => navigate(`/city/${id}/edit`)}
startIcon={<Pencil size={20} />}
>
Редактировать
</Button>
<Button
variant="contained"
color="error"
onClick={() => navigate(`/city/${id}/edit`)}
startIcon={<Trash2 size={20} />}
>
Удалить
</Button>
</div> */}
</div>
<div className="flex flex-col gap-10 w-full">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Название</h1>
<p>{city?.name}</p>
<p>{city[id!]?.[language]?.name}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Страна</h1>
<p>{city?.country}</p>
<p>{city[id!]?.[language]?.country}</p>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 pb-10">
<h1 className="text-lg font-bold">Герб</h1>
<div className="w-[300px] h-[200px]">
<MediaViewer

View File

@ -1,3 +1,4 @@
export * from "./CityListPage";
export * from "./CityPreviewPage";
export * from "./CityCreatePage";
export * from "./CityEditPage";

View File

@ -4,20 +4,20 @@ import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { countryStore } from "@shared";
import { countryStore, languageStore } from "@shared";
import { useState } from "react";
import { LanguageSwitcher } from "@widgets";
export const CountryCreatePage = observer(() => {
const navigate = useNavigate();
const [name, setName] = useState("");
const [code, setCode] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
const { createCountryData, setCountryData, createCountry } = countryStore;
const handleCreate = async () => {
try {
setIsLoading(true);
await countryStore.createCountry(code, name);
await createCountry();
toast.success("Страна успешно создана");
navigate("/country");
} catch (error) {
@ -44,16 +44,24 @@ export const CountryCreatePage = observer(() => {
<TextField
fullWidth
label="Код страны"
value={code}
value={createCountryData.code}
required
onChange={(e) => setCode(e.target.value)}
onChange={(e) =>
setCountryData(
e.target.value,
createCountryData[language].name,
language
)
}
/>
<TextField
fullWidth
label="Название"
value={name}
value={createCountryData[language].name}
required
onChange={(e) => setName(e.target.value)}
onChange={(e) =>
setCountryData(createCountryData.code, e.target.value, language)
}
/>
<Button
@ -61,7 +69,7 @@ export const CountryCreatePage = observer(() => {
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={isLoading || !name || !code}
disabled={isLoading || !createCountryData[language].name}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />

View File

@ -0,0 +1,87 @@
import { Button, Paper, TextField } from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { countryStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { LanguageSwitcher } from "@widgets";
export const CountryEditPage = observer(() => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const { language } = languageStore;
const { id } = useParams();
const { editCountryData, editCountry, getCountry, setEditCountryData } =
countryStore;
const handleEdit = async () => {
try {
setIsLoading(true);
await editCountry(id as string);
toast.success("Страна успешно обновлена");
} catch (error) {
toast.error("Ошибка при обновлении страны");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
(async () => {
if (id) {
const data = await getCountry(id as string, language);
setEditCountryData(data.name, language);
}
})();
}, [id, language]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/country")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
fullWidth
label="Код страны"
value={id as string}
required
disabled
/>
<TextField
fullWidth
label="Название"
value={editCountryData[language].name}
required
onChange={(e) =>
countryStore.setEditCountryData(e.target.value, language)
}
/>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={isLoading || !editCountryData[language].name}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Обновить"
)}
</Button>
</div>
</Paper>
);
});

View File

@ -2,7 +2,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { countryStore, languageStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
@ -14,7 +14,7 @@ export const CountryListPage = observer(() => {
const { language } = languageStore;
useEffect(() => {
getCountries();
getCountries(language);
}, [language]);
const columns: GridColDef[] = [
@ -28,10 +28,15 @@ export const CountryListPage = observer(() => {
headerName: "Действия",
align: "center",
headerAlign: "center",
width: 200,
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button
onClick={() => navigate(`/country/${params.row.code}/edit`)}
>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/country/${params.row.code}`)}>
<Eye size={20} className="text-green-500" />
</button>
@ -49,7 +54,7 @@ export const CountryListPage = observer(() => {
},
];
const rows = countries.map((country) => ({
const rows = countries[language]?.map((country) => ({
id: country.code,
code: country.code,
name: country.name,
@ -66,12 +71,14 @@ export const CountryListPage = observer(() => {
</div>
<DataGrid rows={rows} columns={columns} hideFooter />
</div>
<DeleteModal
open={isDeleteModalOpen}
onDelete={async () => {
if (rowId) {
await countryStore.deleteCountry(rowId);
getCountries(); // Refresh the list after deletion
await countryStore.deleteCountry(rowId, language);
getCountries(language); // Refresh the list after deletion
setIsDeleteModalOpen(false);
}
setIsDeleteModalOpen(false);
setRowId(null);

View File

@ -1,23 +1,29 @@
import { Paper } from "@mui/material";
import { countryStore } from "@shared";
import { countryStore, languageStore } from "@shared";
import { observer } from "mobx-react-lite";
import { ArrowLeft } from "lucide-react";
import { useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { LanguageSwitcher } from "@widgets";
export const CountryPreviewPage = observer(() => {
const { id } = useParams();
const { getCountry, country } = countryStore;
const { getCountry, country, setEditCountryData } = countryStore;
const navigate = useNavigate();
const { language } = languageStore;
useEffect(() => {
(async () => {
await getCountry(id as string);
if (id) {
const data = await getCountry(id as string, language);
setEditCountryData(data.name, language);
}
})();
}, [id]);
}, [id, language]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex justify-between items-center">
<button
className="flex items-center gap-2"
@ -45,11 +51,11 @@ export const CountryPreviewPage = observer(() => {
</Button>
</div> */}
</div>
{country && (
{country[id!]?.[language] && (
<div className="flex flex-col gap-10 w-full">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Название</h1>
<p>{country?.name}</p>
<p>{country[id!]?.[language]?.name}</p>
</div>
</div>
)}

View File

@ -1,3 +1,4 @@
export * from "./CountryListPage";
export * from "./CountryPreviewPage";
export * from "./CountryCreatePage";
export * from "./CountryEditPage";

View File

@ -0,0 +1,129 @@
import {
Button,
Paper,
TextField,
Checkbox,
FormControlLabel,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { userStore } from "@shared";
import { useState } from "react";
export const UserCreatePage = observer(() => {
const navigate = useNavigate();
const { createUserData, setCreateUserData, createUser } = userStore;
const [isLoading, setIsLoading] = useState(false);
const handleCreate = async () => {
try {
setIsLoading(true);
await createUser();
toast.success("Пользователь успешно создан");
navigate("/user");
} catch (error) {
toast.error("Ошибка при создании пользователя");
} finally {
setIsLoading(false);
}
};
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/user")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
fullWidth
label="Имя"
value={createUserData.name || ""}
required
onChange={(e) =>
setCreateUserData(
e.target.value,
createUserData.email || "",
createUserData.password || "",
createUserData.is_admin || false
)
}
/>
<TextField
fullWidth
label="Email"
value={createUserData.email || ""}
required
onChange={(e) =>
setCreateUserData(
createUserData.name || "",
e.target.value,
createUserData.password || "",
createUserData.is_admin || false
)
}
/>
<TextField
fullWidth
label="Пароль"
value={createUserData.password || ""}
required
onChange={(e) =>
setCreateUserData(
createUserData.name || "",
createUserData.email || "",
e.target.value,
createUserData.is_admin || false
)
}
/>
<div className="w-full flex flex-col items-start">
<FormControlLabel
control={
<Checkbox
checked={createUserData.is_admin || false}
onChange={(e) => {
setCreateUserData(
createUserData.name || "",
createUserData.email || "",
createUserData.password || "",
e.target.checked
);
}}
/>
}
label="Администратор"
/>
</div>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleCreate}
disabled={
isLoading || !createUserData.name || !createUserData.password
}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Создать"
)}
</Button>
</div>
</Paper>
);
});

View File

@ -0,0 +1,139 @@
import {
Button,
FormControlLabel,
Checkbox,
Paper,
TextField,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Save } from "lucide-react";
import { Loader2 } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "react-toastify";
import { userStore } from "@shared";
import { useEffect, useState } from "react";
export const UserEditPage = observer(() => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const { id } = useParams();
const { editUserData, editUser, getUser, setEditUserData, user } = userStore;
const handleEdit = async () => {
try {
setIsLoading(true);
await editUser(Number(id));
toast.success("Пользователь успешно обновлен");
navigate("/user");
} catch (error) {
toast.error("Ошибка при обновлении пользователя");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
(async () => {
if (id) {
const data = await getUser(Number(id));
setEditUserData(
data?.name || "",
data?.email || "",
data?.password || "",
data?.is_admin || false
);
}
})();
}, [id]);
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/user")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-start">
<TextField
fullWidth
label="Имя"
value={editUserData.name || ""}
required
onChange={(e) =>
setEditUserData(
e.target.value,
editUserData.email || "",
editUserData.password || "",
editUserData.is_admin || false
)
}
/>
<TextField
fullWidth
label="Email"
value={editUserData.email || ""}
required
onChange={(e) =>
setEditUserData(
editUserData.name || "",
e.target.value,
editUserData.password || "",
editUserData.is_admin || false
)
}
/>
<TextField
fullWidth
label="Пароль"
value={editUserData.password || ""}
required
onChange={(e) =>
setEditUserData(
editUserData.name || "",
editUserData.email || "",
e.target.value,
editUserData.is_admin || false
)
}
/>
<FormControlLabel
control={
<Checkbox
checked={editUserData.is_admin || false}
onChange={(e) =>
setEditUserData(
editUserData.name || "",
editUserData.email || "",
editUserData.password || "",
e.target.checked
)
}
/>
}
label="Администратор"
/>
<Button
variant="contained"
className="w-min flex gap-2 items-center self-end"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={isLoading || !editUserData.name || !editUserData.email}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Обновить"
)}
</Button>
</div>
</Paper>
);
});

View File

@ -1,21 +1,21 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { languageStore, userStore } from "@shared";
import { userStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { DeleteModal, LanguageSwitcher } from "@widgets";
import { CreateButton, DeleteModal } from "@widgets";
export const UserListPage = observer(() => {
const { users, getUsers, deleteUser } = userStore;
const navigate = useNavigate();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [rowId, setRowId] = useState<number | null>(null); // Lifted state
const { language } = languageStore;
useEffect(() => {
getUsers();
}, [language]);
}, []);
const columns: GridColDef[] = [
{
@ -56,6 +56,15 @@ export const UserListPage = observer(() => {
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button>
<Pencil
size={20}
className="text-blue-500"
onClick={() => {
navigate(`/user/${params.row.id}/edit`);
}}
/>
</button>
<button
onClick={() => {
setIsDeleteModalOpen(true);
@ -79,9 +88,11 @@ export const UserListPage = observer(() => {
return (
<>
<LanguageSwitcher />
<div style={{ width: "100%" }}>
<div className="w-full">
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Пользователи</h1>
<CreateButton label="Создать пользователя" path="/user/create" />
</div>
<DataGrid
rows={rows}
columns={columns}
@ -96,6 +107,7 @@ export const UserListPage = observer(() => {
if (rowId) {
await deleteUser(rowId);
}
setIsDeleteModalOpen(false);
setRowId(null);
}}

View File

@ -1 +1,3 @@
export * from "./UserListPage";
export * from "./UserCreatePage";
export * from "./UserEditPage";

View File

@ -14,7 +14,6 @@ import { Loader2 } from "lucide-react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { LanguageSwitcher } from "@widgets";
export const VehicleCreatePage = observer(() => {
const navigate = useNavigate();
@ -46,7 +45,6 @@ export const VehicleCreatePage = observer(() => {
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<LanguageSwitcher />
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"

View File

@ -0,0 +1,140 @@
import {
Paper,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Button,
} from "@mui/material";
import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { carrierStore, VEHICLE_TYPES, vehicleStore } from "@shared";
import { toast } from "react-toastify";
export const VehicleEditPage = observer(() => {
const navigate = useNavigate();
const { id } = useParams();
const {
getVehicle,
vehicle,
editVehicleData,
setEditVehicleData,
editVehicle,
} = vehicleStore;
const { getCarriers } = carrierStore;
useEffect(() => {
(async () => {
await getVehicle(Number(id));
await getCarriers();
setEditVehicleData({
tail_number: vehicle[Number(id)]?.vehicle.tail_number,
type: vehicle[Number(id)]?.vehicle.type,
carrier: vehicle[Number(id)]?.vehicle.carrier,
carrier_id: vehicle[Number(id)]?.vehicle.carrier_id,
});
})();
}, [id]);
const [isLoading, setIsLoading] = useState(false);
const handleEdit = async () => {
try {
setIsLoading(true);
await editVehicle(Number(id), editVehicleData);
toast.success("Транспортное средство успешно обновлено");
navigate("/vehicle");
} catch (error) {
toast.error("Ошибка при обновлении транспортного средства");
} finally {
setIsLoading(false);
}
};
return (
<Paper className="w-full h-full p-3 flex flex-col gap-10">
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2"
onClick={() => navigate("/vehicle")}
>
<ArrowLeft size={20} />
Назад
</button>
</div>
<div className="flex flex-col gap-10 w-full items-end">
<TextField
fullWidth
label="Бортовой номер"
value={editVehicleData.tail_number}
required
onChange={(e) =>
setEditVehicleData({
...editVehicleData,
tail_number: Number(e.target.value),
})
}
/>
<FormControl fullWidth>
<InputLabel>Тип</InputLabel>
<Select
value={editVehicleData.type}
label="Тип"
required
onChange={(e) =>
setEditVehicleData({ ...editVehicleData, type: e.target.value })
}
>
{VEHICLE_TYPES.map((type) => (
<MenuItem key={type.value} value={type.value}>
{type.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Перевозчик</InputLabel>
<Select
value={editVehicleData.carrier_id}
label="Перевозчик"
required
onChange={(e) =>
setEditVehicleData({
...editVehicleData,
carrier_id: e.target.value as number,
})
}
>
{carrierStore.carriers.map((carrier) => (
<MenuItem key={carrier.id} value={carrier.id}>
{carrier.full_name}
</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />}
onClick={handleEdit}
disabled={
isLoading ||
!editVehicleData.tail_number ||
!editVehicleData.type ||
!editVehicleData.carrier_id
}
>
{isLoading ? (
<Loader2 size={20} className="animate-spin" />
) : (
"Сохранить"
)}
</Button>
</div>
</Paper>
);
});

View File

@ -2,9 +2,9 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { carrierStore, languageStore, vehicleStore } from "@shared";
import { useEffect, useState } from "react";
import { observer } from "mobx-react-lite";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Pencil, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { CreateButton, DeleteModal, LanguageSwitcher } from "@widgets";
import { CreateButton, DeleteModal } from "@widgets";
import { VEHICLE_TYPES } from "@shared";
export const VehicleListPage = observer(() => {
@ -68,6 +68,9 @@ export const VehicleListPage = observer(() => {
renderCell: (params: GridRenderCellParams) => {
return (
<div className="flex h-full gap-7 justify-center items-center">
<button onClick={() => navigate(`/vehicle/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" />
</button>
<button onClick={() => navigate(`/vehicle/${params.row.id}`)}>
<Eye size={20} className="text-green-500" />
</button>
@ -96,8 +99,6 @@ export const VehicleListPage = observer(() => {
return (
<>
<LanguageSwitcher />
<div style={{ width: "100%" }}>
<div className="flex justify-between items-center mb-10">
<h1 className="text-2xl">Транспортные средства</h1>

View File

@ -45,23 +45,23 @@ export const VehiclePreviewPage = observer(() => {
</Button>
</div> */}
</div>
{vehicle && (
{vehicle[id!] && (
<div className="flex flex-col gap-10 w-full">
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Системный номер</h1>
<p>{vehicle?.vehicle.tail_number}</p>
<p>{vehicle[id!]?.vehicle.tail_number}</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Тип транспортного средства</h1>
<p>
{VEHICLE_TYPES.find(
(type) => type.value === vehicle?.vehicle.type
)?.label || vehicle?.vehicle.type}
(type) => type.value === vehicle[id!]?.vehicle.type
)?.label || vehicle[id!]?.vehicle.type}
</p>
</div>
<div className="flex flex-col gap-2">
<h1 className="text-lg font-bold">Перевозчик</h1>
<p>{vehicle?.vehicle.carrier}</p>
<p>{vehicle[id!]?.vehicle.carrier}</p>
</div>
</div>
)}

View File

@ -1,3 +1,4 @@
export * from "./VehicleListPage";
export * from "./VehiclePreviewPage";
export * from "./VehicleCreatePage";
export * from "./VehicleEditPage";

View File

@ -3,7 +3,6 @@ import {
Power,
LucideIcon,
Building2,
MonitorSmartphone,
Map,
Users,
Earth,
@ -11,6 +10,14 @@ import {
BusFront,
Bus,
GitBranch,
Car,
Train,
Ship,
Table,
Split,
Newspaper,
PersonStanding,
Cpu,
} from "lucide-react";
export const DRAWER_WIDTH = 300;
@ -20,6 +27,7 @@ interface NavigationItem {
icon: LucideIcon;
path?: string;
onClick?: () => void;
nestedItems?: NavigationItem[];
}
export const NAVIGATION_ITEMS: {
@ -45,30 +53,7 @@ export const NAVIGATION_ITEMS: {
icon: BusFront,
path: "/carrier",
},
// {
// id: "media",
// label: "Медиа",
// icon: BookImage,
// path: "/media",
// },
// {
// id: "articles",
// label: "Статьи",
// icon: Newspaper,
// path: "/article",
// },
{
id: "attractions",
label: "Достопримечательности",
icon: Landmark,
path: "/sight",
},
// {
// id: "stations",
// label: "Остановки",
// icon: PersonStanding,
// path: "/station",
// },
{
id: "snapshots",
label: "Снапшоты",
@ -84,33 +69,59 @@ export const NAVIGATION_ITEMS: {
{
id: "devices",
label: "Устройства",
icon: MonitorSmartphone,
icon: Cpu,
path: "/devices",
},
{
id: "all",
label: "Все сущности",
icon: Table,
nestedItems: [
// {
// id: "media",
// label: "Медиа",
// icon: BookImage,
// path: "/media",
// },
// {
// id: "articles",
// label: "Статьи",
// icon: Newspaper,
// path: "/article",
// },
{
id: "attractions",
label: "Достопримечательности",
icon: Landmark,
path: "/sight",
},
{
id: "stations",
label: "Остановки",
icon: PersonStanding,
path: "/station",
},
{
id: "routes",
label: "Маршруты",
icon: Split,
path: "/route",
},
],
},
{
id: "vehicles",
label: "Транспорт",
icon: Bus,
icon: Car,
path: "/vehicle",
},
// {
// id: "routes",
// label: "Маршруты",
// icon: Split,
// path: "/route",
// },
{
id: "users",
label: "Пользователи",
icon: Users,
path: "/user",
},
// {
// id: "articles",
// label: "Статьи",
// icon: Newspaper,
// path: "/articles",
// },
],
secondary: [
{

View File

@ -1,4 +1,4 @@
import { authInstance } from "@shared";
import { authInstance, languageStore } from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type Carrier = {
@ -14,14 +14,19 @@ export type Carrier = {
right_color: string;
};
type Carriers = Carrier[];
type CashedCarrier = Record<number, Carrier>;
class CarrierStore {
carriers: Carrier[] = [];
carrier: Carrier | null = null;
carriers: Carriers = [];
carrier: CashedCarrier = {};
constructor() {
makeAutoObservable(this);
}
getCarriers = async () => {
if (this.carriers.length > 0) return;
const response = await authInstance.get("/carrier");
runInAction(() => {
@ -34,13 +39,30 @@ class CarrierStore {
runInAction(() => {
this.carriers = this.carriers.filter((carrier) => carrier.id !== id);
delete this.carrier[id];
});
};
getCarrier = async (id: number) => {
if (this.carrier[id]) return this.carrier[id];
const response = await authInstance.get(`/carrier/${id}`);
runInAction(() => {
this.carrier = response.data;
if (!this.carrier[id]) {
this.carrier[id] = {
id: 0,
short_name: "",
full_name: "",
slogan: "",
city: "",
city_id: 0,
logo: "",
main_color: "",
left_color: "",
right_color: "",
};
}
this.carrier[id] = response.data;
});
return response.data;
};
@ -50,9 +72,9 @@ class CarrierStore {
shortName: string,
city: string,
cityId: number,
primaryColor: string,
secondaryColor: string,
accentColor: string,
main_color: string,
left_color: string,
right_color: string,
slogan: string,
logoId: string
) => {
@ -61,9 +83,9 @@ class CarrierStore {
short_name: shortName,
city,
city_id: cityId,
primary_color: primaryColor,
secondary_color: secondaryColor,
accent_color: accentColor,
main_color,
left_color,
right_color,
slogan,
logo: logoId,
});
@ -71,6 +93,57 @@ class CarrierStore {
this.carriers.push(response.data);
});
};
editCarrierData = {
full_name: "",
short_name: "",
city: "",
city_id: 0,
main_color: "",
left_color: "",
right_color: "",
slogan: "",
logo: "",
};
setEditCarrierData = (
fullName: string,
shortName: string,
city: string,
cityId: number,
main_color: string,
left_color: string,
right_color: string,
slogan: string,
logoId: string
) => {
this.editCarrierData = {
full_name: fullName,
short_name: shortName,
city,
city_id: cityId,
main_color: main_color,
left_color: left_color,
right_color: right_color,
slogan: slogan,
logo: logoId,
};
};
editCarrier = async (id: number) => {
const response = await authInstance.patch(
`/carrier/${id}`,
this.editCarrierData
);
runInAction(() => {
this.carriers = this.carriers.map((carrier) =>
carrier.id === id ? { ...carrier, ...response.data } : carrier
);
this.carrier[id] = response.data;
});
};
}
export const carrierStore = new CarrierStore();

View File

@ -70,4 +70,4 @@ class CityStore {
};
}
export const cityStore = new CityStore();
// export const cityStore = new CityStore();

View File

@ -0,0 +1,266 @@
import {
authInstance,
languageInstance,
Language,
languageStore,
countryStore,
} from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type City = {
id?: number;
name: string;
country: string;
country_code: string;
arms: string;
};
export type CashedCities = {
ru: City[];
en: City[];
zh: City[];
};
export type CashedCity = {
ru: City | null;
en: City | null;
zh: City | null;
};
class CityStore {
cities: CashedCities = {
ru: [],
en: [],
zh: [],
};
city: Record<string, CashedCity> = {};
constructor() {
makeAutoObservable(this);
}
getCities = async (language: keyof CashedCities) => {
if (this.cities[language] && this.cities[language].length > 0) {
return;
}
const response = await authInstance.get(`/city`);
runInAction(() => {
this.cities[language] = response.data;
});
};
getCity = async (code: string, language: keyof CashedCities) => {
if (this.city[code]?.[language] && this.city[code][language] !== null) {
return;
}
const response = await authInstance.get(`/city/${code}`);
runInAction(() => {
if (!this.city[code]) {
this.city[code] = {
ru: null,
en: null,
zh: null,
};
}
this.city[code][language] = response.data;
});
return response.data;
};
deleteCity = async (code: string, language: keyof CashedCities) => {
await authInstance.delete(`/city/${code}`);
runInAction(() => {
this.cities[language] = this.cities[language].filter(
(city) => city.country_code !== code
);
this.city[code][language] = null;
});
};
createCityData = {
country: "",
country_code: "",
arms: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
setCreateCityData = (
name: string,
country: string,
country_code: string,
arms: string,
language: keyof CashedCities
) => {
this.createCityData = {
...this.createCityData,
country: country,
country_code: country_code,
arms: arms,
[language]: {
name: name,
},
};
};
createCity = async () => {
const { language } = languageStore;
const { country, country_code, arms } = this.createCityData;
const { name } = this.createCityData[language as keyof CashedCities];
if (name && country && country_code && arms) {
const cityResponse = await languageInstance(language as Language).post(
"/city",
{
name: name,
country: country,
country_code: country_code,
arms: arms,
}
);
runInAction(() => {
this.cities[language as keyof CashedCities] = [
...this.cities[language as keyof CashedCities],
cityResponse.data,
];
});
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
(l) => l !== language
)) {
const { name } =
this.createCityData[secondaryLanguage as keyof CashedCities];
const patchResponse = await languageInstance(
secondaryLanguage as Language
).patch(`/city/${cityResponse.data.id}`, {
name: name,
country: country,
country_code: country_code,
arms: arms,
});
runInAction(() => {
this.cities[secondaryLanguage as keyof CashedCities] = [
...this.cities[secondaryLanguage as keyof CashedCities],
patchResponse.data,
];
});
}
}
runInAction(() => {
this.createCityData = {
country: "",
country_code: "",
arms: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
});
};
editCityData = {
country: "",
country_code: "",
arms: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
setEditCityData = (
name: string,
country: string,
country_code: string,
arms: string,
language: keyof CashedCities
) => {
this.editCityData = {
...this.editCityData,
country: country,
country_code: country_code,
arms: arms,
[language]: {
name: name,
},
};
};
editCity = async (code: string) => {
for (const language of ["ru", "en", "zh"]) {
const { country_code, arms } = this.editCityData;
const { name } = this.editCityData[language as keyof CashedCities];
const { countries } = countryStore;
const country = countries[language as keyof CashedCities].find(
(country) => country.code === country_code
);
await languageInstance(language as Language).patch(`/city/${code}`, {
name,
country: country?.name || "",
country_code: country_code,
arms,
});
runInAction(() => {
if (this.city[code]) {
this.city[code][language as keyof CashedCities] = {
name,
country: country?.name || "",
country_code: country_code,
arms,
};
}
if (this.cities[language as keyof CashedCities]) {
this.cities[language as keyof CashedCities] = this.cities[
language as keyof CashedCities
].map((city) =>
city.id === Number(code)
? {
id: city.id,
name,
country: country?.name || "",
country_code: country_code,
arms,
}
: city
);
}
});
}
};
}
export const cityStore = new CityStore();

View File

@ -1,4 +1,9 @@
import { authInstance } from "@shared";
import {
authInstance,
languageInstance,
Language,
languageStore,
} from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type Country = {
@ -6,43 +11,208 @@ export type Country = {
name: string;
};
export type CashedCountries = {
ru: Country[];
en: Country[];
zh: Country[];
};
export type CashedCountry = {
ru: Country | null;
en: Country | null;
zh: Country | null;
};
class CountryStore {
countries: Country[] = [];
country: Country | null = null;
countries: CashedCountries = {
ru: [],
en: [],
zh: [],
};
country: Record<string, CashedCountry> = {};
constructor() {
makeAutoObservable(this);
}
getCountries = async () => {
const response = await authInstance.get("/country");
getCountries = async (language: keyof CashedCountries) => {
if (this.countries[language] && this.countries[language].length > 0) {
return;
}
const response = await authInstance.get(`/country`);
runInAction(() => {
this.countries = response.data;
this.countries[language] = response.data;
});
};
getCountry = async (code: string) => {
getCountry = async (code: string, language: keyof CashedCountries) => {
if (
this.country[code]?.[language] &&
this.country[code][language] !== null
) {
return;
}
const response = await authInstance.get(`/country/${code}`);
runInAction(() => {
this.country = response.data;
if (!this.country[code]) {
this.country[code] = {
ru: null,
en: null,
zh: null,
};
}
this.country[code][language] = response.data;
});
return response.data;
};
deleteCountry = async (code: string) => {
deleteCountry = async (code: string, language: keyof CashedCountries) => {
await authInstance.delete(`/country/${code}`);
runInAction(() => {
this.countries = this.countries.filter(
this.countries[language] = this.countries[language].filter(
(country) => country.code !== code
);
this.country[code][language] = null;
});
};
createCountry = async (code: string, name: string) => {
await authInstance.post("/country", { code: code, name: name });
await this.getCountries();
createCountryData = {
code: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
setCountryData = (
code: string,
name: string,
language: keyof CashedCountries
) => {
this.createCountryData = {
...this.createCountryData,
code: code,
[language]: {
name: name,
},
};
};
createCountry = async () => {
const { code } = this.createCountryData;
const { language } = languageStore;
const { name } = this.createCountryData[language as keyof CashedCountries];
if (code && this.createCountryData[language].name) {
await languageInstance(language as Language).post("/country", {
code: code,
name: name,
});
runInAction(() => {
this.countries[language as keyof CashedCountries] = [
...this.countries[language as keyof CashedCountries],
{ code: code, name: name },
];
});
for (const secondaryLanguage of ["ru", "en", "zh"].filter(
(l) => l !== language
)) {
const { name } =
this.createCountryData[secondaryLanguage as keyof CashedCountries];
if (name) {
await languageInstance(secondaryLanguage as Language).patch(
`/country/${code}`,
{
name: name,
}
);
}
runInAction(() => {
this.countries[secondaryLanguage as keyof CashedCountries] = [
...this.countries[secondaryLanguage as keyof CashedCountries],
{ code: code, name: name },
];
});
}
}
runInAction(() => {
this.createCountryData = {
code: "",
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
});
};
editCountryData = {
ru: {
name: "",
},
en: {
name: "",
},
zh: {
name: "",
},
};
setEditCountryData = (name: string, language: keyof CashedCountries) => {
this.editCountryData = {
...this.editCountryData,
[language]: {
name: name,
},
};
};
editCountry = async (code: string) => {
for (const language of ["ru", "en", "zh"]) {
const { name } = this.editCountryData[language as keyof CashedCountries];
if (name) {
await languageInstance(language as Language).patch(`/country/${code}`, {
name: name,
});
runInAction(() => {
if (this.country[code]) {
this.country[code][language as keyof CashedCountries] = {
code,
name,
};
}
if (this.countries[language as keyof CashedCountries]) {
this.countries[language as keyof CashedCountries] = this.countries[
language as keyof CashedCountries
].map((country) =>
country.code === code ? { code, name } : country
);
}
});
}
}
};
}

View File

@ -6,17 +6,20 @@ export type User = {
email: string;
is_admin: boolean;
name: string;
password?: string;
};
class UserStore {
users: User[] = [];
user: User | null = null;
user: Record<string, User> = {};
constructor() {
makeAutoObservable(this);
}
getUsers = async () => {
if (this.users.length > 0) return;
const response = await authInstance.get("/user");
runInAction(() => {
@ -25,18 +28,77 @@ class UserStore {
};
getUser = async (id: number) => {
if (this.user[id]) return;
const response = await authInstance.get(`/user/${id}`);
runInAction(() => {
this.user = response.data as User;
this.user[id] = response.data as User;
});
return response.data;
};
deleteUser = async (id: number) => {
await authInstance.delete(`/users/${id}`);
await authInstance.delete(`/user/${id}`);
runInAction(() => {
this.users = this.users.filter((user) => user.id !== id);
delete this.user[id];
});
};
createUserData: Partial<User> = {
name: "",
email: "",
password: "",
is_admin: false,
};
setCreateUserData = (
name: string,
email: string,
password: string,
is_admin: boolean
) => {
this.createUserData = { name, email, password, is_admin };
};
createUser = async () => {
const id = this.users[this.users.length - 1].id + 1;
const response = await authInstance.post("/user", this.createUserData);
runInAction(() => {
this.users.push({
id,
...response.data,
});
});
};
editUserData: Partial<User> = {
name: "",
email: "",
password: "",
is_admin: false,
};
setEditUserData = (
name: string,
email: string,
password: string,
is_admin: boolean
) => {
this.editUserData = { name, email, password, is_admin };
};
editUser = async (id: number) => {
const response = await authInstance.patch(`/user/${id}`, this.editUserData);
runInAction(() => {
this.users = this.users.map((user) =>
user.id === id ? { ...user, ...response.data } : user
);
this.user[id] = { ...this.user[id], ...response.data };
});
};
}

View File

@ -1,5 +1,5 @@
import { authInstance } from "@shared";
import { makeAutoObservable } from "mobx";
import { authInstance, languageStore, languageInstance } from "@shared";
import { makeAutoObservable, runInAction } from "mobx";
export type Vehicle = {
vehicle: {
@ -22,42 +22,121 @@ export type Vehicle = {
class VehicleStore {
vehicles: Vehicle[] = [];
vehicle: Vehicle | null = null;
vehicle: Record<string, Vehicle> = {};
constructor() {
makeAutoObservable(this);
}
getVehicles = async () => {
const response = await authInstance.get(`/vehicle`);
this.vehicles = response.data;
const response = await languageInstance("ru").get(`/vehicle`);
runInAction(() => {
this.vehicles = response.data;
});
};
deleteVehicle = async (id: number) => {
await authInstance.delete(`/vehicle/${id}`);
this.vehicles = this.vehicles.filter(
(vehicle) => vehicle.vehicle.id !== id
);
await languageInstance("ru").delete(`/vehicle/${id}`);
runInAction(() => {
this.vehicles = this.vehicles.filter(
(vehicle) => vehicle.vehicle.id !== id
);
});
};
getVehicle = async (id: number) => {
const response = await authInstance.get(`/vehicle/${id}`);
this.vehicle = response.data;
const response = await languageInstance("ru").get(`/vehicle/${id}`);
runInAction(() => {
this.vehicle[id] = response.data;
});
};
createVehicle = async (
tailNumber: number,
type: string,
type: number,
carrier: string,
carrierId: number
) => {
await authInstance.post("/vehicle", {
const response = await languageInstance("ru").post("/vehicle", {
tail_number: tailNumber,
type,
carrier,
carrier_id: carrierId,
});
await this.getVehicles();
runInAction(() => {
this.vehicles.push({
vehicle: {
id: response.data.id,
tail_number: response.data.tail_number,
type: response.data.type,
carrier_id: response.data.carrier_id,
carrier: response.data.carrier,
uuid: response.data.uuid,
},
});
});
};
editVehicleData: {
tail_number: number;
type: number;
carrier: string;
carrier_id: number;
} = {
tail_number: 0,
type: 0,
carrier: "",
carrier_id: 0,
};
setEditVehicleData = (data: {
tail_number: number;
type: number;
carrier: string;
carrier_id: number;
}) => {
this.editVehicleData = {
...this.editVehicleData,
...data,
};
};
editVehicle = async (
id: number,
data: {
tail_number: number;
type: number;
carrier: string;
carrier_id: number;
}
) => {
const response = await languageInstance("ru").patch(`/vehicle/${id}`, {
tail_number: data.tail_number,
type: data.type,
carrier: data.carrier,
carrier_id: data.carrier_id,
});
runInAction(() => {
this.vehicle[id] = {
vehicle: {
...this.vehicle[id].vehicle,
...response.data,
},
};
this.vehicles = this.vehicles.map((vehicle) =>
vehicle.vehicle.id === id
? {
...vehicle,
...response.data,
}
: vehicle
);
});
};
}