diff --git a/contracts/common_types/src/lib.rs b/contracts/common_types/src/lib.rs index f69a18d..e95c30d 100644 --- a/contracts/common_types/src/lib.rs +++ b/contracts/common_types/src/lib.rs @@ -9,10 +9,11 @@ mod types; // Re-export all types pub use types::{ - validate_attribute, validate_metadata, AttendanceAction, MembershipStatus, MetadataUpdate, - MetadataValue, SubscriptionPlan, SubscriptionTier, TierChangeRequest, TierChangeStatus, - TierChangeType, TierFeature, TierLevel, TierPromotion, TokenMetadata, UserRole, - MAX_ATTRIBUTES_COUNT, MAX_ATTRIBUTE_KEY_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_TEXT_VALUE_LENGTH, + validate_attribute, validate_metadata, AttendanceAction, AttendanceFrequency, DateRange, + DayPattern, MembershipStatus, MetadataUpdate, MetadataValue, PeakHourData, SubscriptionPlan, + SubscriptionTier, TierChangeRequest, TierChangeStatus, TierChangeType, TierFeature, TierLevel, + TierPromotion, TimePeriod, TokenMetadata, UserAttendanceStats, UserRole, MAX_ATTRIBUTES_COUNT, + MAX_ATTRIBUTE_KEY_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_TEXT_VALUE_LENGTH, }; #[cfg(test)] diff --git a/contracts/common_types/src/types.rs b/contracts/common_types/src/types.rs index 2fcd28c..ac01f15 100644 --- a/contracts/common_types/src/types.rs +++ b/contracts/common_types/src/types.rs @@ -183,6 +183,146 @@ pub enum MembershipStatus { Inactive, } +// ============================================================================ +// Attendance Analytics Types +// ============================================================================ + +/// Time period options for analytics queries. +/// +/// Used to group and filter attendance data by specific time ranges. +/// +/// # Variants +/// * `Daily` - Daily aggregation +/// * `Weekly` - Weekly aggregation +/// * `Monthly` - Monthly aggregation +/// * `Custom` - Custom date range +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TimePeriod { + /// Daily time period + Daily, + /// Weekly time period + Weekly, + /// Monthly time period + Monthly, + /// Custom date range + Custom, +} + +/// Date range structure for filtering attendance records. +/// +/// Specifies a time window for querying attendance data. +/// +/// # Fields +/// * `start_time` - Start timestamp (inclusive) +/// * `end_time` - End timestamp (inclusive) +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateRange { + /// Start timestamp + pub start_time: u64, + /// End timestamp + pub end_time: u64, +} + +/// Aggregated attendance statistics for a user. +/// +/// Contains comprehensive attendance metrics including duration, +/// frequency, and pattern analysis. +/// +/// # Fields +/// * `user_id` - User address +/// * `total_sessions` - Total number of attendance sessions +/// * `total_duration` - Total time spent (seconds) +/// * `average_duration` - Average session duration (seconds) +/// * `first_clock_in` - Timestamp of first attendance +/// * `last_clock_out` - Timestamp of last departure +/// * `total_days_present` - Number of unique days with attendance +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UserAttendanceStats { + /// User address + pub user_id: Address, + /// Total attendance sessions + pub total_sessions: u32, + /// Total duration in seconds + pub total_duration: u64, + /// Average session duration in seconds + pub average_duration: u64, + /// First clock-in timestamp + pub first_clock_in: u64, + /// Last clock-out timestamp + pub last_clock_out: u64, + /// Total unique days present + pub total_days_present: u32, +} + +/// Attendance frequency metrics for a specific time period. +/// +/// Tracks attendance frequency patterns and distribution. +/// +/// # Fields +/// * `period` - Time period type +/// * `period_start` - Period start timestamp +/// * `period_end` - Period end timestamp +/// * `total_attendances` - Total attendance records in period +/// * `unique_users` - Number of unique users +/// * `average_daily_attendance` - Average attendances per day +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AttendanceFrequency { + /// Time period + pub period: TimePeriod, + /// Period start timestamp + pub period_start: u64, + /// Period end timestamp + pub period_end: u64, + /// Total attendance records + pub total_attendances: u32, + /// Unique users count + pub unique_users: u32, + /// Average daily attendance + pub average_daily_attendance: u32, +} + +/// Peak hour analysis data. +/// +/// Identifies hours and days with highest attendance activity. +/// +/// # Fields +/// * `hour` - Hour of day (0-23) +/// * `attendance_count` - Number of attendances in this hour +/// * `percentage` - Percentage of total attendances +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PeakHourData { + /// Hour of day (0-23) + pub hour: u32, + /// Attendance count + pub attendance_count: u32, + /// Percentage of total + pub percentage: u32, +} + +/// Daily attendance pattern data. +/// +/// Tracks attendance distribution across days of the week. +/// +/// # Fields +/// * `day_of_week` - Day (0=Sunday, 6=Saturday) +/// * `attendance_count` - Number of attendances on this day +/// * `percentage` - Percentage of total attendances +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DayPattern { + /// Day of week (0=Sunday, 6=Saturday) + pub day_of_week: u32, + /// Attendance count + pub attendance_count: u32, + /// Percentage of total + pub percentage: u32, +} + // ============================================================================ // Subscription Tier Types // ============================================================================ diff --git a/contracts/manage_hub/src/attendance_log.rs b/contracts/manage_hub/src/attendance_log.rs index cfa7f73..eecdc7a 100644 --- a/contracts/manage_hub/src/attendance_log.rs +++ b/contracts/manage_hub/src/attendance_log.rs @@ -2,7 +2,10 @@ #![allow(deprecated)] use crate::errors::Error; -use crate::types::AttendanceAction; +use crate::types::{AttendanceAction, AttendanceSummary, SessionPair}; +use common_types::{ + AttendanceFrequency, DateRange, DayPattern, PeakHourData, TimePeriod, UserAttendanceStats, +}; use soroban_sdk::{contracttype, symbol_short, Address, BytesN, Env, Map, String, Vec}; #[contracttype] @@ -94,4 +97,445 @@ impl AttendanceLogModule { pub fn get_attendance_log(env: Env, id: BytesN<32>) -> Option { env.storage().persistent().get(&DataKey::AttendanceLog(id)) } + + // ============================================================================ + // Analytics and Reporting Functions + // ============================================================================ + + /// Get attendance summary for a user within a date range + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to filter records + /// + /// # Returns + /// * `Ok(AttendanceSummary)` - Summary of attendance data + /// * `Err(Error)` - If date range is invalid or no records found + pub fn get_attendance_summary( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + // Validate date range + if date_range.start_time > date_range.end_time { + return Err(Error::InvalidDateRange); + } + + let logs = Self::get_logs_for_user(env.clone(), user_id.clone()); + + if logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Filter logs by date range + let filtered_logs = Self::filter_logs_by_date_range(&logs, &date_range); + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Calculate statistics + let mut total_clock_ins = 0u32; + let mut total_clock_outs = 0u32; + let mut total_duration = 0u64; + let mut sessions = Vec::new(&env); + + let mut i = 0; + while i < filtered_logs.len() { + let log = filtered_logs.get(i).unwrap(); + + match log.action { + AttendanceAction::ClockIn => { + total_clock_ins += 1; + // Look for matching clock out + let mut j = i + 1; + while j < filtered_logs.len() { + let next_log = filtered_logs.get(j).unwrap(); + if next_log.action == AttendanceAction::ClockOut { + let duration = next_log.timestamp - log.timestamp; + total_duration += duration; + sessions.push_back(SessionPair { + clock_in_time: log.timestamp, + clock_out_time: next_log.timestamp, + duration, + }); + break; + } + j += 1; + } + } + AttendanceAction::ClockOut => { + total_clock_outs += 1; + } + } + i += 1; + } + + let total_sessions = sessions.len(); + let average_session_duration = if total_sessions > 0 { + total_duration / total_sessions as u64 + } else { + 0 + }; + + Ok(AttendanceSummary { + user_id, + date_range_start: date_range.start_time, + date_range_end: date_range.end_time, + total_clock_ins, + total_clock_outs, + total_duration, + average_session_duration, + total_sessions, + }) + } + + /// Get time-based attendance records (daily, weekly, monthly) + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `_period` - Time period for grouping + /// * `date_range` - Date range to filter records + /// + /// # Returns + /// * Vector of attendance logs grouped by the specified period + pub fn get_time_based_attendance( + env: Env, + user_id: Address, + _period: TimePeriod, + date_range: DateRange, + ) -> Result, Error> { + if date_range.start_time > date_range.end_time { + return Err(Error::InvalidDateRange); + } + + let logs = Self::get_logs_for_user(env.clone(), user_id); + let filtered_logs = Self::filter_logs_by_date_range(&logs, &date_range); + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Return filtered logs based on period + // For actual implementation, we return all filtered logs + // In a more advanced implementation, you could group by day/week/month + Ok(filtered_logs) + } + + /// Calculate attendance frequency for a user + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * `AttendanceFrequency` - Frequency statistics + pub fn calculate_attendance_frequency( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + if date_range.start_time > date_range.end_time { + return Err(Error::InvalidDateRange); + } + + let logs = Self::get_logs_for_user(env.clone(), user_id); + let filtered_logs = Self::filter_logs_by_date_range(&logs, &date_range); + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + let total_attendances = filtered_logs.len(); + + // Calculate number of days in range + let days_in_range = ((date_range.end_time - date_range.start_time) / 86400) + 1; + let average_daily_attendance = if days_in_range > 0 { + (total_attendances as u64 / days_in_range) as u32 + } else { + 0 + }; + + Ok(AttendanceFrequency { + period: TimePeriod::Custom, + period_start: date_range.start_time, + period_end: date_range.end_time, + total_attendances, + unique_users: 1, // Single user query + average_daily_attendance, + }) + } + + /// Get comprehensive user attendance statistics + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Optional date range (None for all-time stats) + /// + /// # Returns + /// * `UserAttendanceStats` - Comprehensive attendance statistics + pub fn get_user_statistics( + env: Env, + user_id: Address, + date_range: Option, + ) -> Result { + let logs = Self::get_logs_for_user(env.clone(), user_id.clone()); + + if logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + let filtered_logs = match date_range { + Some(range) => { + if range.start_time > range.end_time { + return Err(Error::InvalidDateRange); + } + Self::filter_logs_by_date_range(&logs, &range) + } + None => logs, + }; + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Parse sessions + let sessions = Self::parse_sessions(&env, &filtered_logs); + let total_sessions = sessions.len(); + + let mut total_duration = 0u64; + let mut first_clock_in = u64::MAX; + let mut last_clock_out = 0u64; + let mut unique_days: Vec = Vec::new(&env); + + for i in 0..sessions.len() { + let session = sessions.get(i).unwrap(); + total_duration += session.duration; + + if session.clock_in_time < first_clock_in { + first_clock_in = session.clock_in_time; + } + if session.clock_out_time > last_clock_out { + last_clock_out = session.clock_out_time; + } + + // Track unique days + let day = session.clock_in_time / 86400; + let mut day_exists = false; + for j in 0..unique_days.len() { + if unique_days.get(j).unwrap() == day { + day_exists = true; + break; + } + } + if !day_exists { + unique_days.push_back(day); + } + } + + let average_duration = if total_sessions > 0 { + total_duration / total_sessions as u64 + } else { + 0 + }; + + Ok(UserAttendanceStats { + user_id, + total_sessions, + total_duration, + average_duration, + first_clock_in, + last_clock_out, + total_days_present: unique_days.len(), + }) + } + + /// Analyze peak attendance hours + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * Vector of peak hour data sorted by attendance count + pub fn analyze_peak_hours( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result, Error> { + if date_range.start_time > date_range.end_time { + return Err(Error::InvalidDateRange); + } + + let logs = Self::get_logs_for_user(env.clone(), user_id); + let filtered_logs = Self::filter_logs_by_date_range(&logs, &date_range); + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Count attendances by hour + let mut hour_counts: Map = Map::new(&env); + let total_attendances = filtered_logs.len(); + + for i in 0..filtered_logs.len() { + let log = filtered_logs.get(i).unwrap(); + let hour = ((log.timestamp % 86400) / 3600) as u32; + + let count = hour_counts.get(hour).unwrap_or(0); + hour_counts.set(hour, count + 1); + } + + // Build result vector + let mut result: Vec = Vec::new(&env); + for hour in 0..24 { + if let Some(count) = hour_counts.get(hour) { + let percentage = if total_attendances > 0 { + (count * 100) / total_attendances + } else { + 0 + }; + + result.push_back(PeakHourData { + hour, + attendance_count: count, + percentage, + }); + } + } + + Ok(result) + } + + /// Analyze attendance patterns by day of week + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * Vector of day patterns showing attendance distribution + pub fn analyze_day_patterns( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result, Error> { + if date_range.start_time > date_range.end_time { + return Err(Error::InvalidDateRange); + } + + let logs = Self::get_logs_for_user(env.clone(), user_id); + let filtered_logs = Self::filter_logs_by_date_range(&logs, &date_range); + + if filtered_logs.is_empty() { + return Err(Error::NoAttendanceRecords); + } + + // Count attendances by day of week + let mut day_counts: Map = Map::new(&env); + let total_attendances = filtered_logs.len(); + + for i in 0..filtered_logs.len() { + let log = filtered_logs.get(i).unwrap(); + // Calculate day of week (0 = Thursday, Jan 1, 1970) + // Adjust to 0 = Sunday + let days_since_epoch = log.timestamp / 86400; + let day_of_week = ((days_since_epoch + 4) % 7) as u32; + + let count = day_counts.get(day_of_week).unwrap_or(0); + day_counts.set(day_of_week, count + 1); + } + + // Build result vector + let mut result: Vec = Vec::new(&env); + for day in 0..7 { + if let Some(count) = day_counts.get(day) { + let percentage = if total_attendances > 0 { + (count * 100) / total_attendances + } else { + 0 + }; + + result.push_back(DayPattern { + day_of_week: day, + attendance_count: count, + percentage, + }); + } + } + + Ok(result) + } + + // ============================================================================ + // Helper Functions + // ============================================================================ + + /// Filter logs by date range + fn filter_logs_by_date_range( + logs: &Vec, + date_range: &DateRange, + ) -> Vec { + let env = logs.env(); + let mut filtered: Vec = Vec::new(env); + + for i in 0..logs.len() { + let log = logs.get(i).unwrap(); + if log.timestamp >= date_range.start_time && log.timestamp <= date_range.end_time { + filtered.push_back(log); + } + } + + filtered + } + + /// Parse attendance logs into complete sessions (clock-in to clock-out pairs) + fn parse_sessions(env: &Env, logs: &Vec) -> Vec { + let mut sessions: Vec = Vec::new(env); + let mut pending_clock_in: Option = None; + + for i in 0..logs.len() { + let log = logs.get(i).unwrap(); + + match log.action { + AttendanceAction::ClockIn => { + pending_clock_in = Some(log.timestamp); + } + AttendanceAction::ClockOut => { + if let Some(clock_in_time) = pending_clock_in { + let duration = log.timestamp - clock_in_time; + sessions.push_back(SessionPair { + clock_in_time, + clock_out_time: log.timestamp, + duration, + }); + pending_clock_in = None; + } + } + } + } + + sessions + } + + /// Calculate total hours from total seconds + pub fn calculate_total_hours(total_seconds: u64) -> u64 { + total_seconds / 3600 + } + + /// Calculate average daily attendance from logs + pub fn calculate_average_daily_attendance( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + let frequency = Self::calculate_attendance_frequency(env, user_id, date_range)?; + Ok(frequency.average_daily_attendance as u64) + } } diff --git a/contracts/manage_hub/src/errors.rs b/contracts/manage_hub/src/errors.rs index edce8ba..2f8cf36 100644 --- a/contracts/manage_hub/src/errors.rs +++ b/contracts/manage_hub/src/errors.rs @@ -25,6 +25,9 @@ pub enum Error { MetadataTextValueTooLong = 20, MetadataValidationFailed = 21, InvalidMetadataVersion = 22, + InvalidDateRange = 23, + NoAttendanceRecords = 24, + IncompleteSession = 25, // Tier management errors (30-50) TierNotFound = 30, TierAlreadyExists = 31, diff --git a/contracts/manage_hub/src/lib.rs b/contracts/manage_hub/src/lib.rs index b6629f0..487fc9b 100644 --- a/contracts/manage_hub/src/lib.rs +++ b/contracts/manage_hub/src/lib.rs @@ -8,13 +8,16 @@ mod subscription; mod types; use attendance_log::{AttendanceLog, AttendanceLogModule}; -use common_types::{MetadataUpdate, MetadataValue, TokenMetadata}; +use common_types::{ + AttendanceFrequency, DateRange, DayPattern, MetadataUpdate, MetadataValue, PeakHourData, + TimePeriod, TokenMetadata, UserAttendanceStats, +}; use errors::Error; use membership_token::{MembershipToken, MembershipTokenContract}; use subscription::SubscriptionContract; use types::{ - AttendanceAction, BillingCycle, CreatePromotionParams, CreateTierParams, Subscription, - SubscriptionTier, TierAnalytics, TierFeature, TierPromotion, UpdateTierParams, + AttendanceAction, AttendanceSummary, BillingCycle, CreatePromotionParams, CreateTierParams, + Subscription, SubscriptionTier, TierAnalytics, TierFeature, TierPromotion, UpdateTierParams, UserSubscriptionInfo, }; @@ -384,6 +387,180 @@ impl Contract { ) -> Vec> { MembershipTokenContract::query_tokens_by_attribute(env, attribute_key, attribute_value) } + + // ============================================================================ + // Attendance Analytics Endpoints + // ============================================================================ + + /// Get attendance summary for a user within a date range. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to filter records + /// + /// # Returns + /// * `Ok(AttendanceSummary)` - Summary with clock-ins, clock-outs, duration stats + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn get_attendance_summary( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + AttendanceLogModule::get_attendance_summary(env, user_id, date_range) + } + + /// Get time-based attendance records (daily, weekly, monthly). + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `period` - Time period for grouping (Daily, Weekly, Monthly, Custom) + /// * `date_range` - Date range to filter records + /// + /// # Returns + /// * `Ok(Vec)` - Filtered attendance logs for the period + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn get_time_based_attendance( + env: Env, + user_id: Address, + period: TimePeriod, + date_range: DateRange, + ) -> Result, Error> { + AttendanceLogModule::get_time_based_attendance(env, user_id, period, date_range) + } + + /// Calculate attendance frequency for a user. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * `Ok(AttendanceFrequency)` - Frequency metrics including total, average daily + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn calculate_attendance_frequency( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + AttendanceLogModule::calculate_attendance_frequency(env, user_id, date_range) + } + + /// Get comprehensive user attendance statistics. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Optional date range (None for all-time stats) + /// + /// # Returns + /// * `Ok(UserAttendanceStats)` - Comprehensive stats including total hours, + /// average attendance, session counts, and date ranges + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time (if range provided) + /// * `NoAttendanceRecords` - No records found for user + pub fn get_user_statistics( + env: Env, + user_id: Address, + date_range: Option, + ) -> Result { + AttendanceLogModule::get_user_statistics(env, user_id, date_range) + } + + /// Analyze peak attendance hours for a user. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * `Ok(Vec)` - Peak hour analysis showing attendance count + /// and percentage per hour + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn analyze_peak_hours( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result, Error> { + AttendanceLogModule::analyze_peak_hours(env, user_id, date_range) + } + + /// Analyze attendance patterns by day of week. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * `Ok(Vec)` - Day patterns showing attendance distribution + /// across days of the week with counts and percentages + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn analyze_day_patterns( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result, Error> { + AttendanceLogModule::analyze_day_patterns(env, user_id, date_range) + } + + /// Calculate total hours from seconds. + /// + /// # Arguments + /// * `total_seconds` - Total seconds to convert + /// + /// # Returns + /// * Total hours (rounded down) + pub fn calculate_total_hours(total_seconds: u64) -> u64 { + AttendanceLogModule::calculate_total_hours(total_seconds) + } + + /// Calculate average daily attendance for a user. + /// + /// # Arguments + /// * `env` - Contract environment + /// * `user_id` - User address to query + /// * `date_range` - Date range to analyze + /// + /// # Returns + /// * `Ok(u64)` - Average daily attendance count + /// * `Err(Error)` - If date range is invalid or no records found + /// + /// # Errors + /// * `InvalidDateRange` - Start time is after end time + /// * `NoAttendanceRecords` - No records found for user in range + pub fn get_avg_daily_attendance( + env: Env, + user_id: Address, + date_range: DateRange, + ) -> Result { + AttendanceLogModule::calculate_average_daily_attendance(env, user_id, date_range) + } } mod test; diff --git a/contracts/manage_hub/src/types.rs b/contracts/manage_hub/src/types.rs index f334465..9145c9f 100644 --- a/contracts/manage_hub/src/types.rs +++ b/contracts/manage_hub/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, String}; +use soroban_sdk::{contracttype, Address, String, Vec}; // Re-export types from common_types for consistency pub use common_types::MembershipStatus; @@ -146,3 +146,37 @@ pub struct CreatePromotionParams { /// Maximum number of redemptions (0 = unlimited) pub max_redemptions: u32, } + +// Attendance analytics summary structures +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct AttendanceSummary { + pub user_id: Address, + pub date_range_start: u64, + pub date_range_end: u64, + pub total_clock_ins: u32, + pub total_clock_outs: u32, + pub total_duration: u64, + pub average_session_duration: u64, + pub total_sessions: u32, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct AttendanceReport { + pub report_id: String, + pub generated_at: u64, + pub date_range_start: u64, + pub date_range_end: u64, + pub total_users: u32, + pub total_attendances: u32, + pub user_summaries: Vec, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub struct SessionPair { + pub clock_in_time: u64, + pub clock_out_time: u64, + pub duration: u64, +}