fuel; nest api
This commit is contained in:
@ -17,7 +17,7 @@ const ObjectTree = ({
|
|||||||
|
|
||||||
const { data: existingObjectsList } = useSWR(
|
const { data: existingObjectsList } = useSWR(
|
||||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
|
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)) {
|
if (Array.isArray(res)) {
|
||||||
let count = 0
|
let count = 0
|
||||||
res.forEach(el => {
|
res.forEach(el => {
|
||||||
@ -34,7 +34,7 @@ const ObjectTree = ({
|
|||||||
|
|
||||||
const { data: planningObjectsList } = useSWR(
|
const { data: planningObjectsList } = useSWR(
|
||||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
|
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)) {
|
if (Array.isArray(res)) {
|
||||||
let count = 0
|
let count = 0
|
||||||
res.forEach(el => {
|
res.forEach(el => {
|
||||||
@ -116,7 +116,7 @@ const ObjectList = ({
|
|||||||
|
|
||||||
const { data } = useSWR(
|
const { data } = useSWR(
|
||||||
selectedDistrict && selectedYear ? `/general/objects/list?type=${id}&city_id=${selectedDistrict}&year=${selectedYear}&planning=${planning}` : null,
|
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,
|
revalidateOnFocus: false,
|
||||||
revalidateIfStale: false
|
revalidateIfStale: false
|
||||||
|
|||||||
@ -78,20 +78,20 @@ const MapComponent = ({
|
|||||||
const mapElement = useRef<HTMLDivElement | null>(null)
|
const mapElement = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
// Get type roles
|
// 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)) {
|
if (Array.isArray(res)) {
|
||||||
setTypeRoles(id, res)
|
setTypeRoles(id, res)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}), swrOptions)
|
}), 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)
|
setRegionsData(res)
|
||||||
return res
|
return res
|
||||||
}), swrOptions)
|
}), swrOptions)
|
||||||
|
|
||||||
// Bounds: region
|
// 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
|
// Map init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -169,7 +169,7 @@ const MapComponent = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDistrict && selectedYear && districtBoundLayer) {
|
if (selectedDistrict && selectedYear && districtBoundLayer) {
|
||||||
const bounds = new VectorSource({
|
const bounds = new VectorSource({
|
||||||
url: `${BASE_URL.ems}/gis/bounds/city/${selectedDistrict}`,
|
url: `${BASE_URL.nest}/gis/bounds/city/${selectedDistrict}`,
|
||||||
format: new GeoJSON(),
|
format: new GeoJSON(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ const MapComponent = ({
|
|||||||
}
|
}
|
||||||
}, [currentObjectId, figuresLayer, linesLayer])
|
}, [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)
|
setDistrictsData(res)
|
||||||
return res
|
return res
|
||||||
}), swrOptions)
|
}), swrOptions)
|
||||||
@ -287,7 +287,7 @@ const MapComponent = ({
|
|||||||
const { data: figuresData, isValidating: figuresValidating } = useSWR(
|
const { data: figuresData, isValidating: figuresValidating } = useSWR(
|
||||||
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
selectedDistrict && selectedYear ? `/gis/figures/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
||||||
(url) => axios.get(url, {
|
(url) => axios.get(url, {
|
||||||
baseURL: BASE_URL.ems
|
baseURL: BASE_URL.nest
|
||||||
}).then((res) => res.data),
|
}).then((res) => res.data),
|
||||||
swrOptions
|
swrOptions
|
||||||
)
|
)
|
||||||
@ -295,7 +295,7 @@ const MapComponent = ({
|
|||||||
const { data: linesData, isValidating: linesValidating } = useSWR(
|
const { data: linesData, isValidating: linesValidating } = useSWR(
|
||||||
!figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
!figuresValidating && selectedDistrict && selectedYear ? `/gis/lines/all?city_id=${selectedDistrict}&year=${selectedYear}&offset=0&limit=${10000}` : null,
|
||||||
(url) => axios.get(url, {
|
(url) => axios.get(url, {
|
||||||
baseURL: BASE_URL.ems
|
baseURL: BASE_URL.nest
|
||||||
}).then((res) => res.data),
|
}).then((res) => res.data),
|
||||||
swrOptions
|
swrOptions
|
||||||
)
|
)
|
||||||
@ -303,7 +303,7 @@ const MapComponent = ({
|
|||||||
const { data: districtData } = useSWR(
|
const { data: districtData } = useSWR(
|
||||||
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
|
selectedDistrict ? `/gis/images/all?city_id=${selectedDistrict}` : null,
|
||||||
(url) => axios.get(url, {
|
(url) => axios.get(url, {
|
||||||
baseURL: BASE_URL.ems
|
baseURL: BASE_URL.nest
|
||||||
}).then((res) => Array.isArray(res.data) ? res.data[0] : null),
|
}).then((res) => Array.isArray(res.data) ? res.data[0] : null),
|
||||||
swrOptions
|
swrOptions
|
||||||
)
|
)
|
||||||
@ -398,7 +398,7 @@ const MapComponent = ({
|
|||||||
list.push(district.id as Number)
|
list.push(district.id as Number)
|
||||||
})
|
})
|
||||||
|
|
||||||
fetch(`${BASE_URL.ems}/gis/bounds/city`, {
|
fetch(`${BASE_URL.nest}/gis/bounds/city`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const MapLegend = ({
|
|||||||
|
|
||||||
const { data: existingObjectsList } = useSWR(
|
const { data: existingObjectsList } = useSWR(
|
||||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=0` : null,
|
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
|
revalidateOnFocus: false
|
||||||
}
|
}
|
||||||
@ -23,7 +23,7 @@ const MapLegend = ({
|
|||||||
|
|
||||||
const { data: planningObjectsList } = useSWR(
|
const { data: planningObjectsList } = useSWR(
|
||||||
selectedYear && selectedDistrict ? `/general/objects/list?year=${selectedYear}&city_id=${selectedDistrict}&planning=1` : null,
|
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
|
revalidateOnFocus: false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const MapObjectSearch = ({
|
|||||||
|
|
||||||
const { data: searchData } = useSWR(
|
const { data: searchData } = useSWR(
|
||||||
throttledSearchObject !== "" && selectedDistrict && selectedYear ? `/general/search/objects?q=${throttledSearchObject}&id_city=${selectedDistrict}&year=${selectedYear}` : null,
|
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
|
swrOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const MapRegionSelect = ({
|
|||||||
const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
|
const { selectedRegion, selectedYear, selectedDistrict } = useObjectsStore().id[map_id]
|
||||||
const { regionsData } = useRegionsStore()
|
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)
|
setDistrictsData(res)
|
||||||
return res
|
return res
|
||||||
}), swrOptions)
|
}), swrOptions)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { BASE_URL } from '../../constants'
|
|||||||
const ObjectData = (object_data: IObjectData) => {
|
const ObjectData = (object_data: IObjectData) => {
|
||||||
const { data: typeData } = useSWR(
|
const { data: typeData } = useSWR(
|
||||||
object_data.type ? `/general/types` : null,
|
object_data.type ? `/general/types` : null,
|
||||||
(url) => fetcher(url, BASE_URL.ems),
|
(url) => fetcher(url, BASE_URL.nest),
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false
|
revalidateOnFocus: false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const ObjectParameters = ({
|
|||||||
|
|
||||||
const { data: valuesData, isValidating: valuesValidating } = useSWR(
|
const { data: valuesData, isValidating: valuesValidating } = useSWR(
|
||||||
currentObjectId ? `/general/values/?object_id=${currentObjectId}` : null,
|
currentObjectId ? `/general/values/?object_id=${currentObjectId}` : null,
|
||||||
(url) => fetcher(url, BASE_URL.ems),
|
(url) => fetcher(url, BASE_URL.nest),
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
revalidateIfStale: false
|
revalidateIfStale: false
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { BASE_URL } from '../../constants'
|
|||||||
import { fetcher } from '../../http/axiosInstance'
|
import { fetcher } from '../../http/axiosInstance'
|
||||||
|
|
||||||
const RegionSelect = () => {
|
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,
|
revalidateOnFocus: false,
|
||||||
revalidateIfStale: false
|
revalidateIfStale: false
|
||||||
})
|
})
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const TableValue = ({
|
|||||||
|
|
||||||
const { data: tcbAll, isValidating } = useSWR(
|
const { data: tcbAll, isValidating } = useSWR(
|
||||||
type === 'select' && selectedDistrict ? `/general/params/tcb?vtable=${vtable}&id_city=${selectedDistrict}` : null,
|
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)) {
|
if (Array.isArray(res)) {
|
||||||
return res.map((el) => ({
|
return res.map((el) => ({
|
||||||
label: el.name || "",
|
label: el.name || "",
|
||||||
|
|||||||
@ -9,4 +9,5 @@ export const BASE_URL = {
|
|||||||
fuel: import.meta.env.VITE_API_FUEL_URL,
|
fuel: import.meta.env.VITE_API_FUEL_URL,
|
||||||
servers: import.meta.env.VITE_API_SERVERS_URL,
|
servers: import.meta.env.VITE_API_SERVERS_URL,
|
||||||
ems: import.meta.env.VITE_API_EMS_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;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISeason {
|
||||||
|
name: string
|
||||||
|
year: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICity {
|
export interface ICity {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -1,49 +1,117 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 useSWR from 'swr'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { AgGridReact, CustomCellRendererProps } from 'ag-grid-react'
|
import { AgGridReact, CustomCellRendererProps } from 'ag-grid-react'
|
||||||
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
|
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community'
|
||||||
import FuelRenderer from './FuelRenderer'
|
import FuelRenderer from './FuelRenderer'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { ICity, IRegion } from '../../../interfaces/fuel'
|
||||||
|
import BoilersCard from './BoilersCard'
|
||||||
|
|
||||||
ModuleRegistry.registerModules([AllCommunityModule])
|
ModuleRegistry.registerModules([AllCommunityModule])
|
||||||
|
|
||||||
function Boilers() {
|
function Boilers() {
|
||||||
const [, setBoilersPage] = useState(1)
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [boilerSearch, setBoilerSearch] = useState("")
|
|
||||||
const [, setDebouncedBoilerSearch] = useState("")
|
|
||||||
// const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
|
|
||||||
|
|
||||||
const { data: regions } = useSWR('/general/regions', () => axios.get('/general/regions', {
|
const [regionId, setRegionId] = useState<number | undefined>(
|
||||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
searchParams.get("region_id") ? Number(searchParams.get("region_id")) : undefined
|
||||||
}).then(res => res.data))
|
);
|
||||||
|
|
||||||
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}`, {
|
// Load regions
|
||||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
const { data: regions, isLoading: regionsLoading } = useSWR("/general/regions", () =>
|
||||||
}).then(res => res.data))
|
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`, {
|
// Load boilers when cityId exists
|
||||||
baseURL: import.meta.env.VITE_API_NEST_URL
|
const { data: boilers, isLoading: boilersLoading } = useSWR(
|
||||||
}).then(res => res.data))
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const paramCityId = searchParams.get("city_id");
|
||||||
setDebouncedBoilerSearch(boilerSearch)
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
return () => {
|
if (!paramCityId) return;
|
||||||
clearTimeout(handler)
|
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
setBoilersPage(1)
|
const paramRegionId = searchParams.get("region_id");
|
||||||
setBoilerSearch("")
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -58,42 +126,51 @@ function Boilers() {
|
|||||||
</Portal> */}
|
</Portal> */}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
<Combobox
|
{regionsLoading ?
|
||||||
placeholder="Выберите регион"
|
<Spinner />
|
||||||
clearable
|
:
|
||||||
onOptionSelect={(_, data) => {
|
<Dropdown
|
||||||
if (data.optionValue) {
|
placeholder='Выберите район'
|
||||||
setRegionId(Number(data.optionValue))
|
value={selectedRegion?.name ?? ""}
|
||||||
} else {
|
selectedOptions={regionId ? [regionId.toString()] : []}
|
||||||
setCityId(undefined)
|
onOptionSelect={(_, data) => {
|
||||||
setRegionId(undefined)
|
if (!data.optionValue) {
|
||||||
}
|
setRegionId(undefined);
|
||||||
}}
|
return;
|
||||||
>
|
}
|
||||||
{regions && Array.isArray(regions) && regions.map((option) => (
|
setRegionId(Number(data.optionValue));
|
||||||
<Option key={option.id} text={option.name} value={option.id}>
|
}}
|
||||||
{option.name}
|
>
|
||||||
</Option>
|
{regions && Array.isArray(regions) && regions.map((option) => (
|
||||||
))}
|
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||||
</Combobox>
|
{option.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
|
||||||
{cities && <Combobox
|
{citiesLoading ?
|
||||||
clearable
|
<Spinner />
|
||||||
placeholder="Выберите населенный пункт"
|
:
|
||||||
onOptionSelect={(_, data) => {
|
<Dropdown
|
||||||
if (data.optionValue) {
|
placeholder='Выберите населенный пункт'
|
||||||
setCityId(Number(data.optionValue))
|
value={selectedCity?.name ?? ""}
|
||||||
} else {
|
selectedOptions={cityId ? [cityId.toString()] : []}
|
||||||
setCityId(undefined)
|
onOptionSelect={(_, data) => {
|
||||||
}
|
if (!data.optionValue) {
|
||||||
}}
|
setCityId(undefined);
|
||||||
>
|
return;
|
||||||
{cities && Array.isArray(cities) && cities.map((option) => (
|
}
|
||||||
<Option key={option.id} text={option.name} value={option.id}>
|
setCityId(Number(data.optionValue));
|
||||||
{option.name}
|
}}
|
||||||
</Option>
|
>
|
||||||
))}
|
{cities && Array.isArray(cities) && cities.map((option) => (
|
||||||
</Combobox>}
|
<Option key={`region-${option.id}`} text={option.name} value={option.id}>
|
||||||
|
{option.name}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
@ -102,10 +179,6 @@ function Boilers() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: '1rem'
|
gap: '1rem'
|
||||||
}}>
|
}}>
|
||||||
<Text size={400} weight='medium'>
|
|
||||||
{cityId && cities && Array.isArray(cities) && cities.find(city => city.id === cityId).name}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{cityId &&
|
{cityId &&
|
||||||
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', width: '100%', gap: '1rem', justifyContent: 'space-between' }}>
|
||||||
<BoilersCard title='Всего объектов' value={boilers && Array.isArray(boilers) ? boilers.length.toString() : ''} subtitle='' />
|
<BoilersCard title='Всего объектов' value={boilers && Array.isArray(boilers) ? boilers.length.toString() : ''} subtitle='' />
|
||||||
@ -119,80 +192,48 @@ function Boilers() {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AgGridReact
|
{boilersLoading ?
|
||||||
// rowData={[
|
<Spinner />
|
||||||
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test1' }), {}),
|
:
|
||||||
// Object.keys(table.headers).reduce((obj, key) => ({ ...obj, [key]: 'test' }), {})
|
<AgGridReact
|
||||||
// ]}
|
key={`boilers-${cityId}`}
|
||||||
key={`boilers-${cityId}`}
|
loading={boilersLoading}
|
||||||
loading={boilersLoading}
|
overlayLoadingTemplate='Загрузка...'
|
||||||
overlayLoadingTemplate='Загрузка...'
|
overlayNoRowsTemplate='Нет данных'
|
||||||
overlayNoRowsTemplate='Нет данных'
|
rowData={boilers}
|
||||||
rowData={boilers}
|
columnDefs={[
|
||||||
columnDefs={[
|
{
|
||||||
{
|
field: 'boiler_name',
|
||||||
field: 'boiler_name',
|
headerName: 'Наименование'
|
||||||
headerName: 'Наименование'
|
},
|
||||||
},
|
{
|
||||||
{
|
field: 'boiler_code',
|
||||||
field: 'boiler_code',
|
headerName: 'Идент. код'
|
||||||
headerName: 'Идент. код'
|
},
|
||||||
},
|
{
|
||||||
{
|
field: 'id_fuels',
|
||||||
field: 'id_fuels',
|
headerName: 'Вид топлива',
|
||||||
headerName: 'Вид топлива',
|
//editable: true,
|
||||||
//editable: true,
|
//cellEditor: FuelTypeEditor,
|
||||||
//cellEditor: FuelTypeEditor,
|
autoHeight: true,
|
||||||
autoHeight: true,
|
cellRenderer: FuelRenderer,
|
||||||
cellRenderer: FuelRenderer,
|
cellStyle: (_) => {
|
||||||
cellStyle: (_) => {
|
return { padding: '1px' }
|
||||||
return { padding: '1px' }
|
}
|
||||||
|
//enableCellChangeFlash: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'activity',
|
||||||
|
headerName: 'Активный',
|
||||||
|
cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
|
||||||
}
|
}
|
||||||
//enableCellChangeFlash: true
|
]}
|
||||||
},
|
defaultColDef={{
|
||||||
{
|
flex: 1,
|
||||||
field: 'activity',
|
}}
|
||||||
headerName: 'Активный',
|
/>}
|
||||||
cellRenderer: (params: CustomCellRendererProps) => (<span>{params.value === true ? 'Да' : 'Нет'}</span>)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
defaultColDef={{
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</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
|
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'>
|
<Table style={{ width: '100%' }} size='small'>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.isArray(params.value) && params.value.map(fuel => (
|
{Array.isArray(params.value) && params.value.map(fuel => (
|
||||||
<TableRow>
|
<TableRow key={`${fuel.id}`}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<TableCellLayout truncate>
|
<TableCellLayout truncate>
|
||||||
{fuel.name}
|
{fuel.name}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ const FuelsPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((option) => (
|
{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.name}
|
||||||
</Option>
|
</Option>
|
||||||
))}
|
))}
|
||||||
@ -131,7 +131,7 @@ const FuelsPage = () => {
|
|||||||
setSelectedIdFuels(Number(data.value))
|
setSelectedIdFuels(Number(data.value))
|
||||||
}}>
|
}}>
|
||||||
{fuelTypes && Array.isArray(fuelTypes) && fuelTypes.map((ft: FuelType) => (
|
{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}
|
{ft.name}
|
||||||
</Tab>
|
</Tab>
|
||||||
))
|
))
|
||||||
|
|||||||
@ -1,7 +1,391 @@
|
|||||||
const LimitsPage = () => {
|
import { useEffect, useState } from 'react'
|
||||||
return (
|
import { Button, CompoundButton, Dropdown, Option, Spinner, Text } from '@fluentui/react-components'
|
||||||
<div>LimitsPage</div>
|
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
|
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 { 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 {
|
export class CreateLimitDto {
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
@ -10,15 +19,13 @@ export class CreateLimitDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
id_fuel: number
|
id_fuel: number
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
@IsNumber()
|
|
||||||
month: Date
|
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
year: number
|
year: number
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsNumber()
|
@IsArray()
|
||||||
value: number
|
@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 { FuelTransferDto } from './dto/transfer'
|
||||||
import { GetBoilersDTO } from './dto/get-boilers'
|
import { GetBoilersDTO } from './dto/get-boilers'
|
||||||
import { GetFuelsDTO } from './dto/get-fuels'
|
import { GetFuelsDTO } from './dto/get-fuels'
|
||||||
|
import { GetLimitsDTO } from './dto/get-limits'
|
||||||
|
import { GetCitySettingsDTO } from './dto/get-city-settings'
|
||||||
|
|
||||||
@Controller('fuel')
|
@Controller('fuel')
|
||||||
export class FuelController {
|
export class FuelController {
|
||||||
@ -43,8 +45,8 @@ export class FuelController {
|
|||||||
type: FuelLimitDto,
|
type: FuelLimitDto,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
})
|
})
|
||||||
async getBoilersFuelsLimits(): Promise<FuelLimitDto[]> {
|
async getBoilersFuelsLimits(@Query() getLimitsDTO: GetLimitsDTO): Promise<FuelLimitDto[]> {
|
||||||
return this.fuelService.getBoilersFuelsLimits()
|
return this.fuelService.getBoilersFuelsLimits(getLimitsDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/limits')
|
@Post('/limits')
|
||||||
@ -52,6 +54,16 @@ export class FuelController {
|
|||||||
return this.fuelService.addBoilersFuelsLimit(createLimitDto)
|
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
|
// Fuel expenses
|
||||||
@Get('/expenses')
|
@Get('/expenses')
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { InjectDataSource } from '@nestjs/typeorm';
|
import { InjectDataSource } from '@nestjs/typeorm';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { CreateExpenseDto } from './dto/create-expense';
|
import { CreateExpenseDto } from './dto/create-expense';
|
||||||
import { CreateLimitDto } from './dto/create-limit';
|
import { CreateLimitDto } from './dto/create-limit';
|
||||||
import { CreateTransferDto } from './dto/create-transfer';
|
import { CreateTransferDto } from './dto/create-transfer';
|
||||||
import { GetFuelsDTO } from './dto/get-fuels';
|
import { GetFuelsDTO } from './dto/get-fuels';
|
||||||
|
import { GetLimitsDTO } from './dto/get-limits';
|
||||||
|
import { GetCitySettingsDTO } from './dto/get-city-settings';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FuelService {
|
export class FuelService {
|
||||||
@ -56,13 +58,12 @@ export class FuelService {
|
|||||||
const result = await this.wsDataSource.query(`
|
const result = await this.wsDataSource.query(`
|
||||||
SELECT
|
SELECT
|
||||||
b.*,
|
b.*,
|
||||||
COALESCE(
|
(
|
||||||
(SELECT fp.*
|
SELECT fp.*
|
||||||
FROM isFuels.dbo.BoilersFuels bf
|
FROM isFuels.dbo.BoilersFuels bf
|
||||||
INNER JOIN isWorldstone.dbo.dFuelsParameters fp ON bf.id_fuels = fp.id
|
INNER JOIN isWorldstone.dbo.dFuelsParameters fp ON bf.id_fuels = fp.id
|
||||||
WHERE bf.id_boilers = b.id_object
|
WHERE bf.id_boilers = b.id_object
|
||||||
FOR JSON PATH),
|
FOR JSON PATH
|
||||||
'[]'
|
|
||||||
) AS id_fuels
|
) AS id_fuels
|
||||||
FROM isWorldstone.dbo.vBoilers b
|
FROM isWorldstone.dbo.vBoilers b
|
||||||
WHERE id_city = @0
|
WHERE id_city = @0
|
||||||
@ -90,11 +91,118 @@ export class FuelService {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBoilersFuelsLimits(): Promise<any[]> {
|
async getCitySettings(getCitySettings: GetCitySettingsDTO): Promise<any[]> {
|
||||||
|
const { city_id } = getCitySettings
|
||||||
|
|
||||||
const result = await this.wsDataSource.query(`
|
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 *
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,10 +222,14 @@ export class FuelService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> {
|
async addBoilersFuelsLimit(createLimitDto: CreateLimitDto): Promise<any[]> {
|
||||||
const result = await this.dataSource.query(`
|
const logger = new Logger('INFO'); // 'MyContext' is an optional string for log context
|
||||||
INSERT INTO dbo.BoilersFuelsLimits (id_boiler, id_fuel, value, month, year) VALUES ($1, $2, $3, $4, $5)
|
logger.log(createLimitDto)
|
||||||
`, [createLimitDto.id_boiler, createLimitDto.id_fuel, createLimitDto.value, createLimitDto.month, createLimitDto.year])
|
|
||||||
return result
|
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[]> {
|
async addFuelsTransfer(createTransferDto: CreateTransferDto): Promise<any[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user