mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 02:01:40 +00:00
ADD - introduce Health phone app
Add the Health app with server-authoritative health, wellness, medical ID, and emergency-contact data flows. Register the app in the phone UI, add localized configuration and NUI coverage, persist its schema through the resource migration and install SQL, and include design references and the WebP app icon.
This commit is contained in:
@@ -270,6 +270,16 @@ Config.Banking = {
|
||||
HistoryLimit = 50,
|
||||
}
|
||||
|
||||
Config.Health = {
|
||||
DailyStepGoal = 8000,
|
||||
SampleIntervalMs = 500,
|
||||
ReportIntervalSeconds = 30,
|
||||
ReportsPerMinute = 4,
|
||||
MaximumSpeedMetersPerSecond = 12.0,
|
||||
EmergencyNumber = "911",
|
||||
ProfileTextMaxLength = 500,
|
||||
}
|
||||
|
||||
Config.Billing = {
|
||||
Enabled = true,
|
||||
Currency = "$",
|
||||
|
||||
@@ -184,6 +184,51 @@ Locales["en"] = {
|
||||
},
|
||||
},
|
||||
Apps = {
|
||||
health = {
|
||||
name = "Health",
|
||||
navigation = "Health navigation",
|
||||
tabs = { today = "Today", trends = "Trends", medicalId = "Medical ID" },
|
||||
loading = "Loading health data...",
|
||||
errorTitle = "Health is unavailable",
|
||||
errorBody = "Your activity data could not be loaded.",
|
||||
tryAgain = "Try Again",
|
||||
goal = "of {count}",
|
||||
steps = "Steps",
|
||||
distance = "Distance",
|
||||
active = "Active",
|
||||
energy = "Energy",
|
||||
kilometers = "{count} km",
|
||||
minutes = "{count} min",
|
||||
kilocalories = "{count} kcal",
|
||||
thisWeek = "This week",
|
||||
snapshot = "Health snapshot",
|
||||
condition = "Condition",
|
||||
recovery = "Recovery",
|
||||
currentHealth = "Current health",
|
||||
conditions = { good = "Good", fair = "Fair", low = "Low" },
|
||||
trends = {
|
||||
title = "Trends", week = "Week", month = "Month", total = "{count} steps",
|
||||
more = "{count}% more than last week", less = "{count}% less than last week",
|
||||
same = "Same as last week", dailyAverage = "Daily average", activeTime = "Active time",
|
||||
dailyActivity = "Daily activity", goal = "Goal",
|
||||
},
|
||||
medicalId = {
|
||||
title = "Medical ID", edit = "Edit", done = "Done", resident = "Los Santos resident",
|
||||
emergencyInformation = "Emergency information", bloodType = "Blood type",
|
||||
allergies = "Allergies", conditions = "Conditions", medication = "Medication",
|
||||
noneRecorded = "None recorded", emergencyContact = "Emergency contact",
|
||||
contactName = "Contact name", relation = "Relation", phoneNumber = "Phone number",
|
||||
emergencyCall = "Emergency call", callContact = "Call emergency contact",
|
||||
privacy = "Information is stored with your character and can be shown to emergency services.",
|
||||
saveFailed = "Medical ID could not be saved.",
|
||||
},
|
||||
errors = {
|
||||
invalid_phone_number = "Enter a valid in-game phone number.",
|
||||
invalid_request = "Check the Medical ID fields.",
|
||||
rate_limited = "Please wait before trying again.",
|
||||
request_failed = "Health could not complete the request.",
|
||||
},
|
||||
},
|
||||
customApps = {
|
||||
loading = "Opening app...",
|
||||
unavailableTitle = "App unavailable",
|
||||
|
||||
@@ -37,6 +37,7 @@ client_scripts {
|
||||
'source/client/skyride.lua',
|
||||
'source/client/housing.lua',
|
||||
'source/client/crewlink.lua',
|
||||
'source/client/health.lua',
|
||||
'source/bridge/client/radio.lua',
|
||||
'source/client/payphones.lua',
|
||||
'source/client/custom_apps.lua',
|
||||
@@ -87,6 +88,7 @@ server_scripts {
|
||||
'source/server/notes.lua',
|
||||
'source/server/mail.lua',
|
||||
'source/server/banking.lua',
|
||||
'source/server/health.lua',
|
||||
'source/server/billing.lua',
|
||||
'source/server/garage.lua',
|
||||
'source/server/housing.lua',
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
local pending_steps = 0
|
||||
local pending_distance = 0.0
|
||||
local pending_active_seconds = 0.0
|
||||
local step_progress = 0.0
|
||||
local last_coords = nil
|
||||
local last_report_at = GetGameTimer()
|
||||
|
||||
local function health_snapshot()
|
||||
local ped = PlayerPedId()
|
||||
if not DoesEntityExist(ped) then
|
||||
return { healthPercent = 0 }
|
||||
end
|
||||
|
||||
local health = GetEntityHealth(ped)
|
||||
local maximum = math.max(1, GetEntityMaxHealth(ped))
|
||||
local base = maximum > 100 and 100 or 0
|
||||
local percentage = math.floor(math.max(0, math.min(100, (health - base) / (maximum - base) * 100)) + 0.5)
|
||||
return { healthPercent = percentage }
|
||||
end
|
||||
|
||||
local function flush_activity()
|
||||
last_report_at = GetGameTimer()
|
||||
local distance_meters = math.floor(pending_distance + 0.5)
|
||||
local active_seconds = math.floor(pending_active_seconds + 0.5)
|
||||
if pending_steps == 0 and distance_meters == 0 and active_seconds == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
TriggerServerEvent("sky_phone:health:record-activity", {
|
||||
steps = pending_steps,
|
||||
distanceMeters = distance_meters,
|
||||
activeSeconds = active_seconds,
|
||||
})
|
||||
pending_steps = 0
|
||||
pending_distance = 0.0
|
||||
pending_active_seconds = 0.0
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(Config.Health.SampleIntervalMs)
|
||||
|
||||
local ped = PlayerPedId()
|
||||
if DoesEntityExist(ped) then
|
||||
local coords = GetEntityCoords(ped)
|
||||
local is_moving_on_foot = IsPedOnFoot(ped)
|
||||
and not IsPedDeadOrDying(ped, true)
|
||||
and not IsPedFalling(ped)
|
||||
and not IsPedRagdoll(ped)
|
||||
and (IsPedWalking(ped) or IsPedRunning(ped) or IsPedSprinting(ped))
|
||||
|
||||
if last_coords and is_moving_on_foot then
|
||||
local delta_x = coords.x - last_coords.x
|
||||
local delta_y = coords.y - last_coords.y
|
||||
local distance = math.sqrt(delta_x * delta_x + delta_y * delta_y)
|
||||
local maximum_sample_distance = Config.Health.MaximumSpeedMetersPerSecond
|
||||
* Config.Health.SampleIntervalMs / 1000.0
|
||||
if distance >= 0.04 and distance <= maximum_sample_distance then
|
||||
local stride_length = 0.75
|
||||
if IsPedSprinting(ped) then
|
||||
stride_length = 1.15
|
||||
elseif IsPedRunning(ped) then
|
||||
stride_length = 1.0
|
||||
end
|
||||
|
||||
pending_distance = pending_distance + distance
|
||||
pending_active_seconds = pending_active_seconds + Config.Health.SampleIntervalMs / 1000.0
|
||||
step_progress = step_progress + distance
|
||||
while step_progress >= stride_length do
|
||||
step_progress = step_progress - stride_length
|
||||
pending_steps = pending_steps + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
last_coords = coords
|
||||
else
|
||||
last_coords = nil
|
||||
end
|
||||
|
||||
if GetGameTimer() - last_report_at >= Config.Health.ReportIntervalSeconds * 1000 then
|
||||
flush_activity()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler("playerSpawned", function()
|
||||
last_coords = nil
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
if resource_name == GetCurrentResourceName() then
|
||||
flush_activity()
|
||||
end
|
||||
end)
|
||||
|
||||
RegisterNUICallback("health:overview", function(data, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:health:overview", data or {})
|
||||
if result and result.success and result.data then
|
||||
result.data.snapshot = health_snapshot()
|
||||
end
|
||||
cb(result or { success = false, error = "request_failed" })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("health:save-profile", function(data, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:health:save-profile", data or {})
|
||||
cb(result or { success = false, error = "request_failed" })
|
||||
end)
|
||||
|
||||
RegisterNetEvent("sky_phone:health:changed", function()
|
||||
SendNUIMessage({ type = "health:changed" })
|
||||
end)
|
||||
@@ -532,6 +532,43 @@ local schema = {
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_health_daily",
|
||||
columns = {
|
||||
{ name = "id", type = "BIGINT UNSIGNED NOT NULL AUTO_INCREMENT" },
|
||||
{ name = "owner_identifier", type = "VARCHAR(80) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "activity_date", type = "DATE NOT NULL" },
|
||||
{ name = "steps", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
{ name = "distance_meters", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
{ name = "active_seconds", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
{ name = "energy_kcal", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
|
||||
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "id",
|
||||
uniqueKeys = {
|
||||
{ name = "uniq_sky_phone_health_daily", columns = "(`owner_identifier`, `activity_date`)" },
|
||||
},
|
||||
indexes = {
|
||||
{ name = "idx_sky_phone_health_history", columns = "(`owner_identifier`, `activity_date`)" },
|
||||
},
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_health_profiles",
|
||||
columns = {
|
||||
{ name = "owner_identifier", type = "VARCHAR(80) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "blood_type", type = "VARCHAR(3) NOT NULL DEFAULT ''", characterSet = "ascii", collation = "ascii_general_ci" },
|
||||
{ name = "allergies", type = "VARCHAR(500) NOT NULL DEFAULT ''" },
|
||||
{ name = "conditions", type = "VARCHAR(500) NOT NULL DEFAULT ''" },
|
||||
{ name = "medication", type = "VARCHAR(500) NOT NULL DEFAULT ''" },
|
||||
{ name = "emergency_name", type = "VARCHAR(80) NOT NULL DEFAULT ''" },
|
||||
{ name = "emergency_relation", type = "VARCHAR(40) NOT NULL DEFAULT ''" },
|
||||
{ name = "emergency_phone", type = "VARCHAR(24) NOT NULL DEFAULT ''", characterSet = "ascii", collation = "ascii_bin" },
|
||||
{ name = "updated_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" },
|
||||
},
|
||||
primaryKey = "owner_identifier",
|
||||
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
},
|
||||
{
|
||||
name = "sky_phone_billing_invoices",
|
||||
columns = {
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
Bridge.Database.AfterMigration("sky_phone", function()
|
||||
|
||||
local activity_reports = {}
|
||||
local allowed_blood_types = {
|
||||
[""] = true,
|
||||
["A+"] = true,
|
||||
["A-"] = true,
|
||||
["B+"] = true,
|
||||
["B-"] = true,
|
||||
["AB+"] = true,
|
||||
["AB-"] = true,
|
||||
["O+"] = true,
|
||||
["O-"] = true,
|
||||
}
|
||||
|
||||
local function character_identifier(source, require_session)
|
||||
if require_session then
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return nil, error_response
|
||||
end
|
||||
end
|
||||
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if type(identifier) ~= "string" or identifier == "" then
|
||||
return nil, { success = false, error = "health_unavailable" }
|
||||
end
|
||||
return identifier
|
||||
end
|
||||
|
||||
local function player_name(source)
|
||||
local firstname = Bridge.Framework.GetFirstname(source)
|
||||
local lastname = Bridge.Framework.GetLastname(source)
|
||||
local name = ((firstname or "") .. " " .. (lastname or "")):match("^%s*(.-)%s*$")
|
||||
if name == "" then
|
||||
return GetPlayerName(source) or ("Player %s"):format(source)
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
local function clean_text(value, maximum_length)
|
||||
if type(value) ~= "string" or value:find("[%z\1-\31]") then
|
||||
return nil
|
||||
end
|
||||
local cleaned = value:match("^%s*(.-)%s*$")
|
||||
if #cleaned > maximum_length then
|
||||
return nil
|
||||
end
|
||||
return cleaned
|
||||
end
|
||||
|
||||
local function empty_profile(source)
|
||||
return {
|
||||
playerName = player_name(source),
|
||||
bloodType = "",
|
||||
allergies = "",
|
||||
conditions = "",
|
||||
medication = "",
|
||||
emergencyName = "",
|
||||
emergencyRelation = "",
|
||||
emergencyPhone = "",
|
||||
}
|
||||
end
|
||||
|
||||
local function profile_for(source, identifier)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT `blood_type`, `allergies`, `conditions`, `medication`,
|
||||
`emergency_name`, `emergency_relation`, `emergency_phone`
|
||||
FROM `sky_phone_health_profiles`
|
||||
WHERE `owner_identifier` = ?
|
||||
LIMIT 1
|
||||
]], { identifier })
|
||||
local row = rows[1]
|
||||
if not row then
|
||||
return empty_profile(source)
|
||||
end
|
||||
return {
|
||||
playerName = player_name(source),
|
||||
bloodType = row.blood_type or "",
|
||||
allergies = row.allergies or "",
|
||||
conditions = row.conditions or "",
|
||||
medication = row.medication or "",
|
||||
emergencyName = row.emergency_name or "",
|
||||
emergencyRelation = row.emergency_relation or "",
|
||||
emergencyPhone = row.emergency_phone or "",
|
||||
}
|
||||
end
|
||||
|
||||
local function day_key(timestamp)
|
||||
return os.date("%Y-%m-%d", timestamp)
|
||||
end
|
||||
|
||||
local function activity_history(identifier)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT DATE_FORMAT(`activity_date`, '%Y-%m-%d') AS `activity_date`,
|
||||
`steps`, `distance_meters`, `active_seconds`, `energy_kcal`
|
||||
FROM `sky_phone_health_daily`
|
||||
WHERE `owner_identifier` = ?
|
||||
AND `activity_date` >= DATE_SUB(CURDATE(), INTERVAL 13 DAY)
|
||||
ORDER BY `activity_date` ASC
|
||||
]], { identifier })
|
||||
local by_date = {}
|
||||
for _, row in ipairs(rows) do
|
||||
by_date[row.activity_date] = row
|
||||
end
|
||||
|
||||
local now = os.time()
|
||||
local days = {}
|
||||
local previous_week_steps = 0
|
||||
for days_ago = 13, 0, -1 do
|
||||
local date = day_key(now - days_ago * 86400)
|
||||
local row = by_date[date]
|
||||
local item = {
|
||||
date = date,
|
||||
steps = math.max(0, tonumber(row and row.steps) or 0),
|
||||
distanceMeters = math.max(0, tonumber(row and row.distance_meters) or 0),
|
||||
activeSeconds = math.max(0, tonumber(row and row.active_seconds) or 0),
|
||||
energyKcal = math.max(0, tonumber(row and row.energy_kcal) or 0),
|
||||
}
|
||||
if days_ago >= 7 then
|
||||
previous_week_steps = previous_week_steps + item.steps
|
||||
else
|
||||
days[#days + 1] = item
|
||||
end
|
||||
end
|
||||
return days, previous_week_steps
|
||||
end
|
||||
|
||||
local function overview(source, identifier)
|
||||
local days, previous_week_steps = activity_history(identifier)
|
||||
return {
|
||||
dailyStepGoal = Config.Health.DailyStepGoal,
|
||||
days = days,
|
||||
previousWeekSteps = previous_week_steps,
|
||||
medicalId = profile_for(source, identifier),
|
||||
emergencyNumber = Config.Health.EmergencyNumber,
|
||||
}
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:health:overview", function(source)
|
||||
if not SkyPhone.AllowOperation(source, "health_overview", 60, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local identifier, error_response = character_identifier(source, true)
|
||||
if not identifier then
|
||||
return error_response
|
||||
end
|
||||
return { success = true, data = overview(source, identifier) }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:health:save-profile", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "health_profile_save", 12, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local identifier, error_response = character_identifier(source, true)
|
||||
if not identifier then
|
||||
return error_response
|
||||
end
|
||||
if type(data) ~= "table" then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
|
||||
local blood_type = clean_text(data.bloodType, 3)
|
||||
local allergies = clean_text(data.allergies, Config.Health.ProfileTextMaxLength)
|
||||
local conditions = clean_text(data.conditions, Config.Health.ProfileTextMaxLength)
|
||||
local medication = clean_text(data.medication, Config.Health.ProfileTextMaxLength)
|
||||
local emergency_name = clean_text(data.emergencyName, 80)
|
||||
local emergency_relation = clean_text(data.emergencyRelation, 40)
|
||||
local emergency_phone = clean_text(data.emergencyPhone, 24)
|
||||
if not blood_type or not allowed_blood_types[blood_type]
|
||||
or not allergies or not conditions or not medication
|
||||
or not emergency_name or not emergency_relation or not emergency_phone
|
||||
then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
if emergency_phone ~= "" then
|
||||
emergency_phone = SkyPhoneSimNumber.Normalize(
|
||||
emergency_phone,
|
||||
Config.Sim.NumberLength,
|
||||
Config.Sim.NumberPrefix
|
||||
)
|
||||
if not emergency_phone then
|
||||
return { success = false, error = "invalid_phone_number" }
|
||||
end
|
||||
end
|
||||
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_health_profiles`
|
||||
(`owner_identifier`, `blood_type`, `allergies`, `conditions`, `medication`,
|
||||
`emergency_name`, `emergency_relation`, `emergency_phone`)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`blood_type` = VALUES(`blood_type`),
|
||||
`allergies` = VALUES(`allergies`),
|
||||
`conditions` = VALUES(`conditions`),
|
||||
`medication` = VALUES(`medication`),
|
||||
`emergency_name` = VALUES(`emergency_name`),
|
||||
`emergency_relation` = VALUES(`emergency_relation`),
|
||||
`emergency_phone` = VALUES(`emergency_phone`)
|
||||
]], {
|
||||
identifier,
|
||||
blood_type,
|
||||
allergies,
|
||||
conditions,
|
||||
medication,
|
||||
emergency_name,
|
||||
emergency_relation,
|
||||
emergency_phone,
|
||||
})
|
||||
return { success = true, data = profile_for(source, identifier) }
|
||||
end)
|
||||
|
||||
RegisterNetEvent("sky_phone:health:record-activity", function(data)
|
||||
local player_source = source
|
||||
if type(data) ~= "table"
|
||||
or type(data.steps) ~= "number" or data.steps ~= math.floor(data.steps)
|
||||
or type(data.distanceMeters) ~= "number" or data.distanceMeters ~= math.floor(data.distanceMeters)
|
||||
or type(data.activeSeconds) ~= "number" or data.activeSeconds ~= math.floor(data.activeSeconds)
|
||||
or data.steps < 0 or data.distanceMeters < 0 or data.activeSeconds < 0
|
||||
then
|
||||
Bridge.Debug("warn", "[sky_phone] Rejected malformed health activity from source %s.", tostring(player_source))
|
||||
return
|
||||
end
|
||||
if not SkyPhone.AllowOperation(player_source, "health_activity", Config.Health.ReportsPerMinute, 60) then
|
||||
return
|
||||
end
|
||||
|
||||
local identifier = character_identifier(player_source, false)
|
||||
if not identifier then
|
||||
return
|
||||
end
|
||||
local now = os.time()
|
||||
local previous = activity_reports[player_source]
|
||||
local elapsed = previous and math.max(1, now - previous) or Config.Health.ReportIntervalSeconds + 5
|
||||
activity_reports[player_source] = now
|
||||
local maximum_distance = math.ceil(elapsed * Config.Health.MaximumSpeedMetersPerSecond)
|
||||
local maximum_steps = math.ceil(data.distanceMeters / 0.35) + 4
|
||||
if data.distanceMeters > maximum_distance
|
||||
or data.activeSeconds > elapsed + 5
|
||||
or data.distanceMeters > data.activeSeconds * Config.Health.MaximumSpeedMetersPerSecond + 5
|
||||
or data.steps > maximum_steps
|
||||
then
|
||||
Bridge.Debug("warn", "[sky_phone] Rejected implausible health activity from source %s.", tostring(player_source))
|
||||
return
|
||||
end
|
||||
if data.steps == 0 and data.distanceMeters == 0 and data.activeSeconds == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local energy_kcal = math.floor(data.distanceMeters * 0.06 + 0.5)
|
||||
Bridge.Database.Query([[
|
||||
INSERT INTO `sky_phone_health_daily`
|
||||
(`owner_identifier`, `activity_date`, `steps`, `distance_meters`, `active_seconds`, `energy_kcal`)
|
||||
VALUES (?, CURDATE(), ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`steps` = `steps` + VALUES(`steps`),
|
||||
`distance_meters` = `distance_meters` + VALUES(`distance_meters`),
|
||||
`active_seconds` = `active_seconds` + VALUES(`active_seconds`),
|
||||
`energy_kcal` = `energy_kcal` + VALUES(`energy_kcal`)
|
||||
]], { identifier, data.steps, data.distanceMeters, data.activeSeconds, energy_kcal })
|
||||
TriggerClientEvent("sky_phone:health:changed", player_source)
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
activity_reports[source] = nil
|
||||
end)
|
||||
|
||||
end)
|
||||
@@ -267,6 +267,33 @@ CREATE TABLE IF NOT EXISTS `sky_phone_bank_transactions` (
|
||||
KEY `idx_sky_phone_bank_reference` (`reference`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_health_daily` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`activity_date` DATE NOT NULL,
|
||||
`steps` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`distance_meters` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`active_seconds` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`energy_kcal` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_sky_phone_health_daily` (`owner_identifier`, `activity_date`),
|
||||
KEY `idx_sky_phone_health_history` (`owner_identifier`, `activity_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_health_profiles` (
|
||||
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`blood_type` VARCHAR(3) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '',
|
||||
`allergies` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`conditions` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`medication` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`emergency_name` VARCHAR(80) NOT NULL DEFAULT '',
|
||||
`emergency_relation` VARCHAR(40) NOT NULL DEFAULT '',
|
||||
`emergency_phone` VARCHAR(24) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`owner_identifier`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sky_phone_billing_invoices` (
|
||||
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`recipient_identifier` VARCHAR(80) NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user