Jodinho commited on
Commit
b4c8fac
·
1 Parent(s): 39e1acf

fix(kpis): update get_dashboard_kpis in frontend sql reference with rh.created_at and availability breakdown

Browse files
misc/supabase_frontend_realest_views_n_rpcs.sql ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- public.get_dashboard_kpis() (RPC) — Parameterized version
2
+ -- All p_* params default to NULL → treated as "no filter" (fully backward-compatible).
3
+ -- Accepts optional filter params for the real_estate block so all KPIs reflect
4
+ -- the active filter state from the frontend.
5
+
6
+ DROP FUNCTION IF EXISTS public.get_dashboard_kpis(text, text, integer, boolean, uuid[], date, date);
7
+
8
+ DROP FUNCTION IF EXISTS public.get_dashboard_kpis(text, text, integer, boolean, uuid[], date, date);
9
+
10
+ CREATE OR REPLACE FUNCTION public.get_dashboard_kpis(
11
+ p_market TEXT DEFAULT NULL,
12
+ p_platform TEXT DEFAULT NULL,
13
+ p_bedrooms INTEGER DEFAULT NULL,
14
+ p_is_active BOOLEAN DEFAULT NULL,
15
+ p_property_ids UUID[] DEFAULT NULL,
16
+ p_start_date DATE DEFAULT NULL,
17
+ p_end_date DATE DEFAULT NULL
18
+ )
19
+ RETURNS json
20
+ LANGUAGE sql
21
+ SECURITY DEFINER
22
+ AS $
23
+ WITH target_props AS (
24
+ SELECT DISTINCT p.id, p.market, p.platform, p.bedrooms, p.is_active
25
+ FROM properties p
26
+ WHERE (p_market IS NULL OR p.market = p_market)
27
+ AND (p_platform IS NULL OR p.platform = p_platform)
28
+ AND (p_bedrooms IS NULL OR p.bedrooms = p_bedrooms)
29
+ AND (p_is_active IS NULL OR p.is_active = p_is_active)
30
+ AND (p_property_ids IS NULL OR p.id = ANY(p_property_ids))
31
+ ),
32
+ latest_scrape AS (
33
+ SELECT DISTINCT ON (rh.property_id)
34
+ rh.property_id,
35
+ rh.is_available,
36
+ rh.nightly_rate
37
+ FROM rate_history rh
38
+ JOIN target_props tp ON tp.id = rh.property_id
39
+ ORDER BY rh.property_id, rh.created_at DESC
40
+ )
41
+ SELECT json_build_object(
42
+ 'pricing', json_build_object(
43
+ 'products_tracked', (SELECT COUNT(*) FROM products),
44
+ 'price_changes_7d', (
45
+ SELECT COUNT(*) FROM price_history
46
+ WHERE created_at >= NOW() - INTERVAL '7 days'
47
+ AND price != (
48
+ SELECT price FROM price_history ph2
49
+ WHERE ph2.product_id = price_history.product_id
50
+ AND ph2.created_at < price_history.created_at
51
+ ORDER BY ph2.created_at DESC LIMIT 1
52
+ )
53
+ ),
54
+ 'spikes_7d', (
55
+ SELECT COUNT(*) FROM v_price_volatility
56
+ WHERE ABS(pct_above_trailing_avg) >= 25
57
+ AND created_at >= NOW() - INTERVAL '7 days'
58
+ ),
59
+ 'last_scrape_status', (
60
+ SELECT last_status FROM v_scrape_health
61
+ WHERE job_type = 'PRICE_MONITOR'
62
+ ORDER BY last_started_at DESC LIMIT 1
63
+ ),
64
+ 'tracking_since', (SELECT MIN(created_at) FROM products)
65
+ ),
66
+ 'leads', json_build_object(
67
+ 'targets_crawled', (SELECT COUNT(*) FROM lead_targets),
68
+ 'new_leads_7d', (SELECT COUNT(*) FROM leads WHERE created_at >= NOW() - INTERVAL '7 days'),
69
+ 'total_leads_all_time', (SELECT COUNT(*) FROM leads),
70
+ 'last_scrape_status', (
71
+ SELECT last_status FROM v_scrape_health
72
+ WHERE job_type = 'LEAD_GEN'
73
+ ORDER BY last_started_at DESC LIMIT 1
74
+ )
75
+ ),
76
+ 'real_estate', json_build_object(
77
+ -- Properties Tracked (exact count from target_props)
78
+ 'properties_tracked', (SELECT COUNT(*) FROM target_props),
79
+
80
+ -- Live Availability Breakdown
81
+ 'available_count', (SELECT COUNT(*) FROM latest_scrape WHERE is_available = true),
82
+ 'unavailable_count', (SELECT COUNT(*) FROM latest_scrape WHERE is_available = false OR is_available IS NULL),
83
+ 'availability_rate_pct', (
84
+ SELECT ROUND(
85
+ (COUNT(*) FILTER (WHERE is_available = true)::numeric / NULLIF(COUNT(*), 0)::numeric) * 100.0,
86
+ 2
87
+ )
88
+ FROM latest_scrape
89
+ ),
90
+
91
+ -- Rate Changes 7d (scrape-window based)
92
+ 'rate_changes_7d', (
93
+ SELECT COUNT(*)
94
+ FROM rate_history rh
95
+ JOIN target_props tp ON tp.id = rh.property_id
96
+ WHERE rh.created_at >= NOW() - INTERVAL '7 days'
97
+ AND rh.nightly_rate IS NOT NULL
98
+ AND rh.nightly_rate != (
99
+ SELECT nightly_rate FROM rate_history rh2
100
+ WHERE rh2.property_id = rh.property_id
101
+ AND rh2.created_at < rh.created_at
102
+ AND rh2.nightly_rate IS NOT NULL
103
+ ORDER BY rh2.created_at DESC LIMIT 1
104
+ )
105
+ ),
106
+
107
+ -- Spikes 7d (>= 25% deviation from trailing avg within last 7 days)
108
+ 'spikes_7d', (
109
+ SELECT COUNT(*)
110
+ FROM v_rate_volatility v
111
+ JOIN target_props tp ON tp.id = v.property_id
112
+ WHERE ABS(v.pct_above_trailing_avg) >= 25
113
+ AND v.recorded_at >= NOW() - INTERVAL '7 days'
114
+ AND (p_start_date IS NULL OR v.stay_date >= p_start_date)
115
+ AND (p_end_date IS NULL OR v.stay_date <= p_end_date)
116
+ ),
117
+
118
+ 'tracking_since', (SELECT MIN(created_at) FROM properties),
119
+ 'last_scrape_status', (
120
+ SELECT COALESCE(json_object_agg(platform, last_status), '{}'::json)
121
+ FROM v_scrape_health
122
+ WHERE job_type = 'REAL_ESTATE_MONITOR'
123
+ )
124
+ )
125
+ );function$;
126
+
127
+
128
+
129
+ -- Permissions
130
+ GRANT EXECUTE ON FUNCTION public.get_dashboard_kpis(TEXT, TEXT, INTEGER, BOOLEAN, UUID[], DATE, DATE) TO anon;
131
+
132
+
133
+
134
+ -- public.v_rate_volatility — exact DDL
135
+ create or replace view public.v_rate_volatility as
136
+ WITH day_ranked AS (
137
+ SELECT rh_1.id,
138
+ rh_1.property_id,
139
+ rh_1.stay_date,
140
+ rh_1.nightly_rate,
141
+ rh_1.is_available,
142
+ rh_1.currency,
143
+ rh_1.meta_data,
144
+ rh_1.created_at,
145
+ rh_1.updated_at,
146
+ dense_rank() OVER (PARTITION BY rh_1.property_id ORDER BY (date((rh_1.created_at AT TIME ZONE 'America/New_York'::text)))) AS day_rank
147
+ FROM rate_history rh_1
148
+ WHERE rh_1.nightly_rate IS NOT NULL
149
+ ), daily_avg AS (
150
+ SELECT day_ranked.property_id,
151
+ day_ranked.day_rank,
152
+ avg(day_ranked.nightly_rate) AS day_avg
153
+ FROM day_ranked
154
+ GROUP BY day_ranked.property_id, day_ranked.day_rank
155
+ ), trailing_data AS (
156
+ SELECT daily_avg.property_id,
157
+ daily_avg.day_rank,
158
+ avg(daily_avg.day_avg) OVER (PARTITION BY daily_avg.property_id ORDER BY daily_avg.day_rank ROWS BETWEEN 6 PRECEDING AND 1 PRECEDING) AS trailing_avg_rate
159
+ FROM daily_avg
160
+ ), valid_rates AS (
161
+ SELECT dr.id,
162
+ dr.property_id,
163
+ dr.stay_date,
164
+ dr.nightly_rate,
165
+ dr.created_at,
166
+ t.trailing_avg_rate
167
+ FROM day_ranked dr
168
+ JOIN trailing_data t ON t.property_id = dr.property_id AND t.day_rank = dr.day_rank
169
+ )
170
+ SELECT rh.id,
171
+ p.id AS property_id,
172
+ p.name AS property_name,
173
+ p.url,
174
+ p.market,
175
+ p.platform,
176
+ p.latitude,
177
+ p.longitude,
178
+ p.bedrooms,
179
+ p.avg_rating,
180
+ p.review_count,
181
+ p.is_active,
182
+ rh.stay_date,
183
+ rh.nightly_rate,
184
+ rh.is_available,
185
+ rh.currency,
186
+ rh.created_at AS recorded_at,
187
+ vr.trailing_avg_rate,
188
+ round((rh.nightly_rate - vr.trailing_avg_rate) / NULLIF(vr.trailing_avg_rate, 0::numeric) * 100::numeric, 2) AS pct_above_trailing_avg
189
+ FROM rate_history rh
190
+ JOIN properties p ON p.id = rh.property_id
191
+ LEFT JOIN valid_rates vr ON vr.id = rh.id;
192
+
193
+ -- Permissions
194
+ GRANT MAINTAIN ON TABLE public.v_rate_volatility TO anon;
195
+ GRANT REFERENCES ON TABLE public.v_rate_volatility TO anon;
196
+ GRANT SELECT ON TABLE public.v_rate_volatility TO anon;
197
+ GRANT TRIGGER ON TABLE public.v_rate_volatility TO anon;
198
+ GRANT TRUNCATE ON TABLE public.v_rate_volatility TO anon;
199
+
200
+
201
+
202
+
203
+ -- public.v_scrape_health — exact DDL
204
+ create or replace view public.v_scrape_health as
205
+ WITH recent AS (
206
+ SELECT scrape_runs.job_type,
207
+ scrape_runs.status,
208
+ scrape_runs.started_at,
209
+ scrape_runs.finished_at,
210
+ scrape_runs.items_attempted,
211
+ scrape_runs.items_succeeded,
212
+ scrape_runs.items_failed,
213
+ scrape_runs.error_summary,
214
+ scrape_runs.id,
215
+ scrape_runs.created_at,
216
+ scrape_runs.updated_at,
217
+ scrape_runs.platform,
218
+ scrape_runs.meta_data,
219
+ row_number() OVER (PARTITION BY scrape_runs.job_type, scrape_runs.platform ORDER BY scrape_runs.started_at DESC) AS rn
220
+ FROM scrape_runs
221
+ )
222
+ SELECT job_type,
223
+ platform,
224
+ status AS last_status,
225
+ started_at AS last_started_at,
226
+ finished_at AS last_finished_at,
227
+ EXTRACT(epoch FROM finished_at - started_at) AS last_duration_seconds,
228
+ items_attempted,
229
+ items_succeeded,
230
+ items_failed,
231
+ meta_data,
232
+ error_summary,
233
+ CASE
234
+ WHEN status::text = 'failed'::text THEN true
235
+ ELSE false
236
+ END AS is_failed,
237
+ CASE
238
+ WHEN (items_failed::double precision / NULLIF(items_attempted, 0)::double precision) > 0.20::double precision THEN true
239
+ ELSE false
240
+ END AS high_failure_rate,
241
+ CASE
242
+ WHEN ((meta_data ->> 'blocked_count'::text)::integer) > 0 THEN true
243
+ ELSE false
244
+ END AS has_blocks
245
+ FROM recent
246
+ WHERE rn = 1;
247
+
248
+
249
+
250
+
251
+ GRANT MAINTAIN ON TABLE public.v_scrape_health TO anon;
252
+ GRANT REFERENCES ON TABLE public.v_scrape_health TO anon;
253
+ GRANT SELECT ON TABLE public.v_scrape_health TO anon;
254
+ GRANT TRIGGER ON TABLE public.v_scrape_health TO anon;
255
+ GRANT TRUNCATE ON TABLE public.v_scrape_health TO anon;