fuel; nest api
This commit is contained in:
@ -17,7 +17,7 @@ const ObjectTree = ({
|
||||
|
||||
const { data: existingObjectsList } = useSWR(
|
||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
(url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
if (Array.isArray(res)) {
|
||||
let count = 0
|
||||
res.forEach(el => {
|
||||
@ -34,7 +34,7 @@ const ObjectTree = ({
|
||||
|
||||
const { data: planningObjectsList } = useSWR(
|
||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
(url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
if (Array.isArray(res)) {
|
||||
let count = 0
|
||||
res.forEach(el => {
|
||||
@ -116,7 +116,7 @@ const ObjectList = ({
|
||||
|
||||
const { data } = useSWR(
|
||||
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false
|
||||
|
||||
@ -78,20 +78,20 @@ const MapComponent = ({
|
||||
const mapElement = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
// Get type roles
|
||||
useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
useSWR(`/gis/type-roles`, (url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
if (Array.isArray(res)) {
|
||||
setTypeRoles(id, res)
|
||||
}
|
||||
return res
|
||||
}), swrOptions)
|
||||
|
||||
const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
const { data: regionsData } = useSWR(`/general/regions`, (url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
setRegionsData(res)
|
||||
return res
|
||||
}), swrOptions)
|
||||
|
||||
// Bounds: region
|
||||
const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.ems), swrOptions)
|
||||
const { data: regionBoundsData } = useSWR(`/gis/bounds/region`, (url) => fetcher(url, BASE_URL.nest), swrOptions)
|
||||
|
||||
// Map init
|
||||
useEffect(() => {
|
||||
@ -169,7 +169,7 @@ const MapComponent = ({
|
||||
useEffect(() => {
|
||||
if (selectedDistrict && selectedYear && districtBoundLayer) {
|
||||
const bounds = new VectorSource({
|
||||
url: `${BASE_URL.ems}/gis/bounds/city/${selectedDistrict}`,
|
||||
url: `${BASE_URL.nest}/gis/bounds/city/${selectedDistrict}`,
|
||||
format: new GeoJSON(),
|
||||
})
|
||||
|
||||
@ -279,7 +279,7 @@ const MapComponent = ({
|
||||
}
|
||||
}, [currentObjectId, figuresLayer, linesLayer])
|
||||
|
||||
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
setDistrictsData(res)
|
||||
return res
|
||||
}), swrOptions)
|
||||
@ -287,7 +287,7 @@ const MapComponent = ({
|
||||
const { data: figuresData, isValidating: figuresValidating } = useSWR(
|
||||
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
||||
(url) => axios.get(url, {
|
||||
baseURL: BASE_URL.ems
|
||||
baseURL: BASE_URL.nest
|
||||
}).then((res) => res.data),
|
||||
swrOptions
|
||||
)
|
||||
@ -295,7 +295,7 @@ const MapComponent = ({
|
||||
const { data: linesData, isValidating: linesValidating } = useSWR(
|
||||
!figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
||||
(url) => axios.get(url, {
|
||||
baseURL: BASE_URL.ems
|
||||
baseURL: BASE_URL.nest
|
||||
}).then((res) => res.data),
|
||||
swrOptions
|
||||
)
|
||||
@ -303,7 +303,7 @@ const MapComponent = ({
|
||||
const { data: districtData } = useSWR(
|
||||
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
|
||||
(url) => axios.get(url, {
|
||||
baseURL: BASE_URL.ems
|
||||
baseURL: BASE_URL.nest
|
||||
}).then((res) => Array.isArray(res.data) ? res.data[0] : null),
|
||||
swrOptions
|
||||
)
|
||||
@ -398,7 +398,7 @@ const MapComponent = ({
|
||||
list.push(district.id as Number)
|
||||
})
|
||||
|
||||
fetch(`${BASE_URL.ems}/gis/bounds/city`, {
|
||||
fetch(`${BASE_URL.nest}/gis/bounds/city`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@ -15,7 +15,7 @@ const MapLegend = ({
|
||||
|
||||
const { data: existingObjectsList } = useSWR(
|
||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
{
|
||||
revalidateOnFocus: false
|
||||
}
|
||||
@ -23,7 +23,7 @@ const MapLegend = ({
|
||||
|
||||
const { data: planningObjectsList } = useSWR(
|
||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
{
|
||||
revalidateOnFocus: false
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ const MapObjectSearch = ({
|
||||
|
||||
const { data: searchData } = useSWR(
|
||||
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
swrOptions
|
||||
)
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ const MapRegionSelect = ({
|
||||
const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
|
||||
const { regionsData } = useRegionsStore()
|
||||
|
||||
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
const { data: districtsData } = useSWR(selectedRegion ? `/general/districts/?region_id=${selectedRegion}` : null, (url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
setDistrictsData(res)
|
||||
return res
|
||||
}), swrOptions)
|
||||
|
||||
@ -6,7 +6,7 @@ import { BASE_URL } from '../../constants'
|
||||
const ObjectData = (object_data: IObjectData) => {
|
||||
const { data: typeData } = useSWR(
|
||||
object_data.type ? `/general/types` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
{
|
||||
revalidateOnFocus: false
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ const ObjectParameters = ({
|
||||
|
||||
const { data: valuesData, isValidating: valuesValidating } = useSWR(
|
||||
currentObjectId ? `/general/values/?object_id=${currentObjectId}` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems),
|
||||
(url) => fetcher(url, BASE_URL.nest),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false
|
||||
|
||||
@ -3,7 +3,7 @@ import { BASE_URL } from '../../constants'
|
||||
import { fetcher } from '../../http/axiosInstance'
|
||||
|
||||
const RegionSelect = () => {
|
||||
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.ems), {
|
||||
const { data } = useSWR(`/gis/regions/borders`, (url) => fetcher(url, BASE_URL.nest), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false
|
||||
})
|
||||
|
||||
@ -30,7 +30,7 @@ const TableValue = ({
|
||||
|
||||
const { data: tcbAll, isValidating } = useSWR(
|
||||
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
|
||||
(url) => fetcher(url, BASE_URL.ems).then(res => {
|
||||
(url) => fetcher(url, BASE_URL.nest).then(res => {
|
||||
if (Array.isArray(res)) {
|
||||
return res.map((el) => ({
|
||||
label: el.name || "",
|
||||
|
||||
@ -9,4 +9,5 @@ export const BASE_URL = {
|
||||
fuel: import.meta.env.VITE_API_FUEL_URL,
|
||||
servers: import.meta.env.VITE_API_SERVERS_URL,
|
||||
ems: import.meta.env.VITE_API_EMS_URL,
|
||||
nest: import.meta.env.VITE_API_NEST_URL
|
||||
}
|
||||
@ -3,6 +3,11 @@ export interface IRegion {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ISeason {
|
||||
name: string
|
||||
year: number
|
||||
}
|
||||
|
||||
export interface ICity {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@ -1,49 +1,117 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Combobox, CompoundButton, Option, Text } from '@fluentui/react-components'
|
||||
import { Dropdown, Option, Spinner } from '@fluentui/react-components'
|
||||
import useSWR from 'swr'
|
||||
import axios from 'axios'
|
||||
import { AgGridReact, CustomCellRendererProps } from 'ag-grid-react'
|
||||
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
|
||||
import FuelRenderer from './FuelRenderer'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { ICity, IRegion } from '../../../interfaces/fuel'
|
||||
import BoilersCard from './BoilersCard'
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule])
|
||||
|
||||
function Boilers() {
|
||||
const [, setBoilersPage] = useState(1)
|
||||
const [boilerSearch, setBoilerSearch] = useState("")
|
||||
const [, setDebouncedBoilerSearch] = useState("")
|
||||
// const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const { data: regions } = useSWR('/general/regions', () => axios.get('/general/regions', {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
||||
}).then(res => res.data))
|
||||
const [regionId, setRegionId] = useState<number | undefined>(
|
||||
searchParams.get("region_id") ? Number(searchParams.get("region_id")) : undefined
|
||||
);
|
||||
|
||||
const [regionId, setRegionId] = useState<number | undefined>(undefined)
|
||||
const [cityId, setCityId] = useState<number | undefined>(
|
||||
searchParams.get("city_id") ? Number(searchParams.get("city_id")) : undefined
|
||||
)
|
||||
|
||||
const { data: cities } = useSWR(regionId ? `/general/cities?region_id=${regionId}` : null, () => axios.get(`/general/cities?region_id=${regionId}`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
||||
}).then(res => res.data))
|
||||
// Load regions
|
||||
const { data: regions, isLoading: regionsLoading } = useSWR("/general/regions", () =>
|
||||
axios
|
||||
.get("/general/regions", { baseURL: import.meta.env.VITE_API_NEST_URL })
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
const [cityId, setCityId] = useState<number | undefined>(undefined)
|
||||
// Load cities when regionId exists
|
||||
const { data: cities, isLoading: citiesLoading } = useSWR(
|
||||
regionId ? `/general/cities?region_id=${regionId}` : null,
|
||||
() =>
|
||||
axios
|
||||
.get(`/general/cities?region_id=${regionId}`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
const { data: boilers, isLoading: boilersLoading } = useSWR(cityId !== undefined ? `/fuel/boilers?city_id=${cityId}&offset=0&limit=100` : null, () => axios.get(`/fuel/boilers?city_id=${cityId}&offset=0&limit=100`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
||||
}).then(res => res.data))
|
||||
// Load boilers when cityId exists
|
||||
const { data: boilers, isLoading: boilersLoading } = useSWR(
|
||||
cityId ? `/fuel/boilers?city_id=${cityId}&offset=0&limit=100` : null,
|
||||
() =>
|
||||
axios
|
||||
.get(`/fuel/boilers?city_id=${cityId}&offset=0&limit=100`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
//
|
||||
// Sync regionId → URL
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!regionId) return;
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("region_id", regionId.toString());
|
||||
params.delete("city_id"); // reset city when region changes
|
||||
return params;
|
||||
});
|
||||
|
||||
}, [regionId]);
|
||||
|
||||
//
|
||||
// Sync cityId → URL
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!cityId) return;
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("city_id", cityId.toString());
|
||||
return params;
|
||||
});
|
||||
}, [cityId]);
|
||||
|
||||
//
|
||||
// Utility: get display values
|
||||
//
|
||||
const selectedRegion = regions?.find((r: IRegion) => r.id === regionId);
|
||||
const selectedCity = cities?.find((c: ICity) => c.id === cityId);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedBoilerSearch(boilerSearch)
|
||||
}, 500)
|
||||
const paramCityId = searchParams.get("city_id");
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
if (!paramCityId) return;
|
||||
|
||||
const id = Number(paramCityId);
|
||||
|
||||
// If cityId not yet set OR mismatched with URL
|
||||
if (cities && !cityId && cities.some((c: ICity) => c.id === id)) {
|
||||
setCityId(id);
|
||||
}
|
||||
}, [boilerSearch])
|
||||
}, [cities])
|
||||
|
||||
useEffect(() => {
|
||||
setBoilersPage(1)
|
||||
setBoilerSearch("")
|
||||
}, [])
|
||||
const paramRegionId = searchParams.get("region_id");
|
||||
if (!paramRegionId) return;
|
||||
|
||||
const id = Number(paramRegionId);
|
||||
|
||||
if (regions && !regionId && regions.some((r: IRegion) => r.id === id)) {
|
||||
setRegionId(id);
|
||||
}
|
||||
}, [regions]);
|
||||
|
||||
useEffect(() => {
|
||||
setCityId(undefined); // clear stale value before SWR runs
|
||||
}, [regionId]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@ -58,42 +126,51 @@ function Boilers() {
|
||||
</Portal> */}
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<Combobox
|
||||
placeholder="Выберите регион"
|
||||
clearable
|
||||
onOptionSelect={(_, data) => {
|
||||
if (data.optionValue) {
|
||||
setRegionId(Number(data.optionValue))
|
||||
} else {
|
||||
setCityId(undefined)
|
||||
setRegionId(undefined)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{regions && Array.isArray(regions) && regions.map((option) => (
|
||||
<Option key={option.id} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>
|
||||
{regionsLoading ?
|
||||
<Spinner />
|
||||
:
|
||||
<Dropdown
|
||||
placeholder='Выберите район'
|
||||
value={selectedRegion?.name ?? ""}
|
||||
selectedOptions={regionId ? [regionId.toString()] : []}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (!data.optionValue) {
|
||||
setRegionId(undefined);
|
||||
return;
|
||||
}
|
||||
setRegionId(Number(data.optionValue));
|
||||
}}
|
||||
>
|
||||
{regions && Array.isArray(regions) && regions.map((option) => (
|
||||
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
{cities && <Combobox
|
||||
clearable
|
||||
placeholder="Выберите населенный пункт"
|
||||
onOptionSelect={(_, data) => {
|
||||
if (data.optionValue) {
|
||||
setCityId(Number(data.optionValue))
|
||||
} else {
|
||||
setCityId(undefined)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{cities && Array.isArray(cities) && cities.map((option) => (
|
||||
<Option key={option.id} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Combobox>}
|
||||
{citiesLoading ?
|
||||
<Spinner />
|
||||
:
|
||||
<Dropdown
|
||||
placeholder='Выберите населенный пункт'
|
||||
value={selectedCity?.name ?? ""}
|
||||
selectedOptions={cityId ? [cityId.toString()] : []}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (!data.optionValue) {
|
||||
setCityId(undefined);
|
||||
return;
|
||||
}
|
||||
setCityId(Number(data.optionValue));
|
||||
}}
|
||||
>
|
||||
{cities && Array.isArray(cities) && cities.map((option) => (
|
||||
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
@ -102,10 +179,6 @@ function Boilers() {
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Text size={400} weight='medium'>
|
||||
{cityId && cities && Array.isArray(cities) && cities.find(city => city.id === cityId).name}
|
||||
</Text>
|
||||
|
||||
{cityId &&
|
||||
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
|
||||
<BoilersCard title='Всего объектов' value={boilers && Array.isArray(boilers) ? boilers.length.toString() : ''} subtitle='' />
|
||||
@ -119,80 +192,48 @@ function Boilers() {
|
||||
}
|
||||
</div>
|
||||
|
||||
<AgGridReact
|
||||
// rowData={[
|
||||
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test1' }), {}),
|
||||
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
|
||||
// ]}
|
||||
key={`boilers-${cityId}`}
|
||||
loading={boilersLoading}
|
||||
overlayLoadingTemplate='Загрузка...'
|
||||
overlayNoRowsTemplate='Нет данных'
|
||||
rowData={boilers}
|
||||
columnDefs={[
|
||||
{
|
||||
field: 'boiler_name',
|
||||
headerName: 'Наименование'
|
||||
},
|
||||
{
|
||||
field: 'boiler_code',
|
||||
headerName: 'Идент. код'
|
||||
},
|
||||
{
|
||||
field: 'id_fuels',
|
||||
headerName: 'Вид топлива',
|
||||
//editable: true,
|
||||
//cellEditor: FuelTypeEditor,
|
||||
autoHeight: true,
|
||||
cellRenderer: FuelRenderer,
|
||||
cellStyle: (_) => {
|
||||
return { padding: '1px' }
|
||||
{boilersLoading ?
|
||||
<Spinner />
|
||||
:
|
||||
<AgGridReact
|
||||
key={`boilers-${cityId}`}
|
||||
loading={boilersLoading}
|
||||
overlayLoadingTemplate='Загрузка...'
|
||||
overlayNoRowsTemplate='Нет данных'
|
||||
rowData={boilers}
|
||||
columnDefs={[
|
||||
{
|
||||
field: 'boiler_name',
|
||||
headerName: 'Наименование'
|
||||
},
|
||||
{
|
||||
field: 'boiler_code',
|
||||
headerName: 'Идент. код'
|
||||
},
|
||||
{
|
||||
field: 'id_fuels',
|
||||
headerName: 'Вид топлива',
|
||||
//editable: true,
|
||||
//cellEditor: FuelTypeEditor,
|
||||
autoHeight: true,
|
||||
cellRenderer: FuelRenderer,
|
||||
cellStyle: (_) => {
|
||||
return { padding: '1px' }
|
||||
}
|
||||
//enableCellChangeFlash: true
|
||||
},
|
||||
{
|
||||
field: 'activity',
|
||||
headerName: 'Активный',
|
||||
cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
|
||||
}
|
||||
//enableCellChangeFlash: true
|
||||
},
|
||||
{
|
||||
field: 'activity',
|
||||
headerName: 'Активный',
|
||||
cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
|
||||
}
|
||||
]}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
]}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BoilersCard = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
value: string
|
||||
subtitle: string
|
||||
}) => {
|
||||
return (
|
||||
<CompoundButton
|
||||
onClick={() => { }}
|
||||
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
|
||||
//icon={icon}
|
||||
>
|
||||
|
||||
<Text weight='bold' size={300}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text weight='bold' size={500}>
|
||||
{value}
|
||||
</Text>
|
||||
|
||||
<Text weight='regular' size={200} style={{ color: 'gray' }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</CompoundButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default Boilers
|
||||
34
client/src/pages/fuel/Fuel/BoilersCard.tsx
Normal file
34
client/src/pages/fuel/Fuel/BoilersCard.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { CompoundButton, Text } from "@fluentui/react-components"
|
||||
|
||||
const BoilersCard = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
value: string
|
||||
subtitle: string
|
||||
}) => {
|
||||
return (
|
||||
<CompoundButton
|
||||
onClick={() => { }}
|
||||
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
|
||||
//icon={icon}
|
||||
>
|
||||
|
||||
<Text weight='bold' size={300}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text weight='bold' size={500}>
|
||||
{value}
|
||||
</Text>
|
||||
|
||||
<Text weight='regular' size={200} style={{ color: 'gray' }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</CompoundButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default BoilersCard
|
||||
@ -16,7 +16,7 @@ export default (params: CustomCellRendererProps) => {
|
||||
<Table style={{ width: '100%' }} size='small'>
|
||||
<TableBody>
|
||||
{Array.isArray(params.value) && params.value.map(fuel => (
|
||||
<TableRow>
|
||||
<TableRow key={`${fuel.id}`}>
|
||||
<TableCell>
|
||||
<TableCellLayout truncate>
|
||||
{fuel.name}
|
||||
|
||||
@ -97,7 +97,7 @@ const FuelsPage = () => {
|
||||
}}
|
||||
>
|
||||
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((option) => (
|
||||
<Option key={option.id} text={option.name} value={option.id}>
|
||||
<Option key={`fuel-type-${option.id}`} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
@ -131,7 +131,7 @@ const FuelsPage = () => {
|
||||
setSelectedIdFuels(Number(data.value))
|
||||
}}>
|
||||
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((ft: FuelType) => (
|
||||
<Tab id={ft.id.toString()} value={ft.id}>
|
||||
<Tab id={ft.id.toString()} value={ft.id} key={ft.id}>
|
||||
{ft.name}
|
||||
</Tab>
|
||||
))
|
||||
|
||||
@ -1,7 +1,391 @@
|
||||
const LimitsPage = () => {
|
||||
return (
|
||||
<div>LimitsPage</div>
|
||||
)
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, CompoundButton, Dropdown, Option, Spinner, Text } from '@fluentui/react-components'
|
||||
import useSWR from 'swr'
|
||||
import axios from 'axios'
|
||||
import { AgGridReact } from 'ag-grid-react'
|
||||
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
|
||||
import { ICity, IRegion, ISeason } from '../../interfaces/fuel'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import LimitAddForm from './Limits/LimitEditForm'
|
||||
|
||||
ModuleRegistry.registerModules([AllCommunityModule])
|
||||
|
||||
function LimitsPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [year, setYear] = useState<number | undefined>(
|
||||
searchParams.get("year") ? Number(searchParams.get("year")) : undefined
|
||||
)
|
||||
|
||||
const [regionId, setRegionId] = useState<number | undefined>(
|
||||
searchParams.get("region_id") ? Number(searchParams.get("region_id")) : undefined
|
||||
);
|
||||
|
||||
const [cityId, setCityId] = useState<number | undefined>(
|
||||
searchParams.get("city_id") ? Number(searchParams.get("city_id")) : undefined
|
||||
)
|
||||
|
||||
// Load regions
|
||||
const { data: regions, isLoading: regionsLoading } = useSWR("/general/regions", () =>
|
||||
axios
|
||||
.get("/general/regions", { baseURL: import.meta.env.VITE_API_NEST_URL })
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
// Load cities when regionId exists
|
||||
const { data: cities, isLoading: citiesLoading } = useSWR(
|
||||
regionId ? `/general/cities?region_id=${regionId}` : null,
|
||||
() =>
|
||||
axios
|
||||
.get(`/general/cities?region_id=${regionId}`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
// Load seasons
|
||||
const { data: seasons } = useSWR(
|
||||
`/fuel/seasons`,
|
||||
() => [
|
||||
{
|
||||
name: 'Отопительный сезон 2025-2026',
|
||||
year: 2025
|
||||
}
|
||||
] as ISeason[]
|
||||
);
|
||||
|
||||
// Load boilers when cityId exists
|
||||
const { data: limits, isLoading: limitsLoading } = useSWR(
|
||||
cityId && year ? `/fuel/limits?city_id=${cityId}&year=${year}&offset=0&limit=100` : null,
|
||||
() =>
|
||||
axios
|
||||
.get(`/fuel/limits?city_id=${cityId}&year=${year}&offset=0&limit=100`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
);
|
||||
|
||||
//
|
||||
// Sync regionId → URL
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!regionId) return;
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("region_id", regionId.toString());
|
||||
params.delete("city_id"); // reset city when region changes
|
||||
return params;
|
||||
});
|
||||
|
||||
}, [regionId]);
|
||||
|
||||
//
|
||||
// Sync cityId → URL
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!cityId) return;
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("city_id", cityId.toString());
|
||||
return params;
|
||||
});
|
||||
}, [cityId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!year) return;
|
||||
|
||||
setSearchParams((prev) => {
|
||||
const params = new URLSearchParams(prev);
|
||||
params.set("year", year.toString());
|
||||
return params;
|
||||
});
|
||||
}, [year]);
|
||||
|
||||
//
|
||||
// Utility: get display values
|
||||
//
|
||||
const selectedRegion = regions?.find((r: IRegion) => r.id === regionId);
|
||||
const selectedCity = cities?.find((c: ICity) => c.id === cityId);
|
||||
const selectedSeason = seasons?.find((c: ISeason) => c.year === year);
|
||||
|
||||
useEffect(() => {
|
||||
const paramCityId = searchParams.get("city_id");
|
||||
|
||||
if (!paramCityId) return;
|
||||
|
||||
const id = Number(paramCityId);
|
||||
|
||||
// If cityId not yet set OR mismatched with URL
|
||||
if (cities && !cityId && cities.some((c: ICity) => c.id === id)) {
|
||||
setCityId(id);
|
||||
}
|
||||
}, [cities])
|
||||
|
||||
useEffect(() => {
|
||||
const paramRegionId = searchParams.get("region_id");
|
||||
if (!paramRegionId) return;
|
||||
|
||||
const id = Number(paramRegionId);
|
||||
|
||||
if (regions && !regionId && regions.some((r: IRegion) => r.id === id)) {
|
||||
setRegionId(id);
|
||||
}
|
||||
}, [regions]);
|
||||
|
||||
useEffect(() => {
|
||||
const paramYear = searchParams.get("year");
|
||||
if (!paramYear) return;
|
||||
|
||||
const year = Number(paramYear);
|
||||
|
||||
if (year && seasons) {
|
||||
setYear(year);
|
||||
}
|
||||
}, [seasons]);
|
||||
|
||||
useEffect(() => {
|
||||
setCityId(undefined); // clear stale value before SWR runs
|
||||
setYear(undefined)
|
||||
}, [regionId])
|
||||
|
||||
const [limitAddFormOpen, setLimitAddFormOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '1rem',
|
||||
width: '100%',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
{/* <Portal mountNode={document.querySelector('#header-portal')}>
|
||||
|
||||
</Portal> */}
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
{regionsLoading ?
|
||||
<Spinner />
|
||||
:
|
||||
<Dropdown
|
||||
placeholder='Выберите район'
|
||||
value={selectedRegion?.name ?? ""}
|
||||
selectedOptions={regionId ? [regionId.toString()] : []}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (!data.optionValue) {
|
||||
setRegionId(undefined);
|
||||
return;
|
||||
}
|
||||
setRegionId(Number(data.optionValue));
|
||||
}}
|
||||
>
|
||||
{regions && Array.isArray(regions) && regions.map((option) => (
|
||||
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
{citiesLoading ?
|
||||
<Spinner />
|
||||
:
|
||||
<Dropdown
|
||||
placeholder='Выберите населенный пункт'
|
||||
value={selectedCity?.name ?? ""}
|
||||
selectedOptions={cityId ? [cityId.toString()] : []}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (!data.optionValue) {
|
||||
setCityId(undefined);
|
||||
return;
|
||||
}
|
||||
setCityId(Number(data.optionValue));
|
||||
}}
|
||||
>
|
||||
{cities && Array.isArray(cities) && cities.map((option) => (
|
||||
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
<Dropdown
|
||||
placeholder='Выберите отопительный сезон'
|
||||
value={selectedSeason?.name ?? ""}
|
||||
selectedOptions={year ? [year.toString()] : []}
|
||||
onOptionSelect={(_, data) => {
|
||||
if (!data.optionValue) {
|
||||
setYear(undefined);
|
||||
return;
|
||||
}
|
||||
setYear(Number(data.optionValue));
|
||||
}}
|
||||
>
|
||||
{seasons && Array.isArray(seasons) && seasons.map((option) => (
|
||||
<Option key={`region-${option.year}`} text={option.name} value={option.year.toString()}>
|
||||
{option.name}
|
||||
</Option>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Text size={400} weight='medium'>
|
||||
{cityId && cities && Array.isArray(cities) && cities.find(city => city.id === cityId).name}
|
||||
</Text>
|
||||
|
||||
{cityId &&
|
||||
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
|
||||
<BoilersCard title='Всего объектов' value={limits && Array.isArray(limits) ? limits.length.toString() : ''} subtitle='' />
|
||||
|
||||
<BoilersCard title='Общий остаток' value={''} subtitle='' />
|
||||
|
||||
<BoilersCard title='Лимит на сезон' value={''} subtitle='' />
|
||||
|
||||
<BoilersCard title='Требуют внимания' value={''} subtitle='' />
|
||||
</div>
|
||||
}
|
||||
|
||||
<Button appearance='primary' style={{ width: 'min-content' }} onClick={() => setLimitAddFormOpen(true)}>
|
||||
Добавить
|
||||
</Button>
|
||||
|
||||
<LimitAddForm cityId={cityId} open={limitAddFormOpen} setOpen={setLimitAddFormOpen} />
|
||||
</div>
|
||||
|
||||
<AgGridReact
|
||||
key={`boilers-${cityId}`}
|
||||
loading={limitsLoading}
|
||||
overlayLoadingTemplate='Загрузка...'
|
||||
overlayNoRowsTemplate='Нет данных'
|
||||
rowData={limits}
|
||||
onRowClicked={(e) => {
|
||||
console.log(e.data)
|
||||
}
|
||||
}
|
||||
columnDefs={[
|
||||
{
|
||||
field: 'boiler_name',
|
||||
headerName: 'Наименование'
|
||||
},
|
||||
{
|
||||
field: 'fuel_name',
|
||||
headerName: 'Вид топлива'
|
||||
},
|
||||
// {
|
||||
// field: 'boiler_code',
|
||||
// headerName: 'Идент. код'
|
||||
// },
|
||||
// {
|
||||
// field: 'id_fuels',
|
||||
// headerName: 'Вид топлива',
|
||||
// //editable: true,
|
||||
// //cellEditor: FuelTypeEditor,
|
||||
// autoHeight: true,
|
||||
// cellRenderer: FuelRenderer,
|
||||
// cellStyle: (_) => {
|
||||
// return { padding: '1px' }
|
||||
// }
|
||||
// //enableCellChangeFlash: true
|
||||
// },
|
||||
// {
|
||||
// field: 'activity',
|
||||
// headerName: 'Активный',
|
||||
// cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
|
||||
// }
|
||||
{
|
||||
field: 'jul',
|
||||
headerName: 'Июль'
|
||||
},
|
||||
{
|
||||
field: 'aug',
|
||||
headerName: 'Август'
|
||||
},
|
||||
{
|
||||
field: 'sep',
|
||||
headerName: 'Сентябрь'
|
||||
},
|
||||
{
|
||||
field: 'oct',
|
||||
headerName: 'Октябрь'
|
||||
},
|
||||
{
|
||||
field: 'nov',
|
||||
headerName: 'Ноябрь'
|
||||
},
|
||||
{
|
||||
field: 'dec',
|
||||
headerName: 'Декабрь'
|
||||
},
|
||||
{
|
||||
field: 'jan',
|
||||
headerName: 'Январь'
|
||||
},
|
||||
{
|
||||
field: 'feb',
|
||||
headerName: 'Февраль'
|
||||
},
|
||||
{
|
||||
field: 'mar',
|
||||
headerName: 'Март'
|
||||
},
|
||||
{
|
||||
field: 'apr',
|
||||
headerName: 'Апрель'
|
||||
},
|
||||
{
|
||||
field: 'may',
|
||||
headerName: 'Май'
|
||||
},
|
||||
{
|
||||
field: 'jun',
|
||||
headerName: 'Июнь'
|
||||
}
|
||||
]}
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BoilersCard = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
value: string
|
||||
subtitle: string
|
||||
}) => {
|
||||
return (
|
||||
<CompoundButton
|
||||
onClick={() => { }}
|
||||
style={{ display: 'flex', width: '100%', justifyContent: 'flex-start', flexDirection: 'column', gap: '2rem', alignItems: 'flex-start', cursor: 'pointer', userSelect: 'none' }}
|
||||
//icon={icon}
|
||||
>
|
||||
|
||||
<Text weight='bold' size={300}>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text weight='bold' size={500}>
|
||||
{value}
|
||||
</Text>
|
||||
|
||||
<Text weight='regular' size={200} style={{ color: 'gray' }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</CompoundButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default LimitsPage
|
||||
287
client/src/pages/fuel/Limits/LimitEditForm.tsx
Normal file
287
client/src/pages/fuel/Limits/LimitEditForm.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
import { Button, Dialog, DialogBody, DialogContent, DialogSurface, DialogTitle, Field, Input, ProgressBar, Spinner } from '@fluentui/react-components'
|
||||
import { DataPieColor } from '@fluentui/react-icons'
|
||||
import axios from 'axios'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Controller, SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'
|
||||
import useSWR from 'swr'
|
||||
|
||||
type Month = {
|
||||
month: number
|
||||
value: number
|
||||
}
|
||||
|
||||
type Inputs = {
|
||||
id_boiler: string
|
||||
id_fuel: string
|
||||
year: number
|
||||
months: Month[]
|
||||
}
|
||||
|
||||
const months = [
|
||||
{
|
||||
id: 7,
|
||||
month: 'jul',
|
||||
label: 'Июль'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
month: 'aug',
|
||||
label: 'Август'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
month: 'sep',
|
||||
label: 'Сентябрь'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
month: 'nov',
|
||||
label: 'Октябрь'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
month: 'oct',
|
||||
label: 'Ноябрь'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
month: 'dec',
|
||||
label: 'Декабрь'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
month: 'jan',
|
||||
label: 'Январь'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
month: 'feb',
|
||||
label: 'Февраль'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
month: 'mar',
|
||||
label: 'Март'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
month: 'apr',
|
||||
label: 'Апрель'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
month: 'may',
|
||||
label: 'Май'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
month: 'jun',
|
||||
label: 'Июнь'
|
||||
},
|
||||
]
|
||||
|
||||
const LimitAddForm = ({
|
||||
cityId,
|
||||
open,
|
||||
setOpen,
|
||||
percentage
|
||||
}: {
|
||||
cityId: number | undefined
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
percentage?: boolean
|
||||
}) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<Inputs>({
|
||||
defaultValues: {
|
||||
months: Array.from({ length: 12 }, (_, i) => ({
|
||||
month: i + 1,
|
||||
value: 0,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: 'months'
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<Inputs> = async (data) => {
|
||||
await axios.post(`/fuel/limits`, {
|
||||
id_boiler: '06407B1C-C23F-44C8-BADF-4653060EB784',
|
||||
id_fuel: 3,
|
||||
year: 2025,
|
||||
months: data.months
|
||||
}, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
//mutateFuels([...fuels, data])
|
||||
|
||||
// setTimeout(() => {
|
||||
// console.log("done")
|
||||
// }, 1000)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(() => resolve(console.log("done")), 1000);
|
||||
})
|
||||
}
|
||||
|
||||
const [overallLimit, setOverallLimit] = useState(0)
|
||||
|
||||
const watchedMonths = useWatch({
|
||||
control,
|
||||
name: "months",
|
||||
})
|
||||
|
||||
const { data: citySettings } = useSWR(
|
||||
cityId ? `/fuel/city-settings?city_id=${cityId}` : null,
|
||||
() =>
|
||||
axios
|
||||
.get(`/fuel/city-settings?city_id=${cityId}`, {
|
||||
baseURL: import.meta.env.VITE_API_NEST_URL,
|
||||
})
|
||||
.then((res) => res.data)
|
||||
)
|
||||
|
||||
const handlePartition = () => {
|
||||
if (citySettings && Array.isArray(citySettings) && citySettings.length > 0) {
|
||||
citySettings.map(s => {
|
||||
setValue(`months.${Number(s.month - 1)}.value`, Number((Number((overallLimit / 100).toFixed(3)) * s.procent).toFixed(3)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (watchedMonths) {
|
||||
let sum = 0
|
||||
watchedMonths.map(wm => sum = sum + wm.value)
|
||||
setOverallLimit(Number(sum.toFixed(3)))
|
||||
}
|
||||
}, [watchedMonths])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(_, data) => setOpen(data.open)}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
{isSubmitting &&
|
||||
<div style={{ position: 'absolute', inset: 0, zIndex: '1', background: '#00000030', display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }}>
|
||||
<Spinner />
|
||||
</div>
|
||||
}
|
||||
<DialogTitle>Распределение лимитов</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column', gap: '1rem',
|
||||
minWidth: 'fit-content', minHeight: 'fit-content'
|
||||
}}
|
||||
>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handlePartition()
|
||||
}} style={{ display: 'flex', width: '100%', gap: '0.25rem', justifyContent: 'flex-end' }}>
|
||||
<Field style={{ display: 'flex' }} label='Лимит расхода котельного топлива за сезон' orientation='horizontal'>
|
||||
<Input type='number' value={overallLimit.toString()} onChange={(_, data) => setOverallLimit(Number(data.value))} />
|
||||
</Field>
|
||||
|
||||
<Button title='Распределить' type='submit' icon={<DataPieColor />}></Button>
|
||||
</form>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '1rem', width: '100%' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '50%', gap: '0.25rem' }}>
|
||||
{fields.map((f, index) => {
|
||||
if (f.month >= 7 && f.month <= 12) {
|
||||
return (
|
||||
<Controller
|
||||
key={f.month}
|
||||
name={`months.${index}.value`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Field style={{ gridTemplateColumns: '1fr auto' }} key={f.month} validationMessage={percentage ? `${(field.value / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={months.find(m => m.id === f.month)?.label} orientation='horizontal'>
|
||||
<Input value={field.value.toString()} onChange={(_, data) => field.onChange(Number(data.value))} />
|
||||
{percentage && <ProgressBar value={field.value / (overallLimit / 100) * 0.01} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
|
||||
|
||||
<Field style={{ gridTemplateColumns: '1fr auto' }} validationMessage={percentage ? `${(watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0) / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={'2 полугодие'} orientation='horizontal'>
|
||||
<Input disabled value={watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0).toFixed(3)} />
|
||||
|
||||
{percentage && <ProgressBar value={watchedMonths.filter(month => month.month >= 7 && month.month <= 12)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0) / (overallLimit / 100) * 0.01} />}
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', width: '50%', gap: '0.25rem' }}>
|
||||
{fields.map((f, index) => {
|
||||
if (f.month >= 1 && f.month <= 6) {
|
||||
return (
|
||||
<Controller
|
||||
key={f.month}
|
||||
name={`months.${index}.value`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Field style={{ gridTemplateColumns: '1fr auto' }} key={f.month} validationMessage={percentage ? `${(field.value / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={months.find(m => m.id === f.month)?.label} orientation='horizontal'>
|
||||
<Input value={field.value.toString()} onChange={(_, data) => field.onChange(Number(data.value))} />
|
||||
{percentage && <ProgressBar value={field.value / (overallLimit / 100) * 0.01} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})}
|
||||
|
||||
<Field style={{ gridTemplateColumns: '1fr auto' }} validationMessage={percentage ? `${(watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0) / (overallLimit / 100)).toFixed(1)}%` : undefined} validationState='none' label={'1 полугодие'} orientation='horizontal'>
|
||||
<Input disabled value={watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0).toFixed(3)} />
|
||||
{percentage && <ProgressBar value={watchedMonths.filter(month => month.month >= 1 && month.month <= 6)
|
||||
.reduce((sum, month) => {
|
||||
const value = Number(month.value) || 0;
|
||||
return sum + value;
|
||||
}, 0) / (overallLimit / 100) * 0.01} />}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem', justifyContent: 'flex-end' }}>
|
||||
<Button type='submit' appearance='primary'>
|
||||
Добавить
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default LimitAddForm
|
||||
19
client/src/store/fuel.ts
Normal file
19
client/src/store/fuel.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface FuelState {
|
||||
regionId: number | undefined
|
||||
cityId: number | undefined
|
||||
}
|
||||
|
||||
export const useFuelStore = create<FuelState>(() => {
|
||||
return {
|
||||
regionId: undefined,
|
||||
cityId: undefined
|
||||
}
|
||||
})
|
||||
|
||||
export const getRegionId = () => useFuelStore.getState().regionId
|
||||
export const setRegionId = (id: number | undefined) => useFuelStore.setState(() => ({ regionId: id }))
|
||||
|
||||
export const getCityId = () => useFuelStore.getState().cityId
|
||||
export const setCityId = (id: number | undefined) => useFuelStore.setState(() => ({ cityId: id }))
|
||||
@ -1,5 +1,14 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNumber, IsUUID } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsNumber, IsObject, IsUUID, ValidateNested } from "class-validator";
|
||||
|
||||
class MonthDto {
|
||||
@IsNumber()
|
||||
month: number
|
||||
|
||||
@IsNumber()
|
||||
value: number
|
||||
}
|
||||
|
||||
export class CreateLimitDto {
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
@ -10,15 +19,13 @@ export class CreateLimitDto {
|
||||
@IsNumber()
|
||||
id_fuel: number
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
month: Date
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
year: number
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
value: number
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MonthDto)
|
||||
months: MonthDto[]
|
||||
}
|
||||
18
server/src/fuel/dto/get-city-settings.ts
Normal file
18
server/src/fuel/dto/get-city-settings.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
|
||||
import { IsNumberString, IsOptional } from "class-validator"
|
||||
|
||||
export class GetCitySettingsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumberString()
|
||||
city_id: number
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsNumberString()
|
||||
@IsOptional()
|
||||
offset?: number
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsNumberString()
|
||||
@IsOptional()
|
||||
limit?: number
|
||||
}
|
||||
22
server/src/fuel/dto/get-limits.ts
Normal file
22
server/src/fuel/dto/get-limits.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"
|
||||
import { IsNumberString, IsOptional } from "class-validator"
|
||||
|
||||
export class GetLimitsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumberString()
|
||||
city_id: number
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumberString()
|
||||
year: number
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsNumberString()
|
||||
@IsOptional()
|
||||
offset?: number
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsNumberString()
|
||||
@IsOptional()
|
||||
limit?: number
|
||||
}
|
||||
@ -9,6 +9,8 @@ import { FuelExpenseDto } from './dto/expense'
|
||||
import { FuelTransferDto } from './dto/transfer'
|
||||
import { GetBoilersDTO } from './dto/get-boilers'
|
||||
import { GetFuelsDTO } from './dto/get-fuels'
|
||||
import { GetLimitsDTO } from './dto/get-limits'
|
||||
import { GetCitySettingsDTO } from './dto/get-city-settings'
|
||||
|
||||
@Controller('fuel')
|
||||
export class FuelController {
|
||||
@ -43,8 +45,8 @@ export class FuelController {
|
||||
type: FuelLimitDto,
|
||||
isArray: true,
|
||||
})
|
||||
async getBoilersFuelsLimits(): Promise<FuelLimitDto[]> {
|
||||
return this.fuelService.getBoilersFuelsLimits()
|
||||
async getBoilersFuelsLimits(@Query() getLimitsDTO: GetLimitsDTO): Promise<FuelLimitDto[]> {
|
||||
return this.fuelService.getBoilersFuelsLimits(getLimitsDTO)
|
||||
}
|
||||
|
||||
@Post('/limits')
|
||||
@ -52,6 +54,16 @@ export class FuelController {
|
||||
return this.fuelService.addBoilersFuelsLimit(createLimitDto)
|
||||
}
|
||||
|
||||
// Fuel limits
|
||||
@Get('/city-settings')
|
||||
@ApiOkResponse({
|
||||
description: 'City settings',
|
||||
isArray: true,
|
||||
})
|
||||
async getCitySettings(@Query() getCitySettingsDTO: GetCitySettingsDTO): Promise<any[]> {
|
||||
return this.fuelService.getCitySettings(getCitySettingsDTO)
|
||||
}
|
||||
|
||||
// Fuel expenses
|
||||
@Get('/expenses')
|
||||
@ApiOkResponse({
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { CreateExpenseDto } from './dto/create-expense';
|
||||
import { CreateLimitDto } from './dto/create-limit';
|
||||
import { CreateTransferDto } from './dto/create-transfer';
|
||||
import { GetFuelsDTO } from './dto/get-fuels';
|
||||
import { GetLimitsDTO } from './dto/get-limits';
|
||||
import { GetCitySettingsDTO } from './dto/get-city-settings';
|
||||
|
||||
@Injectable()
|
||||
export class FuelService {
|
||||
@ -56,13 +58,12 @@ export class FuelService {
|
||||
const result = await this.wsDataSource.query(`
|
||||
SELECT
|
||||
b.*,
|
||||
COALESCE(
|
||||
(SELECT fp.*
|
||||
(
|
||||
SELECT fp.*
|
||||
FROM isFuels.dbo.BoilersFuels bf
|
||||
INNER JOIN isWorldstone.dbo.dFuelsParameters fp ON bf.id_fuels = fp.id
|
||||
WHERE bf.id_boilers = b.id_object
|
||||
FOR JSON PATH),
|
||||
'[]'
|
||||
FOR JSON PATH
|
||||
) AS id_fuels
|
||||
FROM isWorldstone.dbo.vBoilers b
|
||||
WHERE id_city = @0
|
||||
@ -90,11 +91,118 @@ export class FuelService {
|
||||
return result
|
||||
}
|
||||
|
||||
async getBoilersFuelsLimits(): Promise<any[]> {
|
||||
async getCitySettings(getCitySettings: GetCitySettingsDTO): Promise<any[]> {
|
||||
const { city_id } = getCitySettings
|
||||
|
||||
const result = await this.wsDataSource.query(`
|
||||
SELECT * FROM isWorldstone..vCitySettings
|
||||
WHERE id_city = @0
|
||||
`, [Number(city_id)])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async getBoilersFuelsLimits(getLimitsDTO: GetLimitsDTO): Promise<any[]> {
|
||||
const { city_id, year, offset, limit } = getLimitsDTO
|
||||
|
||||
// const result = await this.wsDataSource.query(`
|
||||
// SELECT
|
||||
// b.*,
|
||||
// (
|
||||
// SELECT
|
||||
// bf.id AS id_fuels_entry,
|
||||
// df.id AS fuel_id,
|
||||
// df.name,
|
||||
|
||||
// -- вложенный JSON: лимиты по месяцам
|
||||
// (
|
||||
// SELECT
|
||||
// l.month,
|
||||
// l.year,
|
||||
// l.value
|
||||
// FROM isFuels..BoilersFuelsLimits l
|
||||
// WHERE l.id_fuel = bf.id
|
||||
// ORDER BY l.year, l.month
|
||||
// FOR JSON PATH
|
||||
// ) AS limits
|
||||
|
||||
// FROM isFuels..BoilersFuels bf
|
||||
// JOIN isWorldstone..dFuelsParameters df
|
||||
// ON df.id = bf.id_fuels
|
||||
// WHERE bf.id_boilers = b.id_object
|
||||
// FOR JSON PATH
|
||||
// ) AS fuels
|
||||
// FROM isWorldstone..vBoilers b
|
||||
// WHERE id_city = @0
|
||||
// ORDER BY id_object
|
||||
// OFFSET @1 ROWS
|
||||
// FETCH NEXT @2 ROWS ONLY
|
||||
// ;
|
||||
// `, [city_id, Number(offset || 0), Number(limit || 100)])
|
||||
|
||||
const result = await this.wsDataSource.query(`
|
||||
WITH Src AS (
|
||||
SELECT
|
||||
b.id_object AS id_boiler,
|
||||
b.name AS boiler_name,
|
||||
bf.id AS id_boiler_fuel,
|
||||
bf.id_fuels AS id_fuel_type,
|
||||
fp.name AS fuel_name,
|
||||
l.month,
|
||||
l.value,
|
||||
l.year
|
||||
FROM isWorldstone..vBoilers b
|
||||
LEFT JOIN isFuels..BoilersFuels bf
|
||||
ON bf.id_boilers = b.id_object
|
||||
LEFT JOIN isWorldstone..dFuelsParameters fp
|
||||
ON fp.id = bf.id_fuels
|
||||
LEFT JOIN isFuels..BoilersFuelsLimits l
|
||||
ON l.id_boiler = bf.id_boilers
|
||||
AND l.id_fuel = bf.id
|
||||
AND (
|
||||
(l.month BETWEEN 7 AND 12 AND l.year = @1) -- second half
|
||||
OR
|
||||
(l.month BETWEEN 1 AND 6 AND l.year = @2) -- first half next year
|
||||
)
|
||||
WHERE b.id_city = @0
|
||||
),
|
||||
|
||||
Renamed AS (
|
||||
SELECT
|
||||
id_boiler,
|
||||
boiler_name,
|
||||
id_boiler_fuel,
|
||||
id_fuel_type,
|
||||
fuel_name,
|
||||
CASE month
|
||||
WHEN 7 THEN 'jul'
|
||||
WHEN 8 THEN 'aug'
|
||||
WHEN 9 THEN 'sep'
|
||||
WHEN 10 THEN 'oct'
|
||||
WHEN 11 THEN 'nov'
|
||||
WHEN 12 THEN 'dec'
|
||||
WHEN 1 THEN 'jan'
|
||||
WHEN 2 THEN 'feb'
|
||||
WHEN 3 THEN 'mar'
|
||||
WHEN 4 THEN 'apr'
|
||||
WHEN 5 THEN 'may'
|
||||
WHEN 6 THEN 'jun'
|
||||
END AS MonthName,
|
||||
value
|
||||
FROM Src
|
||||
)
|
||||
|
||||
SELECT *
|
||||
FROM "vBoilerLimits";
|
||||
`)
|
||||
FROM Renamed
|
||||
PIVOT (
|
||||
MAX(value)
|
||||
FOR MonthName IN (
|
||||
[jul], [aug], [sep], [oct], [nov], [dec],
|
||||
[jan], [feb], [mar], [apr], [may], [jun]
|
||||
)
|
||||
) AS P
|
||||
ORDER BY id_boiler, id_boiler_fuel;
|
||||
`, [city_id, year, year + 1])
|
||||
return result
|
||||
}
|
||||
|
||||
@ -114,10 +222,14 @@ export class FuelService {
|
||||
}
|
||||
|
||||
async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> {
|
||||
const result = await this.dataSource.query(`
|
||||
INSERT INTO dbo.BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5)
|
||||
`, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
|
||||
return result
|
||||
const logger = new Logger('INFO'); // 'MyContext' is an optional string for log context
|
||||
logger.log(createLimitDto)
|
||||
|
||||
return [createLimitDto]
|
||||
// const result = await this.dataSource.query(`
|
||||
// INSERT INTO isFuels..BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5)
|
||||
// `, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
|
||||
// return result
|
||||
}
|
||||
|
||||
async addFuelsTransfer(createTransferDto: CreateTransferDto): Promise<any[]> {
|
||||
|
||||
Reference in New Issue
Block a user