@mxpicture/tesla-superchargers-mate 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/20260913164819_additional_costs.sql +61 -0
- package/db/schema.sql +52 -0
- package/dist/store/CarsStore.d.ts +20 -0
- package/dist/store/CarsStore.d.ts.map +1 -0
- package/dist/store/CarsStore.js +20 -0
- package/dist/store/CarsStore.js.map +1 -0
- package/dist/store/PicAdditionalCostsStore.d.ts +60 -0
- package/dist/store/PicAdditionalCostsStore.d.ts.map +1 -0
- package/dist/store/PicAdditionalCostsStore.js +71 -0
- package/dist/store/PicAdditionalCostsStore.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -1
- package/package.json +2 -2
- package/sql/reset.sql +7 -0
- package/sql/test/history1.sql +55 -0
- package/sql/test/history2.sql +181 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- pic_additional_costs: costs TeslaMate never sees -- service, repairs, tyres,
|
|
2
|
+
-- insurance, tax, subsidies -- entered by hand and attributed to a car and a day.
|
|
3
|
+
--
|
|
4
|
+
-- The first pic_* table whose rows are not derivable: everything else here is a
|
|
5
|
+
-- cache of a feed or of an algorithm and can be rebuilt, these can only be
|
|
6
|
+
-- retyped. No upsert path, no sync, no soft delete.
|
|
7
|
+
--
|
|
8
|
+
-- A recurring cost is ONE row: recurrence = 'monthly' with date as the first
|
|
9
|
+
-- occurrence, end_date as the last (NULL = still running) and cost as the amount
|
|
10
|
+
-- PER MONTH. The future Grafana view expands it with generate_series; nothing
|
|
11
|
+
-- materializes the occurrences.
|
|
12
|
+
-- migrate:up
|
|
13
|
+
CREATE TYPE pic_cost_category AS ENUM(
|
|
14
|
+
'service',
|
|
15
|
+
'repair',
|
|
16
|
+
'tyres',
|
|
17
|
+
'insurance',
|
|
18
|
+
'tax',
|
|
19
|
+
'funding', -- subsidies and quota credits; normally a NEGATIVE cost
|
|
20
|
+
'other' -- escape hatch, so an unforeseen cost never needs a migration
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TYPE pic_cost_recurrence AS ENUM('once', 'monthly');
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS pic_additional_costs (
|
|
26
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
27
|
+
-- SMALLINT and no FK, like pic_refs.car_id: a REFERENCES cars (id) would put
|
|
28
|
+
-- constraint triggers on a TeslaMate table, which this project never writes to.
|
|
29
|
+
car_id SMALLINT NOT NULL,
|
|
30
|
+
category pic_cost_category NOT NULL,
|
|
31
|
+
-- A calendar day, not an instant; for a recurring row, the first occurrence.
|
|
32
|
+
-- Read and written as YYYY-MM-DD text by the store -- see the plan for why.
|
|
33
|
+
date DATE NOT NULL,
|
|
34
|
+
description TEXT NOT NULL,
|
|
35
|
+
-- EUR, like every stored cost here. Negative is allowed: a refund or a
|
|
36
|
+
-- 'funding' credit. On a recurring row this is the amount per occurrence.
|
|
37
|
+
cost NUMERIC(10, 2) NOT NULL,
|
|
38
|
+
recurrence pic_cost_recurrence NOT NULL DEFAULT 'once',
|
|
39
|
+
-- last occurrence of a monthly row; NULL = still running
|
|
40
|
+
end_date DATE,
|
|
41
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
42
|
+
CONSTRAINT pic_additional_costs_description_ck CHECK (btrim(description) <> ''),
|
|
43
|
+
CONSTRAINT pic_additional_costs_end_date_ck CHECK (
|
|
44
|
+
end_date IS NULL
|
|
45
|
+
OR (
|
|
46
|
+
recurrence = 'monthly'
|
|
47
|
+
AND end_date >= date
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- The predicate a per-car cost-over-time panel will filter and order on.
|
|
53
|
+
-- NOT unique: several entries per car and day are explicitly allowed.
|
|
54
|
+
CREATE INDEX IF NOT EXISTS pic_additional_costs_car_date_idx ON pic_additional_costs (car_id, date);
|
|
55
|
+
|
|
56
|
+
-- migrate:down
|
|
57
|
+
DROP TABLE IF EXISTS pic_additional_costs;
|
|
58
|
+
|
|
59
|
+
DROP TYPE IF EXISTS pic_cost_recurrence;
|
|
60
|
+
|
|
61
|
+
DROP TYPE IF EXISTS pic_cost_category;
|
package/db/schema.sql
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
CREATE TYPE public.pic_address_type AS ENUM ('geofence', 'cluster', 'address', 'position');
|
|
13
13
|
|
|
14
|
+
CREATE TYPE public.pic_cost_category AS ENUM ('service', 'repair', 'tyres', 'insurance', 'tax', 'funding', 'other');
|
|
15
|
+
|
|
16
|
+
CREATE TYPE public.pic_cost_recurrence AS ENUM ('once', 'monthly');
|
|
17
|
+
|
|
14
18
|
CREATE TYPE public.pic_ref_type AS ENUM ('drive', 'charge', 'idle');
|
|
15
19
|
|
|
16
20
|
--
|
|
@@ -35,6 +39,39 @@ SET default_tablespace = '';
|
|
|
35
39
|
|
|
36
40
|
SET default_table_access_method = heap;
|
|
37
41
|
|
|
42
|
+
--
|
|
43
|
+
-- Name: pic_additional_costs; Type: TABLE; Schema: public; Owner: -
|
|
44
|
+
--
|
|
45
|
+
|
|
46
|
+
CREATE TABLE public.pic_additional_costs (
|
|
47
|
+
id bigint NOT NULL,
|
|
48
|
+
car_id smallint NOT NULL,
|
|
49
|
+
category public.pic_cost_category NOT NULL,
|
|
50
|
+
date date NOT NULL,
|
|
51
|
+
description text NOT NULL,
|
|
52
|
+
cost numeric(10,2) NOT NULL,
|
|
53
|
+
recurrence public.pic_cost_recurrence DEFAULT 'once'::public.pic_cost_recurrence NOT NULL,
|
|
54
|
+
end_date date,
|
|
55
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
56
|
+
CONSTRAINT pic_additional_costs_description_ck CHECK ((btrim(description) <> ''::text)),
|
|
57
|
+
CONSTRAINT pic_additional_costs_end_date_ck CHECK (((end_date IS NULL) OR ((recurrence = 'monthly'::public.pic_cost_recurrence) AND (end_date >= date))))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
--
|
|
62
|
+
-- Name: pic_additional_costs_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
|
63
|
+
--
|
|
64
|
+
|
|
65
|
+
ALTER TABLE public.pic_additional_costs ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
|
|
66
|
+
SEQUENCE NAME public.pic_additional_costs_id_seq
|
|
67
|
+
START WITH 1
|
|
68
|
+
INCREMENT BY 1
|
|
69
|
+
NO MINVALUE
|
|
70
|
+
NO MAXVALUE
|
|
71
|
+
CACHE 1
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
|
|
38
75
|
--
|
|
39
76
|
-- Name: pic_zip_country; Type: TABLE; Schema: public; Owner: -
|
|
40
77
|
--
|
|
@@ -536,6 +573,14 @@ CREATE UNLOGGED TABLE public.pic_zip_import (
|
|
|
536
573
|
);
|
|
537
574
|
|
|
538
575
|
|
|
576
|
+
--
|
|
577
|
+
-- Name: pic_additional_costs pic_additional_costs_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
|
578
|
+
--
|
|
579
|
+
|
|
580
|
+
ALTER TABLE ONLY public.pic_additional_costs
|
|
581
|
+
ADD CONSTRAINT pic_additional_costs_pkey PRIMARY KEY (id);
|
|
582
|
+
|
|
583
|
+
|
|
539
584
|
--
|
|
540
585
|
-- Name: pic_chargers pic_chargers_location_guid_key; Type: CONSTRAINT; Schema: public; Owner: -
|
|
541
586
|
--
|
|
@@ -680,6 +725,13 @@ ALTER TABLE ONLY public.pic_zip_country
|
|
|
680
725
|
ADD CONSTRAINT pic_zip_country_pkey PRIMARY KEY (country_code);
|
|
681
726
|
|
|
682
727
|
|
|
728
|
+
--
|
|
729
|
+
-- Name: pic_additional_costs_car_date_idx; Type: INDEX; Schema: public; Owner: -
|
|
730
|
+
--
|
|
731
|
+
|
|
732
|
+
CREATE INDEX pic_additional_costs_car_date_idx ON public.pic_additional_costs USING btree (car_id, date);
|
|
733
|
+
|
|
734
|
+
|
|
683
735
|
--
|
|
684
736
|
-- Name: pic_charg_proc_link_uq_idx; Type: INDEX; Schema: public; Owner: -
|
|
685
737
|
--
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Cars, type CarsId, type CarsInitializer, type CarsMutator } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { IMateStore } from "./IMateStore.js";
|
|
3
|
+
import { type SqlTx } from "./db.js";
|
|
4
|
+
/** A car as a picker renders it; `name` is nullable in TeslaMate's own table. */
|
|
5
|
+
export interface CarOption {
|
|
6
|
+
id: CarsId;
|
|
7
|
+
name: string | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read access to TeslaMate's `cars` table. Only {@link CarsStore.queryNames}
|
|
11
|
+
* today: the additional-costs form needs a car dropdown, and with no FK on
|
|
12
|
+
* `pic_additional_costs.car_id` a free-text number would book a cost against a
|
|
13
|
+
* car that may not exist. Read-only, like every TeslaMate table here.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CarsStore extends IMateStore<CarsId, Cars, CarsInitializer, CarsMutator> {
|
|
16
|
+
constructor();
|
|
17
|
+
queryNames(tx?: SqlTx): Promise<CarOption[]>;
|
|
18
|
+
}
|
|
19
|
+
export declare const carsStore: () => CarsStore;
|
|
20
|
+
//# sourceMappingURL=CarsStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CarsStore.d.ts","sourceRoot":"","sources":["../../src/store/CarsStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,IAAI,EACT,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,WAAW,EACjB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAO,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,UAAU,CACvC,MAAM,EACN,IAAI,EACJ,eAAe,EACf,WAAW,CACZ;;IAKc,UAAU,CAAC,EAAE,GAAE,KAAa,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAGjE;AAGD,eAAO,MAAM,SAAS,iBAAqC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { carsInitializer, carsMutator, } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { IMateStore } from "./IMateStore.js";
|
|
3
|
+
import { sql } from "./db.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read access to TeslaMate's `cars` table. Only {@link CarsStore.queryNames}
|
|
6
|
+
* today: the additional-costs form needs a car dropdown, and with no FK on
|
|
7
|
+
* `pic_additional_costs.car_id` a free-text number would book a cost against a
|
|
8
|
+
* car that may not exist. Read-only, like every TeslaMate table here.
|
|
9
|
+
*/
|
|
10
|
+
export class CarsStore extends IMateStore {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("cars", carsInitializer, carsMutator);
|
|
13
|
+
}
|
|
14
|
+
async queryNames(tx = sql()) {
|
|
15
|
+
return await tx `SELECT id, name FROM cars ORDER BY id`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
let _store = null;
|
|
19
|
+
export const carsStore = () => (_store ??= new CarsStore());
|
|
20
|
+
//# sourceMappingURL=CarsStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CarsStore.js","sourceRoot":"","sources":["../../src/store/CarsStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,WAAW,GAKZ,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAc,MAAM,SAAS,CAAC;AAQ1C;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,UAK9B;IACC;QACE,KAAK,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC;IAC9C,CAAC;IAEM,KAAK,CAAC,UAAU,CAAC,KAAY,GAAG,EAAE;QACvC,OAAO,MAAM,EAAE,CAAa,uCAAuC,CAAC;IACtE,CAAC;CACF;AAED,IAAI,MAAM,GAAqB,IAAI,CAAC;AACpC,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { PicAdditionalCostsId, PicCostCategory, PicCostRecurrence } from "@mxpicture/tesla-superchargers-generated/public";
|
|
2
|
+
import { type SqlTx } from "./db.js";
|
|
3
|
+
/**
|
|
4
|
+
* One stored entry. Both dates are `YYYY-MM-DD` and `cost` a NUMERIC string,
|
|
5
|
+
* exactly as they leave Postgres.
|
|
6
|
+
*/
|
|
7
|
+
export interface AdditionalCost {
|
|
8
|
+
id: PicAdditionalCostsId;
|
|
9
|
+
car_id: number;
|
|
10
|
+
category: PicCostCategory;
|
|
11
|
+
date: string;
|
|
12
|
+
description: string;
|
|
13
|
+
cost: string;
|
|
14
|
+
recurrence: PicCostRecurrence;
|
|
15
|
+
end_date: string | null;
|
|
16
|
+
created_at: Date;
|
|
17
|
+
}
|
|
18
|
+
/** A full row as the UI edits it; `id` and `created_at` are the store's. */
|
|
19
|
+
export interface AdditionalCostInput {
|
|
20
|
+
car_id: number;
|
|
21
|
+
category: PicCostCategory;
|
|
22
|
+
/** YYYY-MM-DD */
|
|
23
|
+
date: string;
|
|
24
|
+
description: string;
|
|
25
|
+
cost: string;
|
|
26
|
+
recurrence: PicCostRecurrence;
|
|
27
|
+
/** YYYY-MM-DD, or null on a one-off row and an open-ended monthly one */
|
|
28
|
+
end_date: string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `pic_additional_costs` — the costs TeslaMate never sees, typed in through
|
|
32
|
+
* the web UI. Full CRUD, because nothing else creates these rows: they are the
|
|
33
|
+
* only `pic_*` rows that cannot be rebuilt from a feed or an algorithm.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately NOT an {@link IMateStore}, for the same reason
|
|
36
|
+
* {@link PicZipCodesStore} is not: the base class reads with `SELECT *` and
|
|
37
|
+
* writes through the generated zod schemas, and both put a JS `Date` on the
|
|
38
|
+
* path of a `DATE` column. postgres.js parses `DATE` (OID 1082) into a `Date`
|
|
39
|
+
* at *local* midnight, which `JSON.stringify` then emits as the previous day
|
|
40
|
+
* anywhere east of UTC; writing one back casts a timestamptz to `date` in the
|
|
41
|
+
* *session* zone, which is not the process's. So this store is the boundary
|
|
42
|
+
* and it speaks `YYYY-MM-DD` strings: every read projects `date::TEXT`, every
|
|
43
|
+
* write binds `${…}::DATE`, and no JS `Date` ever exists. Overriding all four
|
|
44
|
+
* inherited methods to do that would leave nothing inherited.
|
|
45
|
+
*/
|
|
46
|
+
export declare class PicAdditionalCostsStore {
|
|
47
|
+
readonly table = "pic_additional_costs";
|
|
48
|
+
/** Newest day first; ties broken by insertion order, newest first. */
|
|
49
|
+
query(tx?: SqlTx): Promise<AdditionalCost[]>;
|
|
50
|
+
insert(input: AdditionalCostInput, tx?: SqlTx): Promise<AdditionalCost>;
|
|
51
|
+
/**
|
|
52
|
+
* Replace every editable column of one row — the UI edits a row as a whole,
|
|
53
|
+
* so there is no patch path. Null when no row has that id.
|
|
54
|
+
*/
|
|
55
|
+
update(id: PicAdditionalCostsId, input: AdditionalCostInput, tx?: SqlTx): Promise<AdditionalCost | null>;
|
|
56
|
+
/** False when the row was already gone — deleting twice is not an error. */
|
|
57
|
+
remove(id: PicAdditionalCostsId, tx?: SqlTx): Promise<boolean>;
|
|
58
|
+
}
|
|
59
|
+
export declare const picAdditionalCostsStore: () => PicAdditionalCostsStore;
|
|
60
|
+
//# sourceMappingURL=PicAdditionalCostsStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PicAdditionalCostsStore.d.ts","sourceRoot":"","sources":["../../src/store/PicAdditionalCostsStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EAClB,MAAM,iDAAiD,CAAC;AACzD,OAAO,EAAO,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAE1C;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,oBAAoB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,CAAC;IAC1B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,yEAAyE;IACzE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAuB;IAClC,SAAgB,KAAK,0BAA0B;IAE/C,sEAAsE;IACzD,KAAK,CAAC,EAAE,GAAE,KAAa,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IASnD,MAAM,CACjB,KAAK,EAAE,mBAAmB,EAC1B,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,cAAc,CAAC;IAe1B;;;OAGG;IACU,MAAM,CACjB,EAAE,EAAE,oBAAoB,EACxB,KAAK,EAAE,mBAAmB,EAC1B,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAiBjC,4EAA4E;IAC/D,MAAM,CACjB,EAAE,EAAE,oBAAoB,EACxB,EAAE,GAAE,KAAa,GAChB,OAAO,CAAC,OAAO,CAAC;CAIpB;AAGD,eAAO,MAAM,uBAAuB,+BACQ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sql } from "./db.js";
|
|
2
|
+
/**
|
|
3
|
+
* `pic_additional_costs` — the costs TeslaMate never sees, typed in through
|
|
4
|
+
* the web UI. Full CRUD, because nothing else creates these rows: they are the
|
|
5
|
+
* only `pic_*` rows that cannot be rebuilt from a feed or an algorithm.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately NOT an {@link IMateStore}, for the same reason
|
|
8
|
+
* {@link PicZipCodesStore} is not: the base class reads with `SELECT *` and
|
|
9
|
+
* writes through the generated zod schemas, and both put a JS `Date` on the
|
|
10
|
+
* path of a `DATE` column. postgres.js parses `DATE` (OID 1082) into a `Date`
|
|
11
|
+
* at *local* midnight, which `JSON.stringify` then emits as the previous day
|
|
12
|
+
* anywhere east of UTC; writing one back casts a timestamptz to `date` in the
|
|
13
|
+
* *session* zone, which is not the process's. So this store is the boundary
|
|
14
|
+
* and it speaks `YYYY-MM-DD` strings: every read projects `date::TEXT`, every
|
|
15
|
+
* write binds `${…}::DATE`, and no JS `Date` ever exists. Overriding all four
|
|
16
|
+
* inherited methods to do that would leave nothing inherited.
|
|
17
|
+
*/
|
|
18
|
+
export class PicAdditionalCostsStore {
|
|
19
|
+
table = "pic_additional_costs";
|
|
20
|
+
/** Newest day first; ties broken by insertion order, newest first. */
|
|
21
|
+
async query(tx = sql()) {
|
|
22
|
+
return await tx `
|
|
23
|
+
SELECT
|
|
24
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
25
|
+
recurrence, end_date::TEXT AS end_date, created_at
|
|
26
|
+
FROM pic_additional_costs
|
|
27
|
+
ORDER BY date DESC, id DESC`;
|
|
28
|
+
}
|
|
29
|
+
async insert(input, tx = sql()) {
|
|
30
|
+
const [row] = await tx `
|
|
31
|
+
INSERT INTO pic_additional_costs (
|
|
32
|
+
car_id, category, date, description, cost, recurrence, end_date
|
|
33
|
+
) VALUES (
|
|
34
|
+
${input.car_id}, ${input.category}, ${input.date}::DATE,
|
|
35
|
+
${input.description}, ${input.cost}, ${input.recurrence},
|
|
36
|
+
${input.end_date}::DATE
|
|
37
|
+
)
|
|
38
|
+
RETURNING
|
|
39
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
40
|
+
recurrence, end_date::TEXT AS end_date, created_at`;
|
|
41
|
+
return row;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Replace every editable column of one row — the UI edits a row as a whole,
|
|
45
|
+
* so there is no patch path. Null when no row has that id.
|
|
46
|
+
*/
|
|
47
|
+
async update(id, input, tx = sql()) {
|
|
48
|
+
const rows = await tx `
|
|
49
|
+
UPDATE pic_additional_costs SET
|
|
50
|
+
car_id = ${input.car_id},
|
|
51
|
+
category = ${input.category},
|
|
52
|
+
date = ${input.date}::DATE,
|
|
53
|
+
description = ${input.description},
|
|
54
|
+
cost = ${input.cost},
|
|
55
|
+
recurrence = ${input.recurrence},
|
|
56
|
+
end_date = ${input.end_date}::DATE
|
|
57
|
+
WHERE id = ${id}
|
|
58
|
+
RETURNING
|
|
59
|
+
id, car_id, category, date::TEXT AS date, description, cost,
|
|
60
|
+
recurrence, end_date::TEXT AS end_date, created_at`;
|
|
61
|
+
return rows[0] ?? null;
|
|
62
|
+
}
|
|
63
|
+
/** False when the row was already gone — deleting twice is not an error. */
|
|
64
|
+
async remove(id, tx = sql()) {
|
|
65
|
+
const result = await tx `DELETE FROM pic_additional_costs WHERE id = ${id}`;
|
|
66
|
+
return result.count > 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let _store = null;
|
|
70
|
+
export const picAdditionalCostsStore = () => (_store ??= new PicAdditionalCostsStore());
|
|
71
|
+
//# sourceMappingURL=PicAdditionalCostsStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PicAdditionalCostsStore.js","sourceRoot":"","sources":["../../src/store/PicAdditionalCostsStore.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,GAAG,EAAc,MAAM,SAAS,CAAC;AA+B1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,uBAAuB;IAClB,KAAK,GAAG,sBAAsB,CAAC;IAE/C,sEAAsE;IAC/D,KAAK,CAAC,KAAK,CAAC,KAAY,GAAG,EAAE;QAClC,OAAO,MAAM,EAAE,CAAkB;;;;;kCAKH,CAAC;IACjC,CAAC;IAEM,KAAK,CAAC,MAAM,CACjB,KAA0B,EAC1B,KAAY,GAAG,EAAE;QAEjB,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAAkB;;;;UAIlC,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,QAAQ,KAAK,KAAK,CAAC,IAAI;UAC9C,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU;UACrD,KAAK,CAAC,QAAQ;;;;2DAImC,CAAC;QACxD,OAAO,GAAG,CAAC;IACb,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,MAAM,CACjB,EAAwB,EACxB,KAA0B,EAC1B,KAAY,GAAG,EAAE;QAEjB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAkB;;mBAExB,KAAK,CAAC,MAAM;qBACV,KAAK,CAAC,QAAQ;iBAClB,KAAK,CAAC,IAAI;wBACH,KAAK,CAAC,WAAW;iBACxB,KAAK,CAAC,IAAI;uBACJ,KAAK,CAAC,UAAU;qBAClB,KAAK,CAAC,QAAQ;mBAChB,EAAE;;;2DAGsC,CAAC;QACxD,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACzB,CAAC;IAED,4EAA4E;IACrE,KAAK,CAAC,MAAM,CACjB,EAAwB,EACxB,KAAY,GAAG,EAAE;QAEjB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAA,+CAA+C,EAAE,EAAE,CAAC;QAC3E,OAAO,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;IAC1B,CAAC;CACF;AAED,IAAI,MAAM,GAAmC,IAAI,CAAC;AAClD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,EAAE,CAC1C,CAAC,MAAM,KAAK,IAAI,uBAAuB,EAAE,CAAC,CAAC"}
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
export * from "./CarsStore.js";
|
|
1
2
|
export * from "./ChargingProcessesStore.js";
|
|
2
3
|
export * from "./GeofencesStore.js";
|
|
3
4
|
export * from "./IFailableStore.js";
|
|
4
5
|
export * from "./IMateStore.js";
|
|
6
|
+
export * from "./PicAdditionalCostsStore.js";
|
|
5
7
|
export * from "./PicCPLinksStore.js";
|
|
6
8
|
export * from "./PicChargersStore.js";
|
|
7
9
|
export * from "./PicInvoiceFilesStore.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AACA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
package/dist/store/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// This file is auto-generated by build-cli. Do not edit manually.
|
|
2
|
+
export * from "./CarsStore.js";
|
|
2
3
|
export * from "./ChargingProcessesStore.js";
|
|
3
4
|
export * from "./GeofencesStore.js";
|
|
4
5
|
export * from "./IFailableStore.js";
|
|
5
6
|
export * from "./IMateStore.js";
|
|
7
|
+
export * from "./PicAdditionalCostsStore.js";
|
|
6
8
|
export * from "./PicCPLinksStore.js";
|
|
7
9
|
export * from "./PicChargersStore.js";
|
|
8
10
|
export * from "./PicInvoiceFilesStore.js";
|
package/dist/store/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mxpicture/tesla-superchargers-mate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.65",
|
|
4
4
|
"description": "TeslaMate integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "MXPicture",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@mxpicture/tesla-superchargers-generated": "^0.1.
|
|
53
|
+
"@mxpicture/tesla-superchargers-generated": "^0.1.65",
|
|
54
54
|
"dbmate": "2.35.1",
|
|
55
55
|
"postgres": "^3.4.9",
|
|
56
56
|
"short-uuid": "^6.0.3",
|
package/sql/reset.sql
CHANGED
|
@@ -115,6 +115,13 @@ DROP PROCEDURE IF EXISTS pic_build_refs_car (SMALLINT);
|
|
|
115
115
|
|
|
116
116
|
DROP PROCEDURE IF EXISTS pic_build_costs_car (SMALLINT);
|
|
117
117
|
|
|
118
|
+
DROP TABLE IF EXISTS pic_additional_costs;
|
|
119
|
+
|
|
120
|
+
-- After the table: both enums are only ever used by its columns.
|
|
121
|
+
DROP TYPE IF EXISTS pic_cost_recurrence;
|
|
122
|
+
|
|
123
|
+
DROP TYPE IF EXISTS pic_cost_category;
|
|
124
|
+
|
|
118
125
|
DROP TABLE IF EXISTS pic_refs;
|
|
119
126
|
|
|
120
127
|
DROP SEQUENCE IF EXISTS pic_refs_idle_id;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
WITH
|
|
2
|
+
base AS (
|
|
3
|
+
SELECT
|
|
4
|
+
r.car_id,
|
|
5
|
+
r.ref_type,
|
|
6
|
+
sum(
|
|
7
|
+
EXTRACT(
|
|
8
|
+
EPOCH
|
|
9
|
+
FROM
|
|
10
|
+
r.end_date - r.start_date
|
|
11
|
+
)
|
|
12
|
+
) AS duration_sec,
|
|
13
|
+
sum(r.real_km) AS real_km,
|
|
14
|
+
sum(r.start_range_km - r.end_range_km) AS dropped_km,
|
|
15
|
+
sum(r.cost) AS cost
|
|
16
|
+
FROM
|
|
17
|
+
pic_refs r
|
|
18
|
+
WHERE
|
|
19
|
+
r.car_id = 1
|
|
20
|
+
-- AND $__timeFilter(r.start_date)
|
|
21
|
+
GROUP BY
|
|
22
|
+
r.car_id,
|
|
23
|
+
r.ref_type
|
|
24
|
+
)
|
|
25
|
+
SELECT
|
|
26
|
+
r.*,
|
|
27
|
+
(
|
|
28
|
+
round(
|
|
29
|
+
(r.dropped_km * c.efficiency)::NUMERIC / NULLIF(r.duration_sec / 3600.0, (0)::NUMERIC),
|
|
30
|
+
3
|
|
31
|
+
)
|
|
32
|
+
) AS mean_draw_kw
|
|
33
|
+
-- kwh_per_real_1km
|
|
34
|
+
-- cost_per_real_100km
|
|
35
|
+
-- cost_per_kwh
|
|
36
|
+
-- floor(
|
|
37
|
+
-- extract(
|
|
38
|
+
-- epoch
|
|
39
|
+
-- FROM
|
|
40
|
+
-- r.start_date
|
|
41
|
+
-- )
|
|
42
|
+
-- ) * 1000 AS start_date_ts,
|
|
43
|
+
-- ceil(
|
|
44
|
+
-- extract(
|
|
45
|
+
-- epoch
|
|
46
|
+
-- FROM
|
|
47
|
+
-- r.end_date
|
|
48
|
+
-- )
|
|
49
|
+
-- ) * 1000 AS end_date_ts
|
|
50
|
+
FROM
|
|
51
|
+
base r
|
|
52
|
+
INNER JOIN cars c ON c.id = r.car_id;
|
|
53
|
+
|
|
54
|
+
-- ORDER BY
|
|
55
|
+
-- r.start_date DESC;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
WITH
|
|
2
|
+
base AS (
|
|
3
|
+
SELECT
|
|
4
|
+
*
|
|
5
|
+
FROM
|
|
6
|
+
pic_refs_v
|
|
7
|
+
WHERE
|
|
8
|
+
car_id = 1
|
|
9
|
+
-- car_id = $car_id
|
|
10
|
+
-- AND $__timeFilter(start_date)
|
|
11
|
+
),
|
|
12
|
+
latest_base AS (
|
|
13
|
+
SELECT
|
|
14
|
+
*
|
|
15
|
+
FROM
|
|
16
|
+
pic_refs_v b
|
|
17
|
+
WHERE
|
|
18
|
+
car_id = 1
|
|
19
|
+
-- car_id = $car_id
|
|
20
|
+
ORDER BY
|
|
21
|
+
start_date DESC,
|
|
22
|
+
end_date DESC
|
|
23
|
+
LIMIT
|
|
24
|
+
1
|
|
25
|
+
),
|
|
26
|
+
latest_selected AS (
|
|
27
|
+
SELECT
|
|
28
|
+
car_id,
|
|
29
|
+
ref_id,
|
|
30
|
+
ref_type
|
|
31
|
+
FROM
|
|
32
|
+
pic_refs_v b
|
|
33
|
+
WHERE
|
|
34
|
+
car_id = 1
|
|
35
|
+
-- car_id = $car_id
|
|
36
|
+
-- AND $__timeFilter(start_date)
|
|
37
|
+
AND ref_type = 'drive'
|
|
38
|
+
ORDER BY
|
|
39
|
+
start_date DESC,
|
|
40
|
+
end_date DESC
|
|
41
|
+
LIMIT
|
|
42
|
+
1
|
|
43
|
+
),
|
|
44
|
+
latest AS (
|
|
45
|
+
SELECT
|
|
46
|
+
b.car_id,
|
|
47
|
+
'idle'::pic_ref_type AS ref_type,
|
|
48
|
+
(-100)::INTEGER AS ref_id,
|
|
49
|
+
end_date AS start_date,
|
|
50
|
+
(
|
|
51
|
+
SELECT
|
|
52
|
+
now()
|
|
53
|
+
) AS end_date,
|
|
54
|
+
b.end_address AS start_address,
|
|
55
|
+
b.end_address,
|
|
56
|
+
NULL:NUMERIC AS real_km,
|
|
57
|
+
b.dropped_kwh,
|
|
58
|
+
b.gross_dropped_kwh,
|
|
59
|
+
b.dropped_cost,
|
|
60
|
+
b.gross_dropped_cost,
|
|
61
|
+
b.cost_per_real_100km,
|
|
62
|
+
b.gross_cost_per_real_100km,
|
|
63
|
+
b.kwh_per_real_1km,
|
|
64
|
+
b.gross_kwh_per_real_1km,
|
|
65
|
+
b.cost_per_kwh,
|
|
66
|
+
b.gross_cost_per_kwh
|
|
67
|
+
FROM
|
|
68
|
+
latest_base b
|
|
69
|
+
INNER JOIN latest_selected s ON s.car_id = b.car_id
|
|
70
|
+
AND s.ref_type = b.ref_type
|
|
71
|
+
AND s.ref_id = b.ref_id
|
|
72
|
+
LIMIT
|
|
73
|
+
1
|
|
74
|
+
),
|
|
75
|
+
res AS (
|
|
76
|
+
SELECT
|
|
77
|
+
rb.start_date,
|
|
78
|
+
rb.ref_type,
|
|
79
|
+
rb.end_date,
|
|
80
|
+
rb.start_address,
|
|
81
|
+
rb.end_address,
|
|
82
|
+
rb.real_km,
|
|
83
|
+
rb.dropped_kwh,
|
|
84
|
+
rb.gross_dropped_kwh,
|
|
85
|
+
rb.dropped_cost,
|
|
86
|
+
rb.gross_dropped_cost,
|
|
87
|
+
rb.cost_per_real_100km,
|
|
88
|
+
rb.gross_cost_per_real_100km,
|
|
89
|
+
rb.kwh_per_real_1km,
|
|
90
|
+
rb.gross_kwh_per_real_1km,
|
|
91
|
+
rb.cost_per_kwh,
|
|
92
|
+
rb.gross_cost_per_kwh,
|
|
93
|
+
rb.car_id,
|
|
94
|
+
rb.ref_id
|
|
95
|
+
FROM
|
|
96
|
+
base rb
|
|
97
|
+
UNION ALL
|
|
98
|
+
SELECT
|
|
99
|
+
rl.start_date,
|
|
100
|
+
rl.ref_type,
|
|
101
|
+
rl.end_date,
|
|
102
|
+
rl.start_address,
|
|
103
|
+
rl.end_address,
|
|
104
|
+
rl.real_km,
|
|
105
|
+
rl.dropped_kwh,
|
|
106
|
+
rl.gross_dropped_kwh,
|
|
107
|
+
rl.dropped_cost,
|
|
108
|
+
rl.gross_dropped_cost,
|
|
109
|
+
rl.cost_per_real_100km,
|
|
110
|
+
rl.gross_cost_per_real_100km,
|
|
111
|
+
rl.kwh_per_real_1km,
|
|
112
|
+
rl.gross_kwh_per_real_1km,
|
|
113
|
+
rl.cost_per_kwh,
|
|
114
|
+
rl.gross_cost_per_kwh,
|
|
115
|
+
rl.car_id,
|
|
116
|
+
rl.ref_id
|
|
117
|
+
FROM
|
|
118
|
+
latest rl
|
|
119
|
+
)
|
|
120
|
+
SELECT
|
|
121
|
+
r.start_date AS "Start",
|
|
122
|
+
CASE
|
|
123
|
+
WHEN r.ref_type = 'drive' THEN '🚗 Driving'
|
|
124
|
+
WHEN r.ref_type = 'idle' THEN '🅿️ Parking'
|
|
125
|
+
WHEN r.ref_type = 'charge' THEN '🔋 Charging'
|
|
126
|
+
ELSE NULL
|
|
127
|
+
END AS "Action",
|
|
128
|
+
EXTRACT(
|
|
129
|
+
EPOCH
|
|
130
|
+
FROM
|
|
131
|
+
r.end_date - r.start_date
|
|
132
|
+
) AS "Duration",
|
|
133
|
+
r.start_address AS "From",
|
|
134
|
+
r.end_address AS "To",
|
|
135
|
+
r.real_km AS "Distance",
|
|
136
|
+
r.dropped_kwh AS "Energy (net)",
|
|
137
|
+
CASE
|
|
138
|
+
WHEN r.ref_type = 'drive' THEN r.gross_dropped_kwh
|
|
139
|
+
END AS "Energy (gross)",
|
|
140
|
+
r.dropped_cost AS "Cost (net)",
|
|
141
|
+
CASE
|
|
142
|
+
WHEN r.ref_type = 'drive' THEN r.gross_dropped_cost
|
|
143
|
+
END AS "Cost (gross)",
|
|
144
|
+
r.cost_per_real_100km AS "Cost / 100 km (net)",
|
|
145
|
+
CASE
|
|
146
|
+
WHEN r.ref_type = 'drive' THEN r.gross_cost_per_real_100km
|
|
147
|
+
END AS "Cost / 100 km (gross)",
|
|
148
|
+
r.kwh_per_real_1km AS "Energy / km (net)",
|
|
149
|
+
CASE
|
|
150
|
+
WHEN r.ref_type = 'drive' THEN r.gross_kwh_per_real_1km
|
|
151
|
+
END AS "Energy / km (gross)",
|
|
152
|
+
r.cost_per_kwh AS "Cost / kWh (net)",
|
|
153
|
+
CASE
|
|
154
|
+
WHEN r.ref_type = 'drive' THEN r.gross_cost_per_kwh
|
|
155
|
+
END AS "Cost / kWh (gross)",
|
|
156
|
+
floor(
|
|
157
|
+
extract(
|
|
158
|
+
epoch
|
|
159
|
+
FROM
|
|
160
|
+
r.start_date
|
|
161
|
+
)
|
|
162
|
+
) * 1000 AS start_date_ts,
|
|
163
|
+
ceil(
|
|
164
|
+
extract(
|
|
165
|
+
epoch
|
|
166
|
+
FROM
|
|
167
|
+
r.end_date
|
|
168
|
+
)
|
|
169
|
+
) * 1000 AS end_date_ts,
|
|
170
|
+
r.car_id,
|
|
171
|
+
r.ref_id AS drive_id,
|
|
172
|
+
-- h.climate_on_ratio,
|
|
173
|
+
-- CONCAT('F: ', round(h.front_defroster_ratio * 100.0, 0), ' %, R: ', round(h.rear_defroster_ratio * 100.0, 0), ' %') AS defroster_ratio,
|
|
174
|
+
h.battery_heater_ratio AS "Battery Heater"
|
|
175
|
+
FROM
|
|
176
|
+
res r
|
|
177
|
+
LEFT JOIN pic_refs_hvac_ratio_v h ON h.car_id = r.car_id
|
|
178
|
+
AND h.ref_type = r.ref_type
|
|
179
|
+
AND h.ref_id = r.ref_id
|
|
180
|
+
ORDER BY
|
|
181
|
+
r.start_date DESC;
|