From 88e3a09579d0afcb3513029e73f2cd0d5c09fa2c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 30 Dec 2025 21:40:31 +0000 Subject: [PATCH] Community Command Center --- api.php | 454 ++++++++++++++++++ assets/css/custom.css | 33 ++ assets/pasted-20251230-211133-cc45faf9.png | Bin 0 -> 24509 bytes db/setup.php | 112 +++++ index.php | 507 +++++++++++++++------ 5 files changed, 961 insertions(+), 145 deletions(-) create mode 100644 api.php create mode 100644 assets/css/custom.css create mode 100644 assets/pasted-20251230-211133-cc45faf9.png create mode 100644 db/setup.php diff --git a/api.php b/api.php new file mode 100644 index 0000000..43cc3ae --- /dev/null +++ b/api.php @@ -0,0 +1,454 @@ +prepare(""" + SELECT COUNT(DISTINCT m.id) AS unique_members + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetch(); +} + +function get_unique_members_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, m.join_date, MAX(a.attendance_date) as last_visit + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id, m.name, m.email, m.join_date + ORDER BY m.name + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetchAll(); +} + +if (isset($_GET['kpi'])) { + header('Content-Type: application/json'); + $community_id = $_GET['community_id'] ?? 1; + $start_date = $_GET['start_date'] ?? date('Y-m-01'); + $end_date = $_GET['end_date'] ?? date('Y-m-t'); + + switch ($_GET['kpi']) { + case 'unique_members': + echo json_encode(get_unique_members($community_id, $start_date, $end_date)); + break; + case 'unique_members_drilldown': + echo json_encode(get_unique_members_drilldown($community_id, $start_date, $end_date)); + break; + case 'avg_class_attendance': + echo json_encode(get_avg_class_attendance($community_id, $start_date, $end_date)); + break; + case 'avg_class_attendance_drilldown': + echo json_encode(get_avg_class_attendance_drilldown($community_id, $start_date, $end_date)); + break; + case 'classes_lightly_attended': + echo json_encode(get_classes_lightly_attended($community_id, $start_date, $end_date)); + break; + case 'classes_lightly_attended_drilldown': + echo json_encode(get_classes_lightly_attended_drilldown($community_id, $start_date, $end_date)); + break; + case 'attendance_trend': + echo json_encode(get_attendance_trend($community_id, $start_date, $end_date)); + break; + case 'avg_visits_per_member': + echo json_encode(get_avg_visits_per_member($community_id, $start_date, $end_date)); + break; + case 'avg_visits_per_member_drilldown': + echo json_encode(get_avg_visits_per_member_drilldown($community_id, $start_date, $end_date)); + break; + case 'high_frequency_members': + echo json_encode(get_high_frequency_members($community_id, $start_date, $end_date)); + break; + case 'high_frequency_members_drilldown': + echo json_encode(get_high_frequency_members_drilldown($community_id, $start_date, $end_date)); + break; + case 'total_revenue': + echo json_encode(get_total_revenue($community_id, $start_date, $end_date)); + break; + case 'total_revenue_drilldown': + echo json_encode(get_total_revenue_drilldown($community_id, $start_date, $end_date)); + break; + case 'revenue_per_active_member': + echo json_encode(get_revenue_per_active_member($community_id, $start_date, $end_date)); + break; + case 'revenue_per_active_member_drilldown': + echo json_encode(get_revenue_per_active_member_drilldown($community_id, $start_date, $end_date)); + break; + case 'inactive_members': + echo json_encode(get_inactive_members($community_id, $start_date, $end_date)); + break; + case 'inactive_members_drilldown': + echo json_encode(get_inactive_members_drilldown($community_id, $start_date, $end_date)); + break; + case 'visits_dropped': + echo json_encode(get_visits_dropped($community_id, $start_date, $end_date)); + break; + case 'attendance_trend_drilldown': + echo json_encode(get_attendance_trend_drilldown($community_id, $start_date, $end_date)); + break; + case 'attendance_over_time': + echo json_encode(get_attendance_over_time($community_id, $start_date, $end_date)); + break; + case 'visit_frequency_distribution': + echo json_encode(get_visit_frequency_distribution($community_id, $start_date, $end_date)); + break; + } + exit; +} + +function get_avg_class_attendance($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT AVG(daily_attendance) as avg_attendance + FROM ( + SELECT COUNT(member_id) as daily_attendance + FROM attendance a + JOIN members m ON a.member_id = m.id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY a.class_id, a.attendance_date + ) as daily_counts + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['avg_class_attendance' => round($result['avg_attendance'], 2)]; +} + +function get_avg_class_attendance_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT c.name as class_name, DATE(a.attendance_date) as date, COUNT(a.member_id) as attendance_count + FROM attendance a + JOIN classes c ON a.class_id = c.id + JOIN members m ON a.member_id = m.id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY c.name, DATE(a.attendance_date) + ORDER BY date, class_name + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetchAll(); +} + +function get_classes_lightly_attended($community_id, $start_date, $end_date, $threshold = 5) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT (CAST(SUM(CASE WHEN attendance_count < ? THEN 1 ELSE 0 END) AS DECIMAL(10,2)) / COUNT(*)) * 100 as percentage + FROM ( + SELECT COUNT(a.member_id) as attendance_count + FROM attendance a + JOIN members m ON a.member_id = m.id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY a.class_id, a.attendance_date + ) as class_counts + """); + $stmt->execute([$threshold, $community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['classes_lightly_attended' => round($result['percentage'], 2)]; +} + +function get_classes_lightly_attended_drilldown($community_id, $start_date, $end_date, $threshold = 5) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT c.name as class_name, DATE(a.attendance_date) as date, COUNT(a.member_id) as attendance_count + FROM attendance a + JOIN classes c ON a.class_id = c.id + JOIN members m ON a.member_id = m.id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY c.name, DATE(a.attendance_date) + HAVING COUNT(a.member_id) < ? + ORDER BY date, class_name + """); + $stmt->execute([$community_id, $start_date, $end_date, $threshold]); + return $stmt->fetchAll(); +} + +function get_attendance_trend($community_id, $start_date, $end_date) { + $pdo = db(); + + $current_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $current_period_stmt->execute([$community_id, $start_date, $end_date]); + $current_period_count = $current_period_stmt->fetchColumn(); + + $period_days = (new DateTime($end_date))->diff(new DateTime($start_date))->days; + $previous_start_date = (new DateTime($start_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + $previous_end_date = (new DateTime($end_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + + $previous_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $previous_period_stmt->execute([$community_id, $previous_start_date, $previous_end_date]); + $previous_period_count = $previous_period_stmt->fetchColumn(); + + if ($previous_period_count == 0) { + $trend = $current_period_count > 0 ? 100 : 0; + } else { + $trend = (($current_period_count - $previous_period_count) / $previous_period_count) * 100; + } + + return ['attendance_trend' => round($trend, 2)]; +} + +function get_avg_visits_per_member($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT AVG(visit_count) as avg_visits + FROM ( + SELECT COUNT(a.id) as visit_count + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id + ) as member_visits + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['avg_visits_per_member' => round($result['avg_visits'], 2)]; +} + +function get_avg_visits_per_member_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, COUNT(a.id) as visit_count + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id, m.name, m.email + ORDER BY visit_count DESC + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetchAll(); +} + +function get_high_frequency_members($community_id, $start_date, $end_date, $frequency_threshold = 5) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT (CAST(SUM(CASE WHEN visit_count >= ? THEN 1 ELSE 0 END) AS DECIMAL(10,2)) / COUNT(*)) * 100 as percentage + FROM ( + SELECT COUNT(a.id) as visit_count + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id + ) as member_visits + """); + $stmt->execute([$frequency_threshold, $community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['high_frequency_members' => round($result['percentage'], 2)]; +} + +function get_high_frequency_members_drilldown($community_id, $start_date, $end_date, $frequency_threshold = 5) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, COUNT(a.id) as visit_count + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id, m.name, m.email + HAVING visit_count >= ? + ORDER BY visit_count DESC + """); + $stmt->execute([$community_id, $start_date, $end_date, $frequency_threshold]); + return $stmt->fetchAll(); +} + +function get_total_revenue($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT SUM(r.amount) as total_revenue + FROM revenue r + JOIN members m ON r.member_id = m.id + WHERE m.community_id = ? + AND r.transaction_date BETWEEN ? AND ? + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['total_revenue' => round($result['total_revenue'], 2)]; +} + +function get_total_revenue_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, r.amount, r.service_category, r.transaction_date + FROM revenue r + JOIN members m ON r.member_id = m.id + WHERE m.community_id = ? + AND r.transaction_date BETWEEN ? AND ? + ORDER BY r.transaction_date DESC + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetchAll(); +} + +function get_revenue_per_active_member($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT SUM(r.amount) / COUNT(DISTINCT r.member_id) as revenue_per_active_member + FROM revenue r + JOIN members m ON r.member_id = m.id + WHERE m.community_id = ? + AND r.transaction_date BETWEEN ? AND ? + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $result = $stmt->fetch(); + return ['revenue_per_active_member' => round($result['revenue_per_active_member'], 2)]; +} + +function get_revenue_per_active_member_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, SUM(r.amount) as total_revenue, COUNT(r.id) as transaction_count + FROM revenue r + JOIN members m ON r.member_id = m.id + WHERE m.community_id = ? + AND r.transaction_date BETWEEN ? AND ? + GROUP BY m.id, m.name, m.email + ORDER BY total_revenue DESC + """); + $stmt->execute([$community_id, $start_date, $end_date]); + return $stmt->fetchAll(); +} + +function get_inactive_members($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT + (1 - (COUNT(DISTINCT a.member_id) / COUNT(DISTINCT m.id))) * 100 as inactive_percentage + FROM members m + LEFT JOIN attendance a ON m.id = a.member_id AND a.attendance_date BETWEEN ? AND ? + WHERE m.community_id = ? + """); + $stmt->execute([$start_date, $end_date, $community_id]); + $result = $stmt->fetch(); + return ['inactive_members' => round($result['inactive_percentage'], 2)]; +} + +function get_inactive_members_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT m.name, m.email, m.join_date + FROM members m + LEFT JOIN attendance a ON m.id = a.member_id AND a.attendance_date BETWEEN ? AND ? + WHERE m.community_id = ? + GROUP BY m.id + HAVING COUNT(a.id) = 0 + """); + $stmt->execute([$start_date, $end_date, $community_id]); + return $stmt->fetchAll(); +} + +function get_visits_dropped($community_id, $start_date, $end_date) { + $pdo = db(); + + $current_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $current_period_stmt->execute([$community_id, $start_date, $end_date]); + $current_period_count = $current_period_stmt->fetchColumn(); + + $period_days = (new DateTime($end_date))->diff(new DateTime($start_date))->days; + $previous_start_date = (new DateTime($start_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + $previous_end_date = (new DateTime($end_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + + $previous_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $previous_period_stmt->execute([$community_id, $previous_start_date, $previous_end_date]); + $previous_period_count = $previous_period_stmt->fetchColumn(); + + if ($previous_period_count == 0) { + $trend = $current_period_count > 0 ? 100 : 0; + } else { + $trend = (($current_period_count - $previous_period_count) / $previous_period_count) * 100; + } + + return ['visits_dropped' => round($trend, 2)]; +} + +function get_attendance_trend_drilldown($community_id, $start_date, $end_date) { + $pdo = db(); + + $current_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $current_period_stmt->execute([$community_id, $start_date, $end_date]); + $current_period_count = $current_period_stmt->fetchColumn(); + + $period_days = (new DateTime($end_date))->diff(new DateTime($start_date))->days; + $previous_start_date = (new DateTime($start_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + $previous_end_date = (new DateTime($end_date))->modify("-" . ($period_days + 1) . " days")->format('Y-m-d'); + + $previous_period_stmt = $pdo->prepare("SELECT COUNT(*) as count FROM attendance a JOIN members m ON m.id = a.member_id WHERE m.community_id = ? AND a.attendance_date BETWEEN ? AND ?"); + $previous_period_stmt->execute([$community_id, $previous_start_date, $previous_end_date]); + $previous_period_count = $previous_period_stmt->fetchColumn(); + + return [ + ['period' => 'Current Period', 'visits' => $current_period_count], + ['period' => 'Previous Period', 'visits' => $previous_period_count] + ]; +} + +function get_attendance_over_time($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT DATE(a.attendance_date) as date, COUNT(a.id) as attendance_count + FROM attendance a + JOIN members m ON a.member_id = m.id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY DATE(a.attendance_date) + ORDER BY date + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $data = $stmt->fetchAll(); + + $labels = []; + $values = []; + foreach ($data as $row) { + $labels[] = $row['date']; + $values[] = $row['attendance_count']; + } + + return ['labels' => $labels, 'data' => $values]; +} + +function get_visit_frequency_distribution($community_id, $start_date, $end_date) { + $pdo = db(); + $stmt = $pdo->prepare(""" + SELECT + CASE + WHEN visit_count = 1 THEN '1 visit' + WHEN visit_count BETWEEN 2 AND 4 THEN '2-4 visits' + WHEN visit_count BETWEEN 5 AND 9 THEN '5-9 visits' + ELSE '10+ visits' + END as frequency_bucket, + COUNT(*) as member_count + FROM ( + SELECT COUNT(a.id) as visit_count + FROM members m + JOIN attendance a ON m.id = a.member_id + WHERE m.community_id = ? + AND a.attendance_date BETWEEN ? AND ? + GROUP BY m.id + ) as member_visits + GROUP BY frequency_bucket + """); + $stmt->execute([$community_id, $start_date, $end_date]); + $data = $stmt->fetchAll(); + + $labels = ['1 visit', '2-4 visits', '5-9 visits', '10+ visits']; + $values = [0, 0, 0, 0]; + foreach ($data as $row) { + $index = array_search($row['frequency_bucket'], $labels); + if ($index !== false) { + $values[$index] = $row['member_count']; + } + } + + return ['labels' => $labels, 'data' => $values]; +} diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..5a7a479 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,33 @@ +body { + background-color: #f8f9fa; +} + +.kpi-card { + border-left-width: 5px; +} + +.border-left-primary { + border-left-color: #007bff; +} + +.border-left-success { + border-left-color: #28a745; +} + +.border-left-danger { + border-left-color: #dc3545; +} + +.border-left-info { + border-left-color: #17a2b8; +} + +.kpi-value { + font-size: 2.5rem; + font-weight: bold; +} + +.kpi-label { + font-weight: 500; + color: #6c757d; +} diff --git a/assets/pasted-20251230-211133-cc45faf9.png b/assets/pasted-20251230-211133-cc45faf9.png new file mode 100644 index 0000000000000000000000000000000000000000..af012e582f1210786ce205b400ce12c5d849f680 GIT binary patch literal 24509 zcmb@tbzD{5+BQ02(OuFF0!l5qJCz0t>F$v322qgiMv#(5y1QHHl*VhXryQW0ARk5msSS=NE`Us7zGJ@eNmSGToa_bJ*-*<88U>>SM4 zIRpd**rA;4oSdv+4OT~YTPFiIR$E6pu&4j-O4`KH$ic$i$->T-`cKyehIY%RsuHexriHnB0Wb#i3qVB`3&SsEJ&I$1bboBWT7OISPoj}mK( zKPwTmHn24lVRU0PHZe7Dwsv9^m6T9cmQkmsW~Js3X8$uWYm5JA`B%w*cgFsIGvr^@ z{$qvzhl`hRa&oXR{PzafIJ>O+*;gO&sl<9gIx=^W^{8p_GG(fs=``C?}MYo0S6!{tC1I zk4OJLJmG&nka2T*0k$yUH{#?rNFOL8RHwT{p5C7le|Ml>HHF;&{Y-IjtS)!c(?)~qwzpMX09P@u~ z*T0tf-`o2C(?Q$+$8Gv^%70zcKj#ZR0MD1}zs?sd6O?dvGPiRO)i5!zrB*hvcC@n< z{!{l~<^N-C|KBAM)D!Aw8Sg7wEcAVG;*06n3&21 z&zA12aqMtE2^PLQbqG1MDu_DK&yD&k3QDMUJH0sIySx~$v5w9ZP#X}c_e4gfzI_p~ zql8wjE>@f^W|+ZqL{Y_c`s^;plea2W#FW7wN8L`h)7Z12D5LZ+y+lPsMMOrGi2yBF zGW)MLLn1$yA8`Nv3YK~iFu@i7UFEY5{8PUte^;nb$U-RpXX8I_E~s(9o4-5kX7k5u z`@44b%u`kPcmEjbuSSgsF9GKN7(P)PRsUae0W0j|{T~l*{T2_1c@(n{RowMs@=S%^ zv6$Z(ze`oc8YVbaghv<0qXyjc+hW)GMS>K+$9+FSZsozf@1oDzE7L~>hJp7kLnhA% zKC1wK&#wl33r^lIDt8Q}U6xaE)d=y-fUt`R=_@=NfU|=5bU~kG00@0p!)SvOA3CP? z|AARQLiii?*^X|M_AR;Nod|Gv-&dvzE^7bpMa?km!m&uR{6IDIL3}{0Y z1E|k>(ps*+t)zR(uy<0WG*CkUB49(*3<&~wikl&CS8N37IaRssqPmHEp<4X*oZxdh z`QPXAR1JFtC+NAAs>|+UC@Zh{nj_6{I13ynzXsA*m#{uWFWi0f`7}9x*9ZiOOG-0& zU?Q=tui3v*!iqPb>^s8a7>f06`$z9~v|z)0xqlysjM`5~U3l@kl-|tOSZC^RcHHpM zXw*0@@Zl_89OzK`Nx}oPHi&;RP2gFw_laU(u%reK=WsK;CM^{ffg{q)HKE1ZZ|@#= z(d>(0q^k(`)t#0r-&L7@Uitk~e!l8c_KGHbI$c8*;R})Q+!U_yOT@37aZrTP4I(gL z$4o|%vGi@d=};hv`p(z6M}q9ce_R03CPa>(rMF_JAgo@cAVqqPstnbQ*N5J8xS^+Q z;U3a^I6j^H?b3(58xVL0=SDswB{>gcK0eey{Xy_qhs;}!EbE`reSIAZ9Ak%*wHq%| zn%>a|j0Hr(mz7{GJ2EG)a!9B$E>u$lHNLHU=#YQ;;3=T~J;KCA8?b`&BFg&IvPYx? z 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 X3oTq=6(jrmz`lx-=F>6-Xz>34N9q@8 literal 0 HcmV?d00001 diff --git a/db/setup.php b/db/setup.php new file mode 100644 index 0000000..e7847f8 --- /dev/null +++ b/db/setup.php @@ -0,0 +1,112 @@ +exec(""" + CREATE TABLE IF NOT EXISTS communities ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL + ) + """); + + // Create members table + $pdo->exec(""" + CREATE TABLE IF NOT EXISTS members ( + id INT AUTO_INCREMENT PRIMARY KEY, + community_id INT NOT NULL, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + join_date DATE NOT NULL, + FOREIGN KEY (community_id) REFERENCES communities(id) + ) + """); + + // Create classes table + $pdo->exec(""" + CREATE TABLE IF NOT EXISTS classes ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + instructor VARCHAR(255) NOT NULL + ) + """); + + // Create attendance table + $pdo->exec(""" + CREATE TABLE IF NOT EXISTS attendance ( + id INT AUTO_INCREMENT PRIMARY KEY, + member_id INT NOT NULL, + class_id INT NOT NULL, + attendance_date DATE NOT NULL, + FOREIGN KEY (member_id) REFERENCES members(id), + FOREIGN KEY (class_id) REFERENCES classes(id) + ) + """); + + // Create revenue table + $pdo->exec(""" + CREATE TABLE IF NOT EXISTS revenue ( + id INT AUTO_INCREMENT PRIMARY KEY, + member_id INT NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + service_category VARCHAR(255) NOT NULL, + transaction_date DATE NOT NULL, + FOREIGN KEY (member_id) REFERENCES members(id) + ) + """); + + // Seed data + $stmt = $pdo->query("SELECT COUNT(*) FROM communities"); + if ($stmt->fetchColumn() == 0) { + // Seed communities + $communities = ['Valencia Sound', 'Valencia Reserve', 'Valencia Cove']; + $stmt = $pdo->prepare("INSERT INTO communities (name) VALUES (?)"); + foreach ($communities as $community) { + $stmt->execute([$community]); + } + + // Seed members + $members = [ + [1, 'John Doe', 'john.doe@email.com', '2025-01-15'], + [1, 'Jane Smith', 'jane.smith@email.com', '2025-02-20'], + [2, 'Peter Jones', 'peter.jones@email.com', '2025-03-10'], + [2, 'Mary Johnson', 'mary.johnson@email.com', '2025-04-05'], + [3, 'David Williams', 'david.williams@email.com', '2025-05-12'], + [3, 'Susan Brown', 'susan.brown@email.com', '2025-06-18'], + ]; + $stmt = $pdo->prepare("INSERT INTO members (community_id, name, email, join_date) VALUES (?, ?, ?, ?)"); + foreach ($members as $member) { + $stmt->execute($member); + } + + // Seed classes + $classes = ['Yoga', 'Pilates', 'Zumba', 'Spin']; + $stmt = $pdo->prepare("INSERT INTO classes (name, instructor) VALUES (?, ?)"); + foreach ($classes as $class) { + $stmt->execute([$class, 'Instructor ' . rand(1, 3)]); + } + + // Seed attendance + for ($i = 1; $i <= 500; $i++) { + $member_id = rand(1, 6); + $class_id = rand(1, 4); + $date = date('Y-m-d', strtotime('-' . rand(0, 180) . ' days')); + $pdo->prepare("INSERT INTO attendance (member_id, class_id, attendance_date) VALUES (?, ?, ?)")->execute([$member_id, $class_id, $date]); + } + + // Seed revenue + $service_categories = ['Group', 'PT', 'Massage', 'Events']; + for ($i = 1; $i <= 200; $i++) { + $member_id = rand(1, 6); + $amount = rand(20, 200); + $service_category = $service_categories[rand(0, 3)]; + $date = date('Y-m-d', strtotime('-' . rand(0, 180) . ' days')); + $pdo->prepare("INSERT INTO revenue (member_id, amount, service_category, transaction_date) VALUES (?, ?, ?, ?)")->execute([$member_id, $amount, $service_category, $date]); + } + } +} + +setup_database(); +echo "Database setup complete."; diff --git a/index.php b/index.php index 7205f3d..4a3ee21 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,367 @@ - - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Community Command Center + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ + + +
+
+ +
+
+
+
1. Participation & Usage
+
+
+
+
+
342
+
Unique Members
+
+
+
12
+
Avg Class Attendance
+
+
+
15%
+
Classes Lightly Attended
+
+
+
+5%
+
Attendance Trend
+
+
+
+ +
+
+
+
+ + +
+
+
+
2. Member Engagement Depth
+
+
+
+
+
4.2
+
Avg Visits per Member
+
+
+
45%
+
High Frequency Members
+
+
+
+ +
+
+
+
+ + +
+
+
+
3. Revenue Performance
+
+
+
+
+
$22,450
+
Total Revenue
+
+
+
$65.64
+
Revenue per Active Member
+
+
+
+
+
+ + +
+
+
+
4. Engagement Risk
+
+
+
+
+
0%
+
Inactive Members
+
+
+
0%
+
Visits Dropped
+
+
+
+
+
+ + +
+
+
+
5. Member Feedback
+
+
+
+
+
Top Praises
+
    +
  • "Love the new yoga class!"
  • +
  • "Instructor was fantastic."
  • +
+
+
+
Top Requests
+
    +
  • "More evening classes."
  • +
  • "Wish there was a smoothie bar."
  • +
+
+
+
Top Frictions
+
    +
  • "Locker room needs attention."
  • +
  • "Difficulty booking online."
  • +
+
+
+
+
+
+ + +
+
+
+
6. Alignment Insight
+
+
+

Noticeable Shift

+

Risk: While revenue is up, the drop in visit frequency and recent negative feedback on facilities could impact retention next period.

+

Opportunity: High praise for specific instructors suggests promoting them more could boost attendance.

+
+
+
+ +
+ + +
-
-
- Page updated: (UTC) -
- - + + + + + + + + + + + + + \ No newline at end of file