timetracker update

This commit is contained in:
Daniel Goc
2026-03-11 09:33:36 +01:00
parent bbf8a2c133
commit 9ef4bb219b
121 changed files with 4328 additions and 2231 deletions

View File

View File

@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "timeTracker API", "title": "timeTracker API",
"description": "Authentication and user management API", "description": "Authentication, user management, and repository time tracking API",
"version": "1.0.0", "version": "1.0.0",
"contact": { "contact": {
"name": "API Support", "name": "API Support",
@@ -22,15 +22,15 @@
}, },
{ {
"name": "Auth", "name": "Auth",
"description": "Authentication endpoints" "description": "Authentication endpoints (under /api/v1/public/auth)"
}, },
{ {
"name": "Languages", "name": "Languages",
"description": "Language and translation endpoints" "description": "Language and translation endpoints"
}, },
{ {
"name": "Protected", "name": "Repo",
"description": "Protected routes requiring authentication" "description": "Repository time tracking data endpoints (under /api/v1/restricted/repo, requires authentication)"
}, },
{ {
"name": "Admin", "name": "Admin",
@@ -208,11 +208,11 @@
} }
} }
}, },
"/api/v1/auth/login": { "/api/v1/public/auth/login": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "User login", "summary": "User login",
"description": "Authenticate a user with email and password", "description": "Authenticate a user with email and password. Sets HTTPOnly cookies (access_token, refresh_token, is_authenticated) on success.",
"operationId": "login", "operationId": "login",
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -239,12 +239,12 @@
"schema": { "schema": {
"type": "string" "type": "string"
}, },
"description": "HTTP-only cookies containing access and refresh tokens" "description": "HTTPOnly cookies: access_token, refresh_token (opaque), is_authenticated (non-HTTPOnly flag)"
} }
} }
}, },
"400": { "400": {
"description": "Invalid request body", "description": "Invalid request body or missing fields",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -276,11 +276,11 @@
} }
} }
}, },
"/api/v1/auth/register": { "/api/v1/public/auth/register": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "User registration", "summary": "User registration",
"description": "Register a new user account", "description": "Register a new user account. Sends a verification email. first_name and last_name are required.",
"operationId": "register", "operationId": "register",
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -310,7 +310,17 @@
} }
}, },
"400": { "400": {
"description": "Invalid request or email already exists", "description": "Invalid request, missing required fields, or invalid password format",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Email already exists",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -322,11 +332,11 @@
} }
} }
}, },
"/api/v1/auth/complete-registration": { "/api/v1/public/auth/complete-registration": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "Complete registration", "summary": "Complete registration",
"description": "Complete registration after email verification", "description": "Complete registration after email verification using the token sent by email. Sets auth cookies on success.",
"operationId": "completeRegistration", "operationId": "completeRegistration",
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -347,10 +357,18 @@
"$ref": "#/components/schemas/AuthResponse" "$ref": "#/components/schemas/AuthResponse"
} }
} }
},
"headers": {
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
} }
}, },
"400": { "400": {
"description": "Invalid token", "description": "Invalid or expired token",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -362,11 +380,11 @@
} }
} }
}, },
"/api/v1/auth/forgot-password": { "/api/v1/public/auth/forgot-password": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "Request password reset", "summary": "Request password reset",
"description": "Request a password reset email", "description": "Request a password reset email. Always returns success to prevent email enumeration.",
"operationId": "forgotPassword", "operationId": "forgotPassword",
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -404,7 +422,7 @@
} }
}, },
"400": { "400": {
"description": "Invalid request", "description": "Invalid request or missing email",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -416,11 +434,11 @@
} }
} }
}, },
"/api/v1/auth/reset-password": { "/api/v1/public/auth/reset-password": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "Reset password", "summary": "Reset password",
"description": "Reset password using reset token", "description": "Reset password using reset token from email. Also revokes all existing refresh tokens for the user.",
"operationId": "resetPassword", "operationId": "resetPassword",
"requestBody": { "requestBody": {
"required": true, "required": true,
@@ -450,7 +468,7 @@
} }
}, },
"400": { "400": {
"description": "Invalid or expired token", "description": "Invalid or expired token, or invalid password format",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -462,11 +480,11 @@
} }
} }
}, },
"/api/v1/auth/logout": { "/api/v1/public/auth/logout": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "User logout", "summary": "User logout",
"description": "Clear authentication cookies", "description": "Revokes the refresh token from the database and clears all authentication cookies.",
"operationId": "logout", "operationId": "logout",
"responses": { "responses": {
"200": { "200": {
@@ -488,11 +506,11 @@
} }
} }
}, },
"/api/v1/auth/refresh": { "/api/v1/public/auth/refresh": {
"post": { "post": {
"tags": ["Auth"], "tags": ["Auth"],
"summary": "Refresh access token", "summary": "Refresh access token",
"description": "Get a new access token using refresh token", "description": "Get a new access token using the refresh token. The refresh token is read from the HTTPOnly cookie first, then from the request body as fallback. Rotates the refresh token on success.",
"operationId": "refreshToken", "operationId": "refreshToken",
"requestBody": { "requestBody": {
"content": { "content": {
@@ -502,7 +520,7 @@
"properties": { "properties": {
"refresh_token": { "refresh_token": {
"type": "string", "type": "string",
"description": "Refresh token from login response" "description": "Opaque refresh token (fallback if cookie not available)"
} }
} }
} }
@@ -518,6 +536,14 @@
"$ref": "#/components/schemas/AuthResponse" "$ref": "#/components/schemas/AuthResponse"
} }
} }
},
"headers": {
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "Rotated HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
} }
}, },
"400": { "400": {
@@ -543,27 +569,25 @@
} }
} }
}, },
"/api/v1/protected/dashboard": { "/api/v1/public/auth/me": {
"get": { "get": {
"tags": ["Protected"], "tags": ["Auth"],
"summary": "Get dashboard data", "summary": "Get current user",
"description": "Protected route requiring authentication", "description": "Returns the currently authenticated user's session information. Requires authentication via cookie.",
"operationId": "getMe",
"security": [ "security": [
{ {
"BearerAuth": [] "CookieAuth": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Dashboard data", "description": "Current user info",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"message": {
"type": "string"
},
"user": { "user": {
"$ref": "#/components/schemas/UserSession" "$ref": "#/components/schemas/UserSession"
} }
@@ -585,28 +609,143 @@
} }
} }
}, },
"/api/v1/admin/users": { "/api/v1/public/auth/google": {
"get": { "get": {
"tags": ["Admin"], "tags": ["Auth"],
"summary": "Get all users", "summary": "Google OAuth2 login",
"description": "Admin-only endpoint for user management", "description": "Redirects the user to Google's OAuth2 consent page. Sets a short-lived oauth_state cookie for CSRF protection.",
"operationId": "googleLogin",
"responses": {
"302": {
"description": "Redirect to Google OAuth2 consent page",
"headers": {
"Location": {
"schema": {
"type": "string"
},
"description": "Google OAuth2 authorization URL"
},
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly oauth_state cookie for CSRF protection (10 min expiry)"
}
}
},
"500": {
"description": "Failed to generate OAuth state",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/public/auth/google/callback": {
"get": {
"tags": ["Auth"],
"summary": "Google OAuth2 callback",
"description": "Handles the OAuth2 callback from Google. Validates state, exchanges code for tokens, creates or updates user, sets auth cookies, and redirects to the app.",
"operationId": "googleCallback",
"parameters": [
{
"name": "code",
"in": "query",
"description": "Authorization code from Google",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "state",
"in": "query",
"description": "State token for CSRF validation",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"302": {
"description": "Redirect to app after successful authentication",
"headers": {
"Location": {
"schema": {
"type": "string"
},
"description": "Redirect to /{lang} (user's preferred language)"
},
"Set-Cookie": {
"schema": {
"type": "string"
},
"description": "HTTPOnly cookies: access_token, refresh_token, is_authenticated"
}
}
},
"400": {
"description": "Invalid state (CSRF) or missing authorization code",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Google OAuth callback processing error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-repos": {
"get": {
"tags": ["Repo"],
"summary": "Get accessible repositories",
"description": "Returns a list of repository IDs that the authenticated user has access to.",
"operationId": "getRepos",
"security": [ "security": [
{ {
"BearerAuth": [] "CookieAuth": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "List of users", "description": "List of repository IDs",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "array",
"properties": { "items": {
"message": { "type": "integer",
"type": "string" "format": "uint"
} },
} "example": [1, 2, 5]
}
}
}
},
"400": {
"description": "Invalid user session",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
} }
} }
} }
@@ -620,9 +759,235 @@
} }
} }
} }
}
}
}
},
"/api/v1/restricted/repo/get-years": {
"get": {
"tags": ["Repo"],
"summary": "Get available years for a repository",
"description": "Returns a list of years for which tracked time data exists in the given repository. User must have access to the repository.",
"operationId": "getYears",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
}
],
"responses": {
"200": {
"description": "List of years with tracked time data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "uint"
},
"example": [2023, 2024, 2025]
}
}
}
}, },
"403": { "400": {
"description": "Admin access required", "description": "Invalid repoID parameter or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-quarters": {
"get": {
"tags": ["Repo"],
"summary": "Get quarterly time data for a repository",
"description": "Returns time tracked per quarter for the given repository and year. All 4 quarters are returned; quarters with no data have time=0. User must have access to the repository.",
"operationId": "getQuarters",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
},
{
"name": "year",
"in": "query",
"description": "Year to retrieve quarterly data for",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 2024
}
}
],
"responses": {
"200": {
"description": "Quarterly time data",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/QuarterData"
}
}
}
}
},
"400": {
"description": "Invalid repoID or year parameter, or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/v1/restricted/repo/get-issues": {
"get": {
"tags": ["Repo"],
"summary": "Get issues with time summaries",
"description": "Returns a paginated list of issues with time tracking summaries for the given repository, year, and quarter. User must have access to the repository.",
"operationId": "getIssues",
"security": [
{
"CookieAuth": []
}
],
"parameters": [
{
"name": "repoID",
"in": "query",
"description": "Repository ID",
"required": true,
"schema": {
"type": "integer",
"format": "uint"
}
},
{
"name": "year",
"in": "query",
"description": "Year to filter issues by",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 2024
}
},
{
"name": "quarter",
"in": "query",
"description": "Quarter number (1-4) to filter issues by",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"minimum": 1,
"maximum": 4,
"example": 2
}
},
{
"name": "page_number",
"in": "query",
"description": "Page number for pagination (1-based)",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 1
}
},
{
"name": "elements_per_page",
"in": "query",
"description": "Number of items per page",
"required": true,
"schema": {
"type": "integer",
"format": "uint",
"example": 30
}
}
],
"responses": {
"200": {
"description": "Paginated list of issues with time summaries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedIssues"
}
}
}
},
"400": {
"description": "Invalid parameters or user does not have access to the repository",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Not authenticated",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -675,7 +1040,13 @@
}, },
"RegisterRequest": { "RegisterRequest": {
"type": "object", "type": "object",
"required": ["email", "password", "confirm_password"], "required": [
"email",
"password",
"confirm_password",
"first_name",
"last_name"
],
"properties": { "properties": {
"email": { "email": {
"type": "string", "type": "string",
@@ -685,7 +1056,7 @@
"password": { "password": {
"type": "string", "type": "string",
"format": "password", "format": "password",
"description": "User's password (min 8 chars, uppercase, lowercase, digit)" "description": "User's password (must meet complexity requirements: min 8 chars, uppercase, lowercase, digit)"
}, },
"confirm_password": { "confirm_password": {
"type": "string", "type": "string",
@@ -694,15 +1065,15 @@
}, },
"first_name": { "first_name": {
"type": "string", "type": "string",
"description": "User's first name" "description": "User's first name (required)"
}, },
"last_name": { "last_name": {
"type": "string", "type": "string",
"description": "User's last name" "description": "User's last name (required)"
}, },
"lang": { "lang": {
"type": "string", "type": "string",
"description": "User's preferred language (e.g., 'en', 'pl', 'cs')" "description": "User's preferred language ISO code (e.g., 'en', 'pl', 'cs')"
} }
} }
}, },
@@ -712,7 +1083,7 @@
"properties": { "properties": {
"token": { "token": {
"type": "string", "type": "string",
"description": "Email verification token" "description": "Email verification token received via email"
} }
} }
}, },
@@ -722,12 +1093,12 @@
"properties": { "properties": {
"token": { "token": {
"type": "string", "type": "string",
"description": "Password reset token" "description": "Password reset token received via email"
}, },
"password": { "password": {
"type": "string", "type": "string",
"format": "password", "format": "password",
"description": "New password" "description": "New password (must meet complexity requirements)"
} }
} }
}, },
@@ -738,17 +1109,13 @@
"type": "string", "type": "string",
"description": "JWT access token" "description": "JWT access token"
}, },
"refresh_token": {
"type": "string",
"description": "JWT refresh token"
},
"token_type": { "token_type": {
"type": "string", "type": "string",
"example": "Bearer" "example": "Bearer"
}, },
"expires_in": { "expires_in": {
"type": "integer", "type": "integer",
"description": "Token expiration in seconds" "description": "Access token expiration in seconds"
}, },
"user": { "user": {
"$ref": "#/components/schemas/UserSession" "$ref": "#/components/schemas/UserSession"
@@ -780,6 +1147,10 @@
}, },
"last_name": { "last_name": {
"type": "string" "type": "string"
},
"lang": {
"type": "string",
"description": "User's preferred language ISO code (e.g., 'en', 'pl', 'cs')"
} }
} }
}, },
@@ -788,7 +1159,7 @@
"properties": { "properties": {
"error": { "error": {
"type": "string", "type": "string",
"description": "Error message" "description": "Translated error message"
} }
} }
}, },
@@ -838,6 +1209,75 @@
} }
} }
}, },
"QuarterData": {
"type": "object",
"description": "Time tracked in a specific quarter",
"properties": {
"time": {
"type": "number",
"format": "double",
"description": "Total hours tracked in this quarter"
},
"quarter": {
"type": "string",
"description": "Quarter identifier in format YYYY_QN (e.g., '2024_Q1')",
"example": "2024_Q1"
}
}
},
"IssueTimeSummary": {
"type": "object",
"description": "Time tracking summary for a single issue",
"properties": {
"issue_id": {
"type": "integer",
"format": "uint",
"description": "Issue ID"
},
"issue_name": {
"type": "string",
"description": "Issue title/name"
},
"user_id": {
"type": "integer",
"format": "uint",
"description": "ID of the user who tracked time"
},
"initials": {
"type": "string",
"description": "Abbreviated initials of the user (e.g., 'J.D.')"
},
"created_date": {
"type": "string",
"format": "date",
"description": "Date when time was tracked"
},
"total_hours_spent": {
"type": "number",
"format": "double",
"description": "Total hours spent on this issue on the given date (rounded to 2 decimal places)"
}
}
},
"PaginatedIssues": {
"type": "object",
"description": "Paginated list of issue time summaries",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/IssueTimeSummary"
},
"description": "List of issue time summaries for the current page"
},
"items_count": {
"type": "integer",
"format": "uint",
"description": "Total number of items across all pages",
"example": 56
}
}
},
"SettingsResponse": { "SettingsResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -872,6 +1312,10 @@
"base_url": { "base_url": {
"type": "string", "type": "string",
"description": "Base URL of the application" "description": "Base URL of the application"
},
"password_regex": {
"type": "string",
"description": "Regular expression for password validation"
} }
} }
}, },
@@ -893,7 +1337,7 @@
"properties": { "properties": {
"jwt_expiration": { "jwt_expiration": {
"type": "integer", "type": "integer",
"description": "JWT token expiration in seconds" "description": "JWT access token expiration in seconds"
}, },
"refresh_expiration": { "refresh_expiration": {
"type": "integer", "type": "integer",
@@ -919,25 +1363,32 @@
"properties": { "properties": {
"version": { "version": {
"type": "string", "type": "string",
"description": "Application version" "description": "Application version (git tag or commit hash)"
}, },
"commit": { "commit": {
"type": "string", "type": "string",
"description": "Git commit hash" "description": "Short git commit hash"
}, },
"date": { "build_date": {
"type": "string", "type": "string",
"description": "Build date" "format": "date-time",
"description": "Build date in RFC3339 format"
} }
} }
} }
}, },
"securitySchemes": { "securitySchemes": {
"CookieAuth": {
"type": "apiKey",
"in": "cookie",
"name": "access_token",
"description": "HTTPOnly JWT access token cookie set during login, registration, or token refresh"
},
"BearerAuth": { "BearerAuth": {
"type": "http", "type": "http",
"scheme": "bearer", "scheme": "bearer",
"bearerFormat": "JWT", "bearerFormat": "JWT",
"description": "JWT token obtained from login response" "description": "JWT token obtained from login response (alternative to cookie-based auth)"
} }
} }
} }

View File

@@ -5,7 +5,7 @@ import (
"log" "log"
"git.ma-al.com/goc_marek/timetracker/app/delivery/web" "git.ma-al.com/goc_marek/timetracker/app/delivery/web"
"git.ma-al.com/goc_marek/timetracker/app/service/langs" "git.ma-al.com/goc_marek/timetracker/app/service/langsService"
"git.ma-al.com/goc_marek/timetracker/app/utils/version" "git.ma-al.com/goc_marek/timetracker/app/utils/version"
) )
@@ -28,7 +28,7 @@ func main() {
} }
// Load translations on startup // Load translations on startup
if err := langs.LangSrv.LoadTranslations(); err != nil { if err := langsService.LangSrv.LoadTranslations(); err != nil {
log.Printf("Warning: Failed to load translations on startup: %v", err) log.Printf("Warning: Failed to load translations on startup: %v", err)
} else { } else {
log.Println("Translations loaded successfully on startup") log.Println("Translations loaded successfully on startup")

View File

@@ -1,11 +0,0 @@
package handler
import (
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public"
"github.com/gofiber/fiber/v3"
)
// AuthHandlerRoutes registers all auth routes
func AuthHandlerRoutes(r fiber.Router) fiber.Router {
return public.AuthHandlerRoutes(r)
}

View File

@@ -1,11 +0,0 @@
package handler
import (
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public"
"github.com/gofiber/fiber/v3"
)
// AuthHandlerRoutes registers all auth routes
func RepoHandlerRoutes(r fiber.Router) fiber.Router {
return public.RepoHandlerRoutes(r)
}

View File

@@ -4,14 +4,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.ma-al.com/goc_marek/timetracker/app/service/langs" "git.ma-al.com/goc_marek/timetracker/app/service/langsService"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
// LanguageMiddleware discovers client's language and stores it in context // LanguageMiddleware discovers client's language and stores it in context
// Priority: Query param > Cookie > Accept-Language header > Default language // Priority: Query param > Cookie > Accept-Language header > Default language
func LanguageMiddleware() fiber.Handler { func LanguageMiddleware() fiber.Handler {
langService := langs.LangSrv langService := langsService.LangSrv
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
var langID uint var langID uint

View File

@@ -1,28 +1,27 @@
package public package api
import ( import (
"strconv" "strconv"
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"
"git.ma-al.com/goc_marek/timetracker/app/service/langs" "git.ma-al.com/goc_marek/timetracker/app/service/langsService"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
type LangHandler struct { type LangHandler struct {
service langs.LangService service langsService.LangService
} }
func NewLangHandler() *LangHandler { func NewLangHandler() *LangHandler {
return &LangHandler{ return &LangHandler{
service: *langs.LangSrv, service: *langsService.LangSrv,
} }
} }
func (h *LangHandler) InitLanguage(api fiber.Router, cfg *config.Config) { func (h *LangHandler) InitLanguage(api fiber.Router, cfg *config.Config) {
api.Get("/langs", h.GetLanguages)
api.Get("langs", h.GetLanguages) api.Get("/translations", h.GetTranslations)
api.Get("translations", h.GetTranslations) api.Get("/translations/reload", h.ReloadTranslations)
api.Get("translations/reload", h.ReloadTranslations)
} }
func (h *LangHandler) GetLanguages(c fiber.Ctx) error { func (h *LangHandler) GetLanguages(c fiber.Ctx) error {

View File

@@ -9,7 +9,7 @@ import (
"git.ma-al.com/goc_marek/timetracker/app/model" "git.ma-al.com/goc_marek/timetracker/app/model"
"git.ma-al.com/goc_marek/timetracker/app/service/authService" "git.ma-al.com/goc_marek/timetracker/app/service/authService"
"git.ma-al.com/goc_marek/timetracker/app/utils/i18n" "git.ma-al.com/goc_marek/timetracker/app/utils/i18n"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -56,22 +56,22 @@ func (h *AuthHandler) Login(c fiber.Ctx) error {
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
// Validate required fields // Validate required fields
if req.Email == "" || req.Password == "" { if req.Email == "" || req.Password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrEmailPasswordRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrEmailPasswordRequired),
}) })
} }
// Attempt login // Attempt login
response, rawRefreshToken, err := h.authService.Login(&req) response, rawRefreshToken, err := h.authService.Login(&req)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -159,14 +159,14 @@ func (h *AuthHandler) ForgotPassword(c fiber.Ctx) error {
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
// Validate email // Validate email
if req.Email == "" { if req.Email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrEmailRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrEmailRequired),
}) })
} }
@@ -187,22 +187,22 @@ func (h *AuthHandler) ResetPassword(c fiber.Ctx) error {
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
// Validate required fields // Validate required fields
if req.Token == "" || req.Password == "" { if req.Token == "" || req.Password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrTokenPasswordRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrTokenPasswordRequired),
}) })
} }
// Reset password (also revokes all refresh tokens for the user) // Reset password (also revokes all refresh tokens for the user)
err := h.authService.ResetPassword(req.Token, req.Password) err := h.authService.ResetPassword(req.Token, req.Password)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -242,7 +242,7 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
if rawRefreshToken == "" { if rawRefreshToken == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrRefreshTokenRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrRefreshTokenRequired),
}) })
} }
@@ -250,8 +250,8 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
if err != nil { if err != nil {
// If refresh token is invalid/expired, clear cookies // If refresh token is invalid/expired, clear cookies
h.clearAuthCookies(c) h.clearAuthCookies(c)
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -266,7 +266,7 @@ func (h *AuthHandler) Me(c fiber.Ctx) error {
user := c.Locals("user") user := c.Locals("user")
if user == nil { if user == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrNotAuthenticated), "error": responseErrors.GetErrorCode(c, responseErrors.ErrNotAuthenticated),
}) })
} }
@@ -281,21 +281,21 @@ func (h *AuthHandler) Register(c fiber.Ctx) error {
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
// Validate required fields // Validate required fields
if req.FirstName == "" || req.LastName == "" { if req.FirstName == "" || req.LastName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrFirstLastNameRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrFirstLastNameRequired),
}) })
} }
// Validate required fields // Validate required fields
if req.Email == "" || req.Password == "" { if req.Email == "" || req.Password == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrEmailPasswordRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrEmailPasswordRequired),
}) })
} }
@@ -303,8 +303,8 @@ func (h *AuthHandler) Register(c fiber.Ctx) error {
err := h.authService.Register(&req) err := h.authService.Register(&req)
if err != nil { if err != nil {
log.Printf("Register error: %v", err) log.Printf("Register error: %v", err)
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -319,22 +319,22 @@ func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
if err := c.Bind().Body(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
// Validate required fields // Validate required fields
if req.Token == "" { if req.Token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrTokenRequired), "error": responseErrors.GetErrorCode(c, responseErrors.ErrTokenRequired),
}) })
} }
// Attempt to complete registration // Attempt to complete registration
response, rawRefreshToken, err := h.authService.CompleteRegistration(&req) response, rawRefreshToken, err := h.authService.CompleteRegistration(&req)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -398,8 +398,8 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code) response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
if err != nil { if err != nil {
log.Printf("Google OAuth callback error: %v", err) log.Printf("Google OAuth callback error: %v", err)
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -412,5 +412,5 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
if lang == "" { if lang == "" {
lang = "en" lang = "en"
} }
return c.Redirect().To(h.config.App.BaseURL + "/" + lang + "/chart") return c.Redirect().To(h.config.App.BaseURL + "/" + lang)
} }

View File

@@ -1,4 +1,4 @@
package public package restricted
import ( import (
"strconv" "strconv"
@@ -6,7 +6,7 @@ import (
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"
"git.ma-al.com/goc_marek/timetracker/app/service/repoService" "git.ma-al.com/goc_marek/timetracker/app/service/repoService"
"git.ma-al.com/goc_marek/timetracker/app/utils/pagination" "git.ma-al.com/goc_marek/timetracker/app/utils/pagination"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
) )
@@ -41,15 +41,15 @@ func RepoHandlerRoutes(r fiber.Router) fiber.Router {
func (h *RepoHandler) GetRepoIDs(c fiber.Ctx) error { func (h *RepoHandler) GetRepoIDs(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
response, err := h.repoService.GetRepositoriesForUser(userID) response, err := h.repoService.GetRepositoriesForUser(userID)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -59,23 +59,23 @@ func (h *RepoHandler) GetRepoIDs(c fiber.Ctx) error {
func (h *RepoHandler) GetYears(c fiber.Ctx) error { func (h *RepoHandler) GetYears(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
repoID_attribute := c.Query("repoID") repoID_attribute := c.Query("repoID")
repoID, err := strconv.Atoi(repoID_attribute) repoID, err := strconv.Atoi(repoID_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
}) })
} }
response, err := h.repoService.GetYearsForUser(userID, uint(repoID)) response, err := h.repoService.GetYearsForUser(userID, uint(repoID))
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -85,31 +85,31 @@ func (h *RepoHandler) GetYears(c fiber.Ctx) error {
func (h *RepoHandler) GetQuarters(c fiber.Ctx) error { func (h *RepoHandler) GetQuarters(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
repoID_attribute := c.Query("repoID") repoID_attribute := c.Query("repoID")
repoID, err := strconv.Atoi(repoID_attribute) repoID, err := strconv.Atoi(repoID_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
}) })
} }
year_attribute := c.Query("year") year_attribute := c.Query("year")
year, err := strconv.Atoi(year_attribute) year, err := strconv.Atoi(year_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadYearAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadYearAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadYearAttribute),
}) })
} }
response, err := h.repoService.GetQuartersForUser(userID, uint(repoID), uint(year)) response, err := h.repoService.GetQuartersForUser(userID, uint(repoID), uint(year))
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }
@@ -119,48 +119,48 @@ func (h *RepoHandler) GetQuarters(c fiber.Ctx) error {
func (h *RepoHandler) GetIssues(c fiber.Ctx) error { func (h *RepoHandler) GetIssues(c fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrInvalidBody), "error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
}) })
} }
repoID_attribute := c.Query("repoID") repoID_attribute := c.Query("repoID")
repoID, err := strconv.Atoi(repoID_attribute) repoID, err := strconv.Atoi(repoID_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
}) })
} }
year_attribute := c.Query("year") year_attribute := c.Query("year")
year, err := strconv.Atoi(year_attribute) year, err := strconv.Atoi(year_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadYearAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadYearAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadYearAttribute),
}) })
} }
quarter_attribute := c.Query("quarter") quarter_attribute := c.Query("quarter")
quarter, err := strconv.Atoi(quarter_attribute) quarter, err := strconv.Atoi(quarter_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadQuarterAttribute)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadQuarterAttribute)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadQuarterAttribute), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadQuarterAttribute),
}) })
} }
page_number_attribute := c.Query("page_number") page_number_attribute := c.Query("page_number")
page_number, err := strconv.Atoi(page_number_attribute) page_number, err := strconv.Atoi(page_number_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadPaging)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadPaging), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadPaging),
}) })
} }
elements_per_page_attribute := c.Query("quarter") elements_per_page_attribute := c.Query("elements_per_page")
elements_per_page, err := strconv.Atoi(elements_per_page_attribute) elements_per_page, err := strconv.Atoi(elements_per_page_attribute)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadPaging)).JSON(fiber.Map{
"error": view.GetErrorCode(c, view.ErrBadPaging), "error": responseErrors.GetErrorCode(c, responseErrors.ErrBadPaging),
}) })
} }
@@ -170,8 +170,8 @@ func (h *RepoHandler) GetIssues(c fiber.Ctx) error {
response, err := h.repoService.GetIssuesForUser(userID, uint(repoID), uint(year), uint(quarter), paging) response, err := h.repoService.GetIssuesForUser(userID, uint(repoID), uint(year), uint(quarter), paging)
if err != nil { if err != nil {
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{ return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
"error": view.GetErrorCode(c, err), "error": responseErrors.GetErrorCode(c, err),
}) })
} }

View File

@@ -1,4 +1,4 @@
package public package api
import ( import (
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"

View File

@@ -1,4 +1,4 @@
package public package general
import ( import (
"git.ma-al.com/goc_marek/timetracker/assets" "git.ma-al.com/goc_marek/timetracker/assets"

View File

@@ -1,4 +1,4 @@
package public package general
import ( import (
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"

View File

@@ -1,4 +1,4 @@
package public package general
import ( import (
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"

View File

@@ -1,4 +1,4 @@
package public package general
import ( import (
"git.ma-al.com/goc_marek/timetracker/app/api" "git.ma-al.com/goc_marek/timetracker/app/api"

View File

@@ -10,9 +10,11 @@ import (
"time" "time"
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"
"git.ma-al.com/goc_marek/timetracker/app/delivery/handler"
"git.ma-al.com/goc_marek/timetracker/app/delivery/middleware" "git.ma-al.com/goc_marek/timetracker/app/delivery/middleware"
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public" "git.ma-al.com/goc_marek/timetracker/app/delivery/web/api"
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/api/public"
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/api/restricted"
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/general"
// "github.com/gofiber/fiber/v2/middleware/filesystem" // "github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
@@ -23,9 +25,11 @@ import (
// Server represents the web server // Server represents the web server
type Server struct { type Server struct {
app *fiber.App app *fiber.App
cfg *config.Config cfg *config.Config
api fiber.Router api fiber.Router
public fiber.Router
restricted fiber.Router
} }
// App returns the fiber app // App returns the fiber app
@@ -61,54 +65,57 @@ func (s *Server) Setup() error {
s.app.Use(middleware.LanguageMiddleware()) s.app.Use(middleware.LanguageMiddleware())
// initialize healthcheck // initialize healthcheck
public.InitHealth(s.App(), s.Cfg()) general.InitHealth(s.App(), s.Cfg())
// serve favicon // serve favicon
public.Favicon(s.app, s.cfg) general.Favicon(s.app, s.cfg)
// initialize swagger endpoints
general.InitSwagger(s.App())
// API routes // API routes
s.api = s.app.Group("/api/v1") s.api = s.app.Group("/api/v1")
s.public = s.api.Group("/public")
s.restricted = s.api.Group("/restricted")
s.restricted.Use(middleware.AuthMiddleware())
// initialize swagger endpoints // initialize language endpoints (general)
public.InitSwagger(s.App()) api.NewLangHandler().InitLanguage(s.api, s.cfg)
// Settings endpoint (general)
api.NewSettingsHandler().InitSettings(s.api, s.cfg)
// Auth routes (public) // Auth routes (public)
auth := s.api.Group("/auth") auth := s.public.Group("/auth")
handler.AuthHandlerRoutes(auth) public.AuthHandlerRoutes(auth)
// Repo routes (public) // Repo routes (restricted)
repo := s.api.Group("/repo") repo := s.restricted.Group("/repo")
repo.Use(middleware.AuthMiddleware()) restricted.RepoHandlerRoutes(repo)
handler.RepoHandlerRoutes(repo)
// Protected routes example // // Restricted routes example
protected := s.api.Group("/restricted") // restricted := s.api.Group("/restricted")
protected.Use(middleware.AuthMiddleware()) // restricted.Use(middleware.AuthMiddleware())
protected.Get("/dashboard", func(c fiber.Ctx) error { // restricted.Get("/dashboard", func(c fiber.Ctx) error {
user := middleware.GetUser(c) // user := middleware.GetUser(c)
return c.JSON(fiber.Map{ // return c.JSON(fiber.Map{
"message": "Welcome to the protected area", // "message": "Welcome to the protected area",
"user": user, // "user": user,
}) // })
}) // })
// Admin routes example // // Admin routes example
admin := s.api.Group("/admin") // admin := s.api.Group("/admin")
admin.Use(middleware.AuthMiddleware()) // admin.Use(middleware.AuthMiddleware())
admin.Use(middleware.RequireAdmin()) // admin.Use(middleware.RequireAdmin())
admin.Get("/users", func(c fiber.Ctx) error { // admin.Get("/users", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{ // return c.JSON(fiber.Map{
"message": "Admin area - user management", // "message": "Admin area - user management",
}) // })
}) // })
public.NewLangHandler().InitLanguage(s.api, s.cfg)
// Settings endpoint
public.NewSettingsHandler().InitSettings(s.api, s.cfg)
// keep this at the end because its wilderange // keep this at the end because its wilderange
public.InitBo(s.App()) general.InitBo(s.App())
return nil return nil
} }

View File

@@ -1,22 +0,0 @@
package model
// LoginRequest represents the login form data
type DataRequest struct {
RepoID uint `json:"repoid" form:"repoid"`
Step uint `json:"step" form:"step"`
}
type PageMeta struct {
Title string
Description string
}
type QuarterData struct {
Time float64 `json:"time"`
Quarter string `json:"quarter"`
}
type DayData struct {
Date string `json:"date"`
Time float64 `json:"time"`
}

View File

@@ -1,15 +1,35 @@
package view package model
import ( import (
"time" "time"
"git.ma-al.com/goc_marek/timetracker/app/model"
"git.ma-al.com/goc_marek/timetracker/app/utils/pagination" "git.ma-al.com/goc_marek/timetracker/app/utils/pagination"
) )
// LoginRequest represents the login form data
type DataRequest struct {
RepoID uint `json:"repoid" form:"repoid"`
Step uint `json:"step" form:"step"`
}
type PageMeta struct {
Title string
Description string
}
type QuarterData struct {
Time float64 `json:"time"`
Quarter string `json:"quarter"`
}
type DayData struct {
Date string `json:"date"`
Time float64 `json:"time"`
}
type RepositoryChartData struct { type RepositoryChartData struct {
Years []uint Years []uint
Quarters []model.QuarterData Quarters []QuarterData
QuartersJSON string QuartersJSON string
Year uint Year uint
} }
@@ -20,7 +40,7 @@ type TimeTrackedData struct {
Quarter uint Quarter uint
Step string Step string
TotalTime float64 TotalTime float64
DailyData []model.DayData DailyData []DayData
DailyDataJSON string DailyDataJSON string
Years []uint Years []uint
IssueSummaries *pagination.Found[IssueTimeSummary] IssueSummaries *pagination.Found[IssueTimeSummary]
@@ -29,8 +49,6 @@ type TimeTrackedData struct {
type IssueTimeSummary struct { type IssueTimeSummary struct {
IssueID uint `gorm:"column:issue_id"` IssueID uint `gorm:"column:issue_id"`
IssueName string `gorm:"column:issue_name"` IssueName string `gorm:"column:issue_name"`
UserID uint `gorm:"column:user_id"` CreatedDate time.Time `gorm:"column:issue_created_at"`
Initials string `gorm:"column:initials"`
CreatedDate time.Time `gorm:"column:created_date"`
TotalHoursSpent float64 `gorm:"column:total_hours_spent"` TotalHoursSpent float64 `gorm:"column:total_hours_spent"`
} }

View File

@@ -1,61 +0,0 @@
package model
import "encoding/json"
type Repository struct {
ID int64 `db:"id"`
OwnerID *int64 `db:"owner_id"`
OwnerName *string `db:"owner_name"`
LowerName string `db:"lower_name"`
Name string `db:"name"`
Description *string `db:"description"`
Website *string `db:"website"`
OriginalServiceType *int `db:"original_service_type"`
OriginalURL *string `db:"original_url"`
DefaultBranch *string `db:"default_branch"`
DefaultWikiBranch *string `db:"default_wiki_branch"`
NumWatches *int `db:"num_watches"`
NumStars *int `db:"num_stars"`
NumForks *int `db:"num_forks"`
NumIssues *int `db:"num_issues"`
NumClosedIssues *int `db:"num_closed_issues"`
NumPulls *int `db:"num_pulls"`
NumClosedPulls *int `db:"num_closed_pulls"`
NumMilestones int `db:"num_milestones"`
NumClosedMilestones int `db:"num_closed_milestones"`
NumProjects int `db:"num_projects"`
NumClosedProjects int `db:"num_closed_projects"`
NumActionRuns int `db:"num_action_runs"`
NumClosedActionRuns int `db:"num_closed_action_runs"`
IsPrivate *bool `db:"is_private"`
IsEmpty *bool `db:"is_empty"`
IsArchived *bool `db:"is_archived"`
IsMirror *bool `db:"is_mirror"`
Status int `db:"status"`
IsFork bool `db:"is_fork"`
ForkID *int64 `db:"fork_id"`
IsTemplate bool `db:"is_template"`
TemplateID *int64 `db:"template_id"`
Size int64 `db:"size"`
GitSize int64 `db:"git_size"`
LFSSize int64 `db:"lfs_size"`
IsFsckEnabled bool `db:"is_fsck_enabled"`
CloseIssuesViaCommitAnyBranch bool `db:"close_issues_via_commit_in_any_branch"`
Topics json.RawMessage `db:"topics"`
ObjectFormatName string `db:"object_format_name"`
TrustModel *int `db:"trust_model"`
Avatar *string `db:"avatar"`
CreatedUnix *int64 `db:"created_unix"`
UpdatedUnix *int64 `db:"updated_unix"`
ArchivedUnix int64 `db:"archived_unix"`
}

View File

@@ -13,7 +13,7 @@ import (
"git.ma-al.com/goc_marek/timetracker/app/model" "git.ma-al.com/goc_marek/timetracker/app/model"
"git.ma-al.com/goc_marek/timetracker/app/service/emailService" "git.ma-al.com/goc_marek/timetracker/app/service/emailService"
constdata "git.ma-al.com/goc_marek/timetracker/app/utils/const_data" constdata "git.ma-al.com/goc_marek/timetracker/app/utils/const_data"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@@ -60,23 +60,23 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
// Find user by email // Find user by email
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil { if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", view.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
return nil, "", fmt.Errorf("database error: %w", err) return nil, "", fmt.Errorf("database error: %w", err)
} }
// Check if user is active // Check if user is active
if !user.IsActive { if !user.IsActive {
return nil, "", view.ErrUserInactive return nil, "", responseErrors.ErrUserInactive
} }
// Check if email is verified // Check if email is verified
if !user.EmailVerified { if !user.EmailVerified {
return nil, "", view.ErrEmailNotVerified return nil, "", responseErrors.ErrEmailNotVerified
} }
// Verify password // Verify password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return nil, "", view.ErrInvalidCredentials return nil, "", responseErrors.ErrInvalidCredentials
} }
// Update last login time // Update last login time
@@ -109,17 +109,17 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
// Check if email already exists // Check if email already exists
var existingUser model.Customer var existingUser model.Customer
if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
return view.ErrEmailExists return responseErrors.ErrEmailExists
} }
// Validate passwords match // Validate passwords match
if req.Password != req.ConfirmPassword { if req.Password != req.ConfirmPassword {
return view.ErrPasswordsDoNotMatch return responseErrors.ErrPasswordsDoNotMatch
} }
// Validate password strength // Validate password strength
if err := validatePassword(req.Password); err != nil { if err := validatePassword(req.Password); err != nil {
return view.ErrInvalidPassword return responseErrors.ErrInvalidPassword
} }
// Hash password // Hash password
@@ -176,14 +176,14 @@ func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationReques
var user model.Customer var user model.Customer
if err := s.db.Where("email_verification_token = ?", req.Token).First(&user).Error; err != nil { if err := s.db.Where("email_verification_token = ?", req.Token).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", view.ErrInvalidVerificationToken return nil, "", responseErrors.ErrInvalidVerificationToken
} }
return nil, "", fmt.Errorf("database error: %w", err) return nil, "", fmt.Errorf("database error: %w", err)
} }
// Check if token is expired // Check if token is expired
if user.EmailVerificationExpires != nil && user.EmailVerificationExpires.Before(time.Now()) { if user.EmailVerificationExpires != nil && user.EmailVerificationExpires.Before(time.Now()) {
return nil, "", view.ErrVerificationTokenExpired return nil, "", responseErrors.ErrVerificationTokenExpired
} }
// Update user - activate account and mark email as verified // Update user - activate account and mark email as verified
@@ -283,19 +283,19 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
var user model.Customer var user model.Customer
if err := s.db.Where("password_reset_token = ?", token).First(&user).Error; err != nil { if err := s.db.Where("password_reset_token = ?", token).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return view.ErrInvalidResetToken return responseErrors.ErrInvalidResetToken
} }
return fmt.Errorf("database error: %w", err) return fmt.Errorf("database error: %w", err)
} }
// Check if token is expired // Check if token is expired
if user.PasswordResetExpires == nil || user.PasswordResetExpires.Before(time.Now()) { if user.PasswordResetExpires == nil || user.PasswordResetExpires.Before(time.Now()) {
return view.ErrResetTokenExpired return responseErrors.ErrResetTokenExpired
} }
// Validate new password // Validate new password
if err := validatePassword(newPassword); err != nil { if err := validatePassword(newPassword); err != nil {
return view.ErrInvalidPassword return responseErrors.ErrInvalidPassword
} }
// Hash new password // Hash new password
@@ -330,14 +330,14 @@ func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
if err != nil { if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) { if errors.Is(err, jwt.ErrTokenExpired) {
return nil, view.ErrTokenExpired return nil, responseErrors.ErrTokenExpired
} }
return nil, view.ErrInvalidToken return nil, responseErrors.ErrInvalidToken
} }
claims, ok := token.Claims.(*JWTClaims) claims, ok := token.Claims.(*JWTClaims)
if !ok || !token.Valid { if !ok || !token.Valid {
return nil, view.ErrInvalidToken return nil, responseErrors.ErrInvalidToken
} }
return claims, nil return claims, nil
@@ -352,7 +352,7 @@ func (s *AuthService) RefreshToken(rawToken string) (*model.AuthResponse, string
var rt model.RefreshToken var rt model.RefreshToken
if err := s.db.Where("token_hash = ?", tokenHash).First(&rt).Error; err != nil { if err := s.db.Where("token_hash = ?", tokenHash).First(&rt).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", view.ErrInvalidToken return nil, "", responseErrors.ErrInvalidToken
} }
return nil, "", fmt.Errorf("database error: %w", err) return nil, "", fmt.Errorf("database error: %w", err)
} }
@@ -361,21 +361,21 @@ func (s *AuthService) RefreshToken(rawToken string) (*model.AuthResponse, string
if rt.ExpiresAt.Before(time.Now()) { if rt.ExpiresAt.Before(time.Now()) {
// Clean up expired token // Clean up expired token
s.db.Delete(&rt) s.db.Delete(&rt)
return nil, "", view.ErrTokenExpired return nil, "", responseErrors.ErrTokenExpired
} }
// Get user from database // Get user from database
var user model.Customer var user model.Customer
if err := s.db.First(&user, rt.CustomerID).Error; err != nil { if err := s.db.First(&user, rt.CustomerID).Error; err != nil {
return nil, "", view.ErrUserNotFound return nil, "", responseErrors.ErrUserNotFound
} }
if !user.IsActive { if !user.IsActive {
return nil, "", view.ErrUserInactive return nil, "", responseErrors.ErrUserInactive
} }
if !user.EmailVerified { if !user.EmailVerified {
return nil, "", view.ErrEmailNotVerified return nil, "", responseErrors.ErrEmailNotVerified
} }
// Delete the old refresh token (rotation: one-time use) // Delete the old refresh token (rotation: one-time use)
@@ -420,7 +420,7 @@ func (s *AuthService) GetUserByID(userID uint) (*model.Customer, error) {
var user model.Customer var user model.Customer
if err := s.db.First(&user, userID).Error; err != nil { if err := s.db.First(&user, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, view.ErrUserNotFound return nil, responseErrors.ErrUserNotFound
} }
return nil, fmt.Errorf("database error: %w", err) return nil, fmt.Errorf("database error: %w", err)
} }
@@ -432,7 +432,7 @@ func (s *AuthService) GetUserByEmail(email string) (*model.Customer, error) {
var user model.Customer var user model.Customer
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil { if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, view.ErrUserNotFound return nil, responseErrors.ErrUserNotFound
} }
return nil, fmt.Errorf("database error: %w", err) return nil, fmt.Errorf("database error: %w", err)
} }

View File

@@ -12,6 +12,7 @@ import (
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"
"git.ma-al.com/goc_marek/timetracker/app/model" "git.ma-al.com/goc_marek/timetracker/app/model"
"git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/view"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
@@ -19,17 +20,6 @@ import (
const googleUserInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo" const googleUserInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo"
// GoogleUserInfo represents the user info returned by Google
type GoogleUserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Picture string `json:"picture"`
}
// googleOAuthConfig returns the OAuth2 config for Google // googleOAuthConfig returns the OAuth2 config for Google
func googleOAuthConfig() *oauth2.Config { func googleOAuthConfig() *oauth2.Config {
cfg := config.Get().OAuth.Google cfg := config.Get().OAuth.Google
@@ -81,7 +71,7 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
} }
if !userInfo.VerifiedEmail { if !userInfo.VerifiedEmail {
return nil, "", view.ErrEmailNotVerified return nil, "", responseErrors.ErrEmailNotVerified
} }
// Find or create user // Find or create user
@@ -117,7 +107,7 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
// findOrCreateGoogleUser finds an existing user by Google provider ID or email, // findOrCreateGoogleUser finds an existing user by Google provider ID or email,
// or creates a new one. // or creates a new one.
func (s *AuthService) findOrCreateGoogleUser(info *GoogleUserInfo) (*model.Customer, error) { func (s *AuthService) findOrCreateGoogleUser(info *view.GoogleUserInfo) (*model.Customer, error) {
var user model.Customer var user model.Customer
// Try to find by provider + provider_id // Try to find by provider + provider_id
@@ -183,7 +173,7 @@ func (s *AuthService) findOrCreateGoogleUser(info *GoogleUserInfo) (*model.Custo
} }
// fetchGoogleUserInfo fetches user info from Google using the provided HTTP client // fetchGoogleUserInfo fetches user info from Google using the provided HTTP client
func fetchGoogleUserInfo(client *http.Client) (*GoogleUserInfo, error) { func fetchGoogleUserInfo(client *http.Client) (*view.GoogleUserInfo, error) {
resp, err := client.Get(googleUserInfoURL) resp, err := client.Get(googleUserInfoURL)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -195,7 +185,7 @@ func fetchGoogleUserInfo(client *http.Client) (*GoogleUserInfo, error) {
return nil, err return nil, err
} }
var userInfo GoogleUserInfo var userInfo view.GoogleUserInfo
if err := json.Unmarshal(body, &userInfo); err != nil { if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, err return nil, err
} }

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"git.ma-al.com/goc_marek/timetracker/app/config" "git.ma-al.com/goc_marek/timetracker/app/config"
"git.ma-al.com/goc_marek/timetracker/app/service/langs" "git.ma-al.com/goc_marek/timetracker/app/service/langsService"
"git.ma-al.com/goc_marek/timetracker/app/templ/emails" "git.ma-al.com/goc_marek/timetracker/app/templ/emails"
"git.ma-al.com/goc_marek/timetracker/app/utils/i18n" "git.ma-al.com/goc_marek/timetracker/app/utils/i18n"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/view"
@@ -32,7 +32,7 @@ func getLangID(isoCode string) uint {
isoCode = "en" isoCode = "en"
} }
lang, err := langs.LangSrv.GetLanguageByISOCode(isoCode) lang, err := langsService.LangSrv.GetLanguageByISOCode(isoCode)
if err != nil || lang == nil { if err != nil || lang == nil {
return 1 // Default to English (ID 1) return 1 // Default to English (ID 1)
} }

View File

@@ -1,4 +1,4 @@
package langs package langsService
import ( import (
langs_repo "git.ma-al.com/goc_marek/timetracker/app/langs" langs_repo "git.ma-al.com/goc_marek/timetracker/app/langs"

View File

@@ -7,7 +7,7 @@ import (
"git.ma-al.com/goc_marek/timetracker/app/db" "git.ma-al.com/goc_marek/timetracker/app/db"
"git.ma-al.com/goc_marek/timetracker/app/model" "git.ma-al.com/goc_marek/timetracker/app/model"
"git.ma-al.com/goc_marek/timetracker/app/utils/pagination" "git.ma-al.com/goc_marek/timetracker/app/utils/pagination"
"git.ma-al.com/goc_marek/timetracker/app/view" "git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -45,7 +45,7 @@ func (s *RepoService) UserHasAccessToRepo(userID uint, repoID uint) (bool, error
} }
if !slices.Contains(repositories, repoID) { if !slices.Contains(repositories, repoID) {
return false, view.ErrInvalidRepoID return false, responseErrors.ErrInvalidRepoID
} }
return true, nil return true, nil
@@ -147,140 +147,19 @@ func (s *RepoService) GetQuarters(repo uint, year uint) ([]model.QuarterData, er
Find(&quarters). Find(&quarters).
Error Error
if err != nil { if err != nil {
fmt.Printf("err: %v\n", err)
return nil, err return nil, err
} }
return quarters, nil return quarters, nil
} }
func (s *RepoService) GetTotalTimeForQuarter(repo uint, year uint, quarter uint) (float64, error) {
var total float64
query := `
SELECT COALESCE(SUM(tt.time) / 3600, 0) AS total_time
FROM tracked_time tt
JOIN issue i ON i.id = tt.issue_id
WHERE i.repo_id = ?
AND EXTRACT(YEAR FROM to_timestamp(tt.created_unix)) = ?
AND EXTRACT(QUARTER FROM to_timestamp(tt.created_unix)) = ?
AND tt.deleted = false
`
err := db.Get().Raw(query, repo, year, quarter).Row().Scan(&total)
if err != nil {
return 0, err
}
return total, nil
}
func (s *RepoService) GetTimeTracked(repo uint, year uint, quarter uint, step string) ([]model.DayData, error) {
var days []model.DayData
// Calculate quarter start and end dates
quarterStartMonth := (quarter-1)*3 + 1
quarterStart := fmt.Sprintf("%d-%02d-01", year, quarterStartMonth)
var quarterEnd string
switch quarter {
case 1:
quarterEnd = fmt.Sprintf("%d-03-31", year)
case 2:
quarterEnd = fmt.Sprintf("%d-06-30", year)
case 3:
quarterEnd = fmt.Sprintf("%d-09-30", year)
default:
quarterEnd = fmt.Sprintf("%d-12-31", year)
}
var bucketExpr string
var seriesInterval string
var seriesStart string
var seriesEnd string
switch step {
case "day":
bucketExpr = "DATE(to_timestamp(tt.created_unix))"
seriesInterval = "1 day"
seriesStart = "p.start_date"
seriesEnd = "p.end_date"
case "week":
bucketExpr = `
(p.start_date +
((DATE(to_timestamp(tt.created_unix)) - p.start_date) / 7) * 7
)::date`
seriesInterval = "7 days"
seriesStart = "p.start_date"
seriesEnd = "p.end_date"
case "month":
bucketExpr = "date_trunc('month', to_timestamp(tt.created_unix))::date"
seriesInterval = "1 month"
seriesStart = "date_trunc('month', p.start_date)"
seriesEnd = "date_trunc('month', p.end_date)"
}
query := fmt.Sprintf(`
WITH params AS (
SELECT ?::date AS start_date, ?::date AS end_date
),
date_range AS (
SELECT generate_series(
%s,
%s,
interval '%s'
)::date AS date
FROM params p
),
data AS (
SELECT
%s AS date,
SUM(tt.time) / 3600 AS time
FROM tracked_time tt
JOIN issue i ON i.id = tt.issue_id
CROSS JOIN params p
WHERE i.repo_id = ?
AND to_timestamp(tt.created_unix) >= p.start_date
AND to_timestamp(tt.created_unix) < p.end_date + interval '1 day'
AND tt.deleted = false
GROUP BY 1
)
SELECT
TO_CHAR(dr.date, 'YYYY-MM-DD') AS date,
COALESCE(d.time, 0) AS time
FROM date_range dr
LEFT JOIN data d ON d.date = dr.date
ORDER BY dr.date
`, seriesStart, seriesEnd, seriesInterval, bucketExpr)
err := db.Get().
Raw(query, quarterStart, quarterEnd, repo).
Scan(&days).Error
if err != nil {
return nil, err
}
return days, nil
}
func (s *RepoService) GetRepoData(repoIds []uint) ([]model.Repository, error) {
var repos []model.Repository
err := db.Get().Model(model.Repository{}).Where("id = ?", repoIds).Find(&repos).Error
if err != nil {
return nil, err
}
return repos, nil
}
func (s *RepoService) GetIssuesForUser( func (s *RepoService) GetIssuesForUser(
userID uint, userID uint,
repoID uint, repoID uint,
year uint, year uint,
quarter uint, quarter uint,
p pagination.Paging, p pagination.Paging,
) (*pagination.Found[view.IssueTimeSummary], error) { ) (*pagination.Found[model.IssueTimeSummary], error) {
if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok { if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok {
return nil, err return nil, err
} }
@@ -293,21 +172,14 @@ func (s *RepoService) GetIssues(
year uint, year uint,
quarter uint, quarter uint,
p pagination.Paging, p pagination.Paging,
) (*pagination.Found[view.IssueTimeSummary], error) { ) (*pagination.Found[model.IssueTimeSummary], error) {
query := db.Get().Debug(). query := db.Get().
Table("issue i"). Table("issue i").
Select(` Select(`
i.id AS issue_id, i.id AS issue_id,
i.name AS issue_name, i.name AS issue_name,
u.id AS user_id, to_timestamp(i.created_unix) AS issue_created_at,
upper(
regexp_replace(
regexp_replace(u.full_name, '(\y\w)\w*', '\1', 'g'),
'(\w)', '\1.', 'g'
)
) AS initials,
to_timestamp(tt.created_unix)::date AS created_date,
ROUND(SUM(tt.time) / 3600.0, 2) AS total_hours_spent ROUND(SUM(tt.time) / 3600.0, 2) AS total_hours_spent
`). `).
Joins(`JOIN tracked_time tt ON tt.issue_id = i.id`). Joins(`JOIN tracked_time tt ON tt.issue_id = i.id`).
@@ -321,12 +193,11 @@ func (s *RepoService) GetIssues(
i.id, i.id,
i.name, i.name,
u.id, u.id,
u.full_name, u.full_name
created_date
`). `).
Order("created_date") Order("i.created_unix")
result, err := pagination.Paginate[view.IssueTimeSummary](p, query) result, err := pagination.Paginate[model.IssueTimeSummary](p, query)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,9 +1,6 @@
package pagination package pagination
import ( import (
"strconv"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -55,9 +52,3 @@ func Paginate[T any](paging Paging, stmt *gorm.DB) (Found[T], error) {
Count: uint(count), Count: uint(count),
}, err }, err
} }
func ParsePagination(c *fiber.Ctx) Paging {
pageNum, _ := strconv.ParseInt((*c).Query("p", "1"), 10, 64)
pageSize, _ := strconv.ParseInt((*c).Query("elems", "10"), 10, 64)
return Paging{Page: uint(pageNum), Elements: uint(pageSize)}
}

View File

@@ -1,4 +1,4 @@
package view package responseErrors
import ( import (
"errors" "errors"

12
app/view/google_oauth.go Normal file
View File

@@ -0,0 +1,12 @@
package view
// GoogleUserInfo represents the user info returned by Google
type GoogleUserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Picture string `json:"picture"`
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{F as e,G as t,Q as n,R as r,d as i,h as a,m as o,p as s,xt as c,yt as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{S as u,n as d,r as f,t as p}from"./tv-uB0-NqWK.js";var m={slots:{root:`rounded-lg overflow-hidden`,header:`p-4 sm:px-6`,body:`p-4 sm:p-6`,footer:`p-4 sm:px-6`},variants:{variant:{solid:{root:`bg-inverted text-inverted`},outline:{root:`bg-default ring ring-default divide-y divide-default`},soft:{root:`bg-elevated/50 divide-y divide-default`},subtle:{root:`bg-elevated/50 ring ring-default divide-y divide-default`}}},defaultVariants:{variant:`outline`}},h={__name:`Card`,props:{as:{type:null,required:!1},variant:{type:null,required:!1},class:{type:null,required:!1},ui:{type:Object,required:!1}},setup(h){let g=h,_=t(),v=u(),y=d(`card`,g),b=i(()=>p({extend:p(m),...v.ui?.card||{}})({variant:g.variant}));return(t,i)=>(e(),s(l(f),{as:h.as,"data-slot":`root`,class:c(b.value.root({class:[l(y)?.root,g.class]}))},{default:n(()=>[_.header?(e(),a(`div`,{key:0,"data-slot":`header`,class:c(b.value.header({class:l(y)?.header}))},[r(t.$slots,`header`)],2)):o(``,!0),_.default?(e(),a(`div`,{key:1,"data-slot":`body`,class:c(b.value.body({class:l(y)?.body}))},[r(t.$slots,`default`)],2)):o(``,!0),_.footer?(e(),a(`div`,{key:2,"data-slot":`footer`,class:c(b.value.footer({class:l(y)?.footer}))},[r(t.$slots,`footer`)],2)):o(``,!0)]),_:3},8,[`as`,`class`]))}};export{h as t};

View File

@@ -1 +0,0 @@
import{F as e,G as t,Q as n,R as r,d as i,h as a,m as o,p as s,xt as c,yt as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{S as u,i as d,n as f,r as p}from"./Icon-Chkiq2IE.js";var m={slots:{root:`rounded-lg overflow-hidden`,header:`p-4 sm:px-6`,body:`p-4 sm:p-6`,footer:`p-4 sm:px-6`},variants:{variant:{solid:{root:`bg-inverted text-inverted`},outline:{root:`bg-default ring ring-default divide-y divide-default`},soft:{root:`bg-elevated/50 divide-y divide-default`},subtle:{root:`bg-elevated/50 ring ring-default divide-y divide-default`}}},defaultVariants:{variant:`outline`}},h={__name:`Card`,props:{as:{type:null,required:!1},variant:{type:null,required:!1},class:{type:null,required:!1},ui:{type:Object,required:!1}},setup(h){let g=h,_=t(),v=u(),y=f(`card`,g),b=i(()=>p({extend:p(m),...v.ui?.card||{}})({variant:g.variant}));return(t,i)=>(e(),s(l(d),{as:h.as,"data-slot":`root`,class:c(b.value.root({class:[l(y)?.root,g.class]}))},{default:n(()=>[_.header?(e(),a(`div`,{key:0,"data-slot":`header`,class:c(b.value.header({class:l(y)?.header}))},[r(t.$slots,`header`)],2)):o(``,!0),_.default?(e(),a(`div`,{key:1,"data-slot":`body`,class:c(b.value.body({class:l(y)?.body}))},[r(t.$slots,`default`)],2)):o(``,!0),_.footer?(e(),a(`div`,{key:2,"data-slot":`footer`,class:c(b.value.footer({class:l(y)?.footer}))},[r(t.$slots,`footer`)],2)):o(``,!0)]),_:3},8,[`as`,`class`]))}};export{h as t};

View File

@@ -1 +0,0 @@
import{I as e,J as t,S as n,Y as r,_t as i,d as a,ot as o,ut as s,w as c,y as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{E as u,a as d}from"./Icon-Chkiq2IE.js";import{f}from"./usePortal-Zddbph8M.js";function p(e){let t=f({dir:s(`ltr`)});return a(()=>e?.value||t.dir?.value||`ltr`)}function m(e){return a(()=>i(e)?!!u(e)?.closest(`form`):!0)}function h(){let e=s();return{primitiveElement:e,currentElement:a(()=>[`#text`,`#comment`].includes(e.value?.$el.nodeName)?e.value?.$el.nextElementSibling:u(e))}}var g=`data-reka-collection-item`;function _(i={}){let{key:u=``,isProvider:f=!1}=i,p=`${u}CollectionProvider`,m;if(f){let t=s(new Map);m={collectionRef:s(),itemMap:t},e(p,m)}else m=c(p);let _=(e=!1)=>{let t=m.collectionRef.value;if(!t)return[];let n=Array.from(t.querySelectorAll(`[${g}]`)),r=Array.from(m.itemMap.value.values()).sort((e,t)=>n.indexOf(e.ref)-n.indexOf(t.ref));return e?r:r.filter(e=>e.ref.dataset.disabled!==``)},v=l({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:r,attrs:i}){let{primitiveElement:a,currentElement:o}=h();return t(o,()=>{m.collectionRef.value=o.value}),()=>n(d,{ref:a,...i},r)}}),y=l({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:t,attrs:i}){let{primitiveElement:a,currentElement:s}=h();return r(t=>{if(s.value){let n=o(s.value);m.itemMap.value.set(n,{ref:s.value,value:e.value}),t(()=>m.itemMap.value.delete(n))}}),()=>n(d,{...i,[g]:``,ref:a},t)}});return{getItems:_,reactiveItems:a(()=>Array.from(m.itemMap.value.values())),itemMapSize:a(()=>m.itemMap.value.size),CollectionSlot:v,CollectionItem:y}}export{p as i,h as n,m as r,_ as t};

View File

@@ -0,0 +1 @@
import{I as e,J as t,S as n,Y as r,_t as i,d as a,ot as o,ut as s,w as c,y as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{E as u,i as d}from"./tv-uB0-NqWK.js";import{f}from"./usePortal-BgeZHop8.js";function p(e){let t=f({dir:s(`ltr`)});return a(()=>e?.value||t.dir?.value||`ltr`)}function m(e){return a(()=>i(e)?!!u(e)?.closest(`form`):!0)}function h(){let e=s();return{primitiveElement:e,currentElement:a(()=>[`#text`,`#comment`].includes(e.value?.$el.nodeName)?e.value?.$el.nextElementSibling:u(e))}}var g=`data-reka-collection-item`;function _(i={}){let{key:u=``,isProvider:f=!1}=i,p=`${u}CollectionProvider`,m;if(f){let t=s(new Map);m={collectionRef:s(),itemMap:t},e(p,m)}else m=c(p);let _=(e=!1)=>{let t=m.collectionRef.value;if(!t)return[];let n=Array.from(t.querySelectorAll(`[${g}]`)),r=Array.from(m.itemMap.value.values()).sort((e,t)=>n.indexOf(e.ref)-n.indexOf(t.ref));return e?r:r.filter(e=>e.ref.dataset.disabled!==``)},v=l({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:r,attrs:i}){let{primitiveElement:a,currentElement:o}=h();return t(o,()=>{m.collectionRef.value=o.value}),()=>n(d,{ref:a,...i},r)}}),y=l({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:t,attrs:i}){let{primitiveElement:a,currentElement:s}=h();return r(t=>{if(s.value){let n=o(s.value);m.itemMap.value.set(n,{ref:s.value,value:e.value}),t(()=>m.itemMap.value.delete(n))}}),()=>n(d,{...i,[g]:``,ref:a},t)}});return{getItems:_,reactiveItems:a(()=>Array.from(m.itemMap.value.values())),itemMapSize:a(()=>m.itemMap.value.size),CollectionSlot:v,CollectionItem:y}}export{p as i,h as n,m as r,_ as t};

View File

@@ -1 +0,0 @@
import{t as e}from"./HomeView-CdMOMcn8.js";export{e as default};

View File

@@ -1 +0,0 @@
import{F as e,Q as t,_ as n,g as r,h as i,z as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";var o=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},s={},c={class:`flex gap-4`};function l(o,s){let l=a(`RouterLink`);return e(),i(`main`,c,[n(l,{class:`bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md`,to:{name:`login`}},{default:t(()=>[...s[0]||=[r(`Login `,-1)]]),_:1}),n(l,{class:`bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md`,to:{name:`register`}},{default:t(()=>[...s[1]||=[r(` Register`,-1)]]),_:1}),n(l,{class:`bg-(--color-blue-600) dark:bg-(--color-blue-500) px-2 py-1 rounded text-white flex items-center shadow-md`,to:{name:`chart`}},{default:t(()=>[...s[2]||=[r(`Chart `,-1)]]),_:1})])}var u=o(s,[[`render`,l]]);export{u as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{F as e,Q as t,_ as n,f as r,g as i,h as a,m as o,o as s,p as c,ut as l,wt as u,y as d,yt as f}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import"./useFetchJson-4WJQFaEO.js";import{n as p}from"./useForwardExpose-BgPOLLFN.js";import{Q as m,X as h,t as g}from"./Icon-Chkiq2IE.js";import{t as _}from"./auth-hZSBdvj-.js";import{t as v}from"./Button-jwL-tYHc.js";import{n as y,r as b,t as x}from"./useValidation-wBItIFut.js";import{n as S}from"./settings-BcOmX106.js";import{t as C}from"./Alert-BNRo6CMI.js";var w={class:`h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8`},T={class:`w-full max-w-md flex flex-col gap-4`},E={key:0,class:`text-center flex flex-col gap-4`},D={class:`text-xl font-semibold dark:text-white text-black`},O={class:`text-sm text-gray-600 dark:text-gray-400`},k={class:`text-center`},A={class:`text-sm text-gray-600 dark:text-gray-400`},j={class:`text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4`},M={class:`text-sm text-gray-600 dark:text-gray-400`},N=d({__name:`PasswordRecoveryView`,setup(d){let{t:N}=m(),P=h(),F=_(),I=x(),L=l(``),R=l(!1);async function z(){await F.requestPasswordReset(L.value)&&(R.value=!0)}function B(){P.push({name:`login`})}function V(){P.push({name:`register`})}function H(){return I.reset(),I.validateEmail(L,`email`,p.t(`validate_error.email_required`)),I.errors}return(l,d)=>{let p=g,m=v,h=C,_=S,x=y,N=b;return e(),a(`div`,w,[r(`div`,T,[R.value?(e(),a(`div`,E,[n(p,{name:`i-heroicons-envelope`,class:`w-12 h-12 mx-auto text-primary-500`}),r(`h2`,D,u(l.$t(`general.check_your_email`)),1),r(`p`,O,u(l.$t(`general.password_reset_link_sent_notice`)),1),n(m,{color:`neutral`,variant:`outline`,block:``,onClick:B,class:`dark:text-white text-black`},{default:t(()=>[i(u(l.$t(`general.back_to_sign_in`)),1)]),_:1})])):(e(),a(s,{key:1},[r(`div`,k,[r(`p`,A,u(l.$t(`general.enter_email_for_password_reset`)),1)]),n(N,{validate:H,onSubmit:z,class:`flex flex-col gap-3`},{default:t(()=>[f(F).error?(e(),c(h,{key:0,color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:f(F).error,"close-button":{icon:`i-heroicons-x-mark-20-solid`,variant:`link`},onClose:f(F).clearError},null,8,[`title`,`onClose`])):o(``,!0),n(x,{label:l.$t(`general.email_address`),name:`email`,required:``,class:`w-full dark:text-white text-black`},{default:t(()=>[n(_,{modelValue:L.value,"onUpdate:modelValue":d[0]||=e=>L.value=e,placeholder:l.$t(`general.enter_your_email`),disabled:f(F).loading,class:`w-full dark:text-white text-black`},null,8,[`modelValue`,`placeholder`,`disabled`])]),_:1},8,[`label`]),n(m,{type:`submit`,block:``,loading:f(F).loading,class:`text-white bg-(--color-blue-600) dark:bg-(--color-blue-500)`},{default:t(()=>[i(u(l.$t(`general.send_password_reset_link`)),1)]),_:1},8,[`loading`])]),_:1}),r(`div`,j,[n(m,{color:`neutral`,variant:`outline`,loading:f(F).loading,class:`w-full flex justify-center dark:text-white text-black`,onClick:B},{default:t(()=>[i(u(l.$t(`general.back_to_sign_in`)),1)]),_:1},8,[`loading`]),r(`p`,M,[i(u(l.$t(`general.dont_have_an_account`))+` `,1),n(m,{variant:`link`,size:`sm`,onClick:V,class:`text-(--color-blue-600) dark:text-(--color-blue-500)`},{default:t(()=>[i(u(l.$t(`general.create_account_now`)),1)]),_:1})])])],64))])])}}});export{N as default};

View File

@@ -0,0 +1 @@
import{F as e,Q as t,_ as n,f as r,g as i,h as a,m as o,o as s,p as c,ut as l,wt as u,y as d,yt as f}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import"./useFetchJson-BTB9doG4.js";import{g as p,t as m}from"./Button-Dys5wjZc.js";import{Z as h,s as g}from"./tv-uB0-NqWK.js";import{t as _}from"./auth-DHyg2egq.js";import{n as v,r as y,t as b}from"./useValidation-pSaoyCcB.js";import{n as x}from"./settings-84EZt-NQ.js";import{t as S}from"./Alert-CvejfPQL.js";var C={class:`h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8`},w={class:`text-center mb-15`},T={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},E={class:`w-full max-w-md flex flex-col gap-4`},D={key:0,class:`text-center flex flex-col gap-4`},O={class:`text-xl font-semibold dark:text-white text-black`},k={class:`text-sm text-gray-600 dark:text-gray-400`},A={class:`text-center`},j={class:`text-sm text-gray-600 dark:text-gray-400`},M={class:`text-center flex flex-col gap-3 border-t dark:border-(--border-dark) border-(--border-light) pt-4`},N=[`loading`],P={class:`text-sm text-gray-600 dark:text-gray-400`},F=d({__name:`PasswordRecoveryView`,setup(d){let F=h(),I=_(),L=b(),R=l(``),z=l(!1);async function B(){await I.requestPasswordReset(R.value)&&(z.value=!0)}function V(){F.push({name:`login`})}function H(){F.push({name:`register`})}function U(){return L.reset(),L.validateEmail(R,`email`,p.t(`validate_error.email_required`)),L.errors}return(l,d)=>{let p=g,h=m,_=S,b=x,F=v,L=y;return e(),a(`div`,C,[r(`div`,w,[r(`div`,T,[n(p,{name:`i-heroicons-clock`,class:`w-8 h-8`})]),d[1]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`TimeTracker`,-1)]),r(`div`,E,[z.value?(e(),a(`div`,D,[n(p,{name:`i-heroicons-envelope`,class:`w-12 h-12 mx-auto text-primary-500`}),r(`h2`,O,u(l.$t(`general.check_your_email`)),1),r(`p`,k,u(l.$t(`general.password_reset_link_sent_notice`)),1),n(h,{color:`neutral`,variant:`outline`,block:``,onClick:V,class:`dark:text-white text-black cursor-pointer`},{default:t(()=>[i(u(l.$t(`general.back_to_sign_in`)),1)]),_:1})])):(e(),a(s,{key:1},[r(`div`,A,[r(`p`,j,u(l.$t(`general.enter_email_for_password_reset`)),1)]),n(L,{validate:U,onSubmit:B,class:`flex flex-col gap-3`},{default:t(()=>[f(I).error?(e(),c(_,{key:0,color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:f(I).error,"close-button":{icon:`i-heroicons-x-mark-20-solid`,variant:`link`},onClose:f(I).clearError},null,8,[`title`,`onClose`])):o(``,!0),n(F,{label:l.$t(`general.email_address`),name:`email`,required:``,class:`w-full dark:text-white text-black`},{default:t(()=>[n(b,{modelValue:R.value,"onUpdate:modelValue":d[0]||=e=>R.value=e,placeholder:l.$t(`general.enter_your_email`),disabled:f(I).loading,class:`w-full dark:text-white text-black placeholder:text-(--placeholder)`},null,8,[`modelValue`,`placeholder`,`disabled`])]),_:1},8,[`label`]),n(h,{type:`submit`,block:``,loading:f(I).loading,class:`text-white bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer`},{default:t(()=>[i(u(l.$t(`general.send_password_reset_link`)),1)]),_:1},8,[`loading`])]),_:1}),r(`div`,M,[r(`button`,{color:`neutral`,variant:`outline`,loading:f(I).loading,class:`w-full flex items-center gap-2 justify-center text-[15px] dark:text-white text-black cursor-pointer`,onClick:V},[n(p,{name:`mingcute:arrow-left-line`,class:`text-(--color-blue-600) dark:text-(--color-blue-500) text-[16px]`}),i(` `+u(l.$t(`general.back_to_sign_in`)),1)],8,N),r(`p`,P,[i(u(l.$t(`general.dont_have_an_account`))+` `,1),r(`button`,{variant:`link`,size:`sm`,onClick:H,class:`text-[15px] text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer`},u(l.$t(`general.create_account_now`)),1)])])],64))])])}}});export{F as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{F as e,M as t,Q as n,_ as r,f as i,g as a,h as o,m as s,p as c,ut as l,wt as u,y as d,yt as f}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import"./useFetchJson-BTB9doG4.js";import{g as p,t as m}from"./Button-Dys5wjZc.js";import{X as h,Z as g,s as _}from"./tv-uB0-NqWK.js";import{t as v}from"./auth-DHyg2egq.js";import{n as y,r as b,t as x}from"./useValidation-pSaoyCcB.js";import{n as S}from"./settings-84EZt-NQ.js";import{t as C}from"./Alert-CvejfPQL.js";var w={class:`h-[100vh] flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8`},T={class:`text-center mb-15`},E={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},D={class:`w-full max-w-md flex flex-col gap-4`},O={key:0,class:`text-center flex flex-col gap-4`},k={class:`text-xl font-semibold dark:text-white text-black`},A={class:`text-sm text-gray-600 dark:text-gray-400`},j={class:`text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4`},M=d({__name:`ResetPasswordForm`,setup(d){let M=g(),N=h(),P=v(),F=x(),I=l(``),L=l(``),R=l(!1),z=l(!1),B=l(``),V=l(!1);t(()=>{B.value=N.query.token||``,B.value||M.push({name:`password-recovery`})});async function H(){await P.resetPassword(B.value,I.value)&&(V.value=!0)}function U(){M.push({name:`login`})}function W(){return F.reset(),F.validatePasswords(I,`new_password`,L,`confirm_new_password`,p.t(`validate_error.confirm_password_required`)),F.errors}return(t,l)=>{let d=_,p=m,h=C,g=S,v=y,x=b;return e(),o(`div`,w,[i(`div`,T,[i(`div`,E,[r(d,{name:`i-heroicons-clock`,class:`w-8 h-8`})]),l[4]||=i(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`TimeTracker`,-1)]),i(`div`,D,[V.value?(e(),o(`div`,O,[r(d,{name:`i-heroicons-check-circle`,class:`w-12 h-12 mx-auto text-green-800`}),i(`h2`,k,u(t.$t(`general.password_updated`)),1),i(`p`,A,u(t.$t(`general.password_updated_description`)),1),r(p,{block:``,onClick:U,class:`dark:text-white text-black`},{default:n(()=>[a(u(t.$t(`general.back_to_sign_in`)),1)]),_:1})])):(e(),c(x,{key:1,validate:W,onSubmit:H,class:`flex flex-col gap-3`},{default:n(()=>[f(P).error?(e(),c(h,{key:0,color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:f(P).error,"close-button":{icon:`i-heroicons-x-mark-20-solid`,variant:`link`},onClose:f(P).clearError},null,8,[`title`,`onClose`])):s(``,!0),r(v,{label:t.$t(`general.new_password`),name:`new_password`,required:``,class:`w-full dark:text-white text-black`},{default:n(()=>[r(g,{modelValue:I.value,"onUpdate:modelValue":l[1]||=e=>I.value=e,type:R.value?`text`:`password`,placeholder:t.$t(`general.enter_your_new_password`),disabled:f(P).loading,class:`w-full dark:text-white text-black placeholder:text-(--placeholder)`,ui:{trailing:`pe-1`}},{trailing:n(()=>[r(d,{color:`neutral`,variant:`link`,size:`sm`,name:R.value?`i-lucide-eye-off`:`i-lucide-eye`,"aria-label":R.value?`Hide password`:`Show password`,"aria-pressed":R.value,"aria-controls":`new_password`,onClick:l[0]||=e=>R.value=!R.value,class:`mr-2`},null,8,[`name`,`aria-label`,`aria-pressed`])]),_:1},8,[`modelValue`,`type`,`placeholder`,`disabled`])]),_:1},8,[`label`]),r(v,{label:t.$t(`general.confirm_password`),name:`confirm_new_password`,required:``,class:`w-full dark:text-white text-black`},{default:n(()=>[r(g,{modelValue:L.value,"onUpdate:modelValue":l[3]||=e=>L.value=e,type:z.value?`text`:`password`,placeholder:t.$t(`general.confirm_your_new_password`),disabled:f(P).loading,class:`w-full dark:text-white text-black placeholder:text-(--placeholder)`,ui:{trailing:`pe-1`}},{trailing:n(()=>[r(d,{color:`neutral`,variant:`ghost`,size:`sm`,name:z.value?`i-lucide-eye-off`:`i-lucide-eye`,onClick:l[2]||=e=>z.value=!z.value,class:`mr-2`},null,8,[`name`])]),_:1},8,[`modelValue`,`type`,`placeholder`,`disabled`])]),_:1},8,[`label`]),r(p,{type:`submit`,block:``,loading:f(P).loading,class:`text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500) cursor-pointer`},{default:n(()=>[a(u(t.$t(`general.reset_password`)),1)]),_:1},8,[`loading`]),i(`div`,j,[i(`button`,{color:`neutral`,variant:`ghost`,onClick:U,class:`text-[15px] flex items-center gap-2 text-(--color-blue-600) dark:text-(--color-blue-500) cursor-pointer`},[r(d,{name:`mingcute:arrow-left-line`}),a(` `+u(t.$t(`general.back_to_sign_in`)),1)])])]),_:1}))])])}}});export{M as default};

View File

@@ -1 +0,0 @@
import{F as e,M as t,Q as n,_ as r,f as i,g as a,h as o,m as s,p as c,ut as l,wt as u,y as d,yt as f}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import"./useFetchJson-4WJQFaEO.js";import{n as p}from"./useForwardExpose-BgPOLLFN.js";import{Q as m,X as h,Y as g,t as _}from"./Icon-Chkiq2IE.js";import{t as v}from"./auth-hZSBdvj-.js";import{t as y}from"./Button-jwL-tYHc.js";import{n as b,r as x,t as S}from"./useValidation-wBItIFut.js";import{n as C}from"./settings-BcOmX106.js";import{t as w}from"./Alert-BNRo6CMI.js";var T={class:`h-[100vh] flex items-center justify-center px-4 sm:px-6 lg:px-8`},E={class:`w-full max-w-md flex flex-col gap-4`},D={key:0,class:`text-center flex flex-col gap-4`},O={class:`text-xl font-semibold dark:text-white text-black`},k={class:`text-sm text-gray-600 dark:text-gray-400`},A={class:`text-center border-t dark:border-(--border-dark) border-(--border-light) pt-4`},j=d({__name:`ResetPasswordForm`,setup(d){let{t:j}=m(),M=h(),N=g(),P=v(),F=S(),I=l(``),L=l(``),R=l(!1),z=l(!1),B=l(``),V=l(!1);t(()=>{B.value=N.query.token||``,B.value||M.push({name:`password-recovery`})});async function H(){await P.resetPassword(B.value,I.value)&&(V.value=!0)}function U(){M.push({name:`login`})}function W(){return F.reset(),F.validatePasswords(I,`new_password`,L,`confirm_new_password`,p.t(`validate_error.confirm_password_required`)),F.errors}return(t,l)=>{let d=_,p=y,m=w,h=C,g=b,v=x;return e(),o(`div`,T,[i(`div`,E,[V.value?(e(),o(`div`,D,[r(d,{name:`i-heroicons-check-circle`,class:`w-12 h-12 mx-auto text-green-500`}),i(`h2`,O,u(t.$t(`general.password_updated`)),1),i(`p`,k,u(t.$t(`general.password_updated_description`)),1),r(p,{block:``,onClick:U,class:`dark:text-white text-black`},{default:n(()=>[a(u(t.$t(`general.back_to_sign_in`)),1)]),_:1})])):(e(),c(v,{key:1,validate:W,onSubmit:H,class:`flex flex-col gap-3`},{default:n(()=>[f(P).error?(e(),c(m,{key:0,color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:f(P).error,"close-button":{icon:`i-heroicons-x-mark-20-solid`,variant:`link`},onClose:f(P).clearError},null,8,[`title`,`onClose`])):s(``,!0),r(g,{label:t.$t(`general.new_password`),name:`new_password`,required:``,class:`w-full dark:text-white text-black`},{default:n(()=>[r(h,{modelValue:I.value,"onUpdate:modelValue":l[1]||=e=>I.value=e,type:R.value?`text`:`password`,placeholder:t.$t(`general.enter_your_new_password`),disabled:f(P).loading,class:`w-full dark:text-white text-black`,ui:{trailing:`pe-1`}},{trailing:n(()=>[r(d,{color:`neutral`,variant:`link`,size:`sm`,name:R.value?`i-lucide-eye-off`:`i-lucide-eye`,"aria-label":R.value?`Hide password`:`Show password`,"aria-pressed":R.value,"aria-controls":`new_password`,onClick:l[0]||=e=>R.value=!R.value},null,8,[`name`,`aria-label`,`aria-pressed`])]),_:1},8,[`modelValue`,`type`,`placeholder`,`disabled`])]),_:1},8,[`label`]),r(g,{label:t.$t(`general.confirm_password`),name:`confirm_new_password`,required:``,class:`w-full dark:text-white text-black`},{default:n(()=>[r(h,{modelValue:L.value,"onUpdate:modelValue":l[3]||=e=>L.value=e,type:z.value?`text`:`password`,placeholder:t.$t(`general.confirm_your_new_password`),disabled:f(P).loading,class:`w-full dark:text-white text-black`,ui:{trailing:`pe-1`}},{trailing:n(()=>[r(d,{color:`neutral`,variant:`ghost`,size:`sm`,name:z.value?`i-lucide-eye-off`:`i-lucide-eye`,onClick:l[2]||=e=>z.value=!z.value},null,8,[`name`])]),_:1},8,[`modelValue`,`type`,`placeholder`,`disabled`])]),_:1},8,[`label`]),r(p,{type:`submit`,block:``,loading:f(P).loading,class:`text-white! bg-(--color-blue-600) dark:bg-(--color-blue-500)`},{default:n(()=>[a(u(t.$t(`general.reset_password`)),1)]),_:1},8,[`loading`]),i(`div`,A,[r(p,{color:`neutral`,variant:`ghost`,onClick:U,class:`dark:text-white text-black`},{default:n(()=>[a(u(t.$t(`general.back_to_sign_in`)),1)]),_:1})])]),_:1}))])])}}});export{j as default};

View File

@@ -1 +0,0 @@
import{F as e,M as t,Q as n,_ as r,f as i,g as a,h as o,m as s,ut as c,wt as l,y as u}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{t as d}from"./useFetchJson-4WJQFaEO.js";import{Q as f,X as p,Y as m,t as h}from"./Icon-Chkiq2IE.js";import{t as g}from"./Button-jwL-tYHc.js";import{t as _}from"./Card-DPC9xXwj.js";import{t as v}from"./Alert-BNRo6CMI.js";var y={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900`},b={class:`pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8`},x={class:`w-full max-w-md`},S={class:`text-center mb-8`},C={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},w={class:`text-center`},T={key:0},E={class:`text-xl font-semibold text-gray-900 dark:text-white`},D={key:1},O={class:`inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4`},k={class:`text-xl font-semibold text-gray-900 dark:text-white`},A={class:`mt-1 text-sm text-gray-500 dark:text-gray-400`},j={key:2},M={class:`inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4`},N={class:`text-xl font-semibold text-gray-900 dark:text-white`},P={class:`mt-1 text-sm text-gray-500 dark:text-gray-400`},F={key:0,class:`text-center py-4`},I={class:`text-gray-600 dark:text-gray-400 mb-4`},L={key:1,class:`text-center py-4`},R={key:2,class:`text-center py-4`},z={class:`text-gray-500 dark:text-gray-400`},B={class:`text-center`},V={class:`text-sm text-gray-600 dark:text-gray-400`},H=u({__name:`VerifyEmailView`,setup(u){let{t:H,te:U}=f(),W=p(),G=m();function K(e,t){return U(e)?H(e):t}let q=c(``),J=c(!1),Y=c(null),X=c(!1),Z=c(!0);t(()=>{if(q.value=G.query.token||``,!q.value){Y.value=K(`verify_email.invalid_token`,`Invalid or missing verification token`),Z.value=!1;return}Q()});async function Q(){if(!q.value){Y.value=K(`verify_email.invalid_token`,`Invalid or missing verification token`);return}J.value=!0,Y.value=null;try{await d(`/api/v1/auth/complete-registration`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({token:q.value})}),X.value=!0,Z.value=!1,setTimeout(()=>{W.push({name:`login`})},3e3)}catch(e){Y.value=e?.message??K(`verify_email.verification_failed`,`Email verification failed`),Z.value=!1}finally{J.value=!1}}function $(){W.push({name:`login`})}return(t,c)=>{let u=h,d=g,f=v,p=_;return e(),o(`div`,y,[i(`div`,b,[i(`div`,x,[i(`div`,S,[i(`div`,C,[r(u,{name:`i-heroicons-envelope-check`,class:`w-8 h-8`})]),c[0]||=i(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`TimeTracker`,-1)]),r(p,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{header:n(()=>[i(`div`,w,[Z.value&&J.value?(e(),o(`div`,T,[r(u,{name:`i-heroicons-arrow-path`,class:`w-8 h-8 animate-spin text-primary-500 mx-auto mb-4`}),i(`h2`,E,l(K(`verify_email.verifying`,`Verifying your email...`)),1)])):X.value?(e(),o(`div`,D,[i(`div`,O,[r(u,{name:`i-heroicons-check-circle`,class:`w-6 h-6`})]),i(`h2`,k,l(K(`verify_email.success_title`,`Email Verified!`)),1),i(`p`,A,l(K(`verify_email.success_message`,`Your email has been verified successfully.`)),1)])):Y.value?(e(),o(`div`,j,[i(`div`,M,[r(u,{name:`i-heroicons-exclamation-circle`,class:`w-6 h-6`})]),i(`h2`,N,l(K(`verify_email.error_title`,`Verification Failed`)),1),i(`p`,P,l(K(`verify_email.error_message`,`We could not verify your email.`)),1)])):s(``,!0)])]),footer:n(()=>[i(`div`,B,[i(`p`,V,[a(l(K(`verify_email.already_registered`,`Already have an account?`))+` `,1),r(d,{variant:`link`,size:`sm`,onClick:$},{default:n(()=>[a(l(K(`verify_email.sign_in`,`Sign in`)),1)]),_:1})])])]),default:n(()=>[X.value?(e(),o(`div`,F,[i(`p`,I,l(K(`verify_email.redirect_message`,`You will be redirected to login page...`)),1),r(d,{color:`primary`,onClick:$},{default:n(()=>[a(l(K(`verify_email.go_to_login`,`Go to Login`)),1)]),_:1})])):Y.value?(e(),o(`div`,L,[r(f,{color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:Y.value,class:`mb-4`},null,8,[`title`]),r(d,{color:`primary`,onClick:$},{default:n(()=>[a(l(K(`verify_email.go_to_login`,`Go to Login`)),1)]),_:1})])):Z.value&&J.value?(e(),o(`div`,R,[i(`p`,z,l(K(`verify_email.please_wait`,`Please wait while we verify your email address.`)),1)])):s(``,!0)]),_:1})])])])}}});export{H as default};

View File

@@ -0,0 +1 @@
import{F as e,M as t,Q as n,_ as r,f as i,g as a,h as o,m as s,ut as c,wt as l,y as u}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{t as d}from"./useFetchJson-BTB9doG4.js";import{g as f,t as p}from"./Button-Dys5wjZc.js";import{$ as m,X as h,Z as g,s as _}from"./tv-uB0-NqWK.js";import{t as v}from"./Card-DJGrWflS.js";import{t as y}from"./Alert-CvejfPQL.js";var b={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900`},x={class:`pt-20 pb-8 flex items-center justify-center px-4 sm:px-6 lg:px-8`},S={class:`w-full max-w-md`},C={class:`text-center mb-8`},w={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},T={class:`text-center`},E={key:0},D={class:`text-xl font-semibold text-gray-900 dark:text-white`},O={key:1},k={class:`inline-flex items-center justify-center w-12 h-12 rounded-full bg-green-100 text-green-600 mb-4`},A={class:`text-xl font-semibold text-gray-900 dark:text-white`},j={class:`mt-1 text-sm text-gray-500 dark:text-gray-400`},M={key:2},N={class:`inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-100 text-red-600 mb-4`},P={class:`text-xl font-semibold text-gray-900 dark:text-white`},F={class:`mt-1 text-sm text-gray-500 dark:text-gray-400`},I={key:0,class:`text-center py-4`},L={class:`text-gray-600 dark:text-gray-400 mb-4`},R={key:1,class:`text-center py-4`},z={key:2,class:`text-center py-4`},B={class:`text-gray-500 dark:text-gray-400`},V={class:`text-center`},H={class:`text-sm text-gray-600 dark:text-gray-400`},U=u({__name:`VerifyEmailView`,setup(u){let{t:U,te:W}=m(),G=g(),K=h(),q=c(``),J=c(!1),Y=c(null),X=c(!1),Z=c(!0);t(()=>{if(q.value=K.query.token||``,!q.value){Y.value=f.t(`verify_email.invalid_token`),Z.value=!1;return}Q()});async function Q(){if(!q.value){Y.value=f.t(`verify_email.invalid_token`);return}J.value=!0,Y.value=null;try{await d(`/api/v1/public/auth/complete-registration`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({token:q.value})}),X.value=!0,Z.value=!1,setTimeout(()=>{G.push({name:`login`})},3e3)}catch(e){Y.value=e?.message??f.t(`verify_email.verification_failed`),Z.value=!1}finally{J.value=!1}}function $(){G.push({name:`login`})}return(t,c)=>{let u=_,d=p,f=y,m=v;return e(),o(`div`,b,[i(`div`,x,[i(`div`,S,[i(`div`,C,[i(`div`,w,[r(u,{name:`i-heroicons-clock`,class:`w-8 h-8`})]),c[0]||=i(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`TimeTracker`,-1)]),r(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{header:n(()=>[i(`div`,T,[Z.value&&J.value?(e(),o(`div`,E,[r(u,{name:`i-heroicons-arrow-path`,class:`w-8 h-8 animate-spin text-primary-500 mx-auto mb-4`}),i(`h2`,D,l(t.$t(`verify_email.verifying`)),1)])):X.value?(e(),o(`div`,O,[i(`div`,k,[r(u,{name:`i-heroicons-check-circle`,class:`w-6 h-6`})]),i(`h2`,A,l(t.$t(`verify_email.success_title`)),1),i(`p`,j,l(t.$t(`verify_email.success_message`)),1)])):Y.value?(e(),o(`div`,M,[i(`div`,N,[r(u,{name:`i-heroicons-exclamation-circle`,class:`w-6 h-6`})]),i(`h2`,P,l(t.$t(`verify_email.error_title`)),1),i(`p`,F,l(t.$t(`verify_email.error_message`)),1)])):s(``,!0)])]),footer:n(()=>[i(`div`,V,[i(`p`,H,[a(l(t.$t(`verify_email.already_registered`))+` `,1),i(`button`,{variant:`link`,size:`sm`,onClick:$,class:`cursor-pointer text-(--color-blue-600) dark:text-(--color-blue-500)`},l(t.$t(`general.sign_in`)),1)])])]),default:n(()=>[X.value?(e(),o(`div`,I,[i(`p`,L,l(t.$t(`verify_email.redirect_message`)),1),r(d,{color:`primary`,onClick:$},{default:n(()=>[a(l(t.$t(`verify_email.go_to_login`)),1)]),_:1})])):Y.value?(e(),o(`div`,R,[r(f,{color:`error`,variant:`subtle`,icon:`i-heroicons-exclamation-triangle`,title:Y.value,class:`mb-4`},null,8,[`title`]),r(d,{color:`primary`,onClick:$,class:`cursor-pointer`},{default:n(()=>[a(l(t.$t(`verify_email.go_to_login`)),1)]),_:1})])):Z.value&&J.value?(e(),o(`div`,z,[i(`p`,B,l(t.$t(`verify_email.please_wait`)),1)])):s(``,!0)]),_:1})])])])}}});export{U as default};

View File

@@ -1 +1 @@
import{D as e,F as t,J as n,L as r,d as i,h as a,m as o,o as s,p as c,y as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{h as u,n as d}from"./usePortal-Zddbph8M.js";import{n as f}from"./Collection-BkGqWqUl.js";var p={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function m(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function h(e,t,n){let r=m(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return p[r]}function g(e,t=!1){let n=u();for(let r of e)if(r===n||(r.focus({preventScroll:t}),u()!==n))return}function _(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var v=l({inheritAttrs:!1,__name:`VisuallyHiddenInputBubble`,props:{name:{type:String,required:!0},value:{type:null,required:!0},checked:{type:Boolean,required:!1,default:void 0},required:{type:Boolean,required:!1},disabled:{type:Boolean,required:!1},feature:{type:String,required:!1,default:`fully-hidden`}},setup(r){let a=r,{primitiveElement:o,currentElement:s}=f();return n(i(()=>a.checked??a.value),(e,t)=>{if(!s.value)return;let n=s.value,r=window.HTMLInputElement.prototype,i=Object.getOwnPropertyDescriptor(r,`value`).set;if(i&&e!==t){let t=new Event(`input`,{bubbles:!0}),r=new Event(`change`,{bubbles:!0});i.call(n,e),n.dispatchEvent(t),n.dispatchEvent(r)}}),(n,r)=>(t(),c(d,e({ref_key:`primitiveElement`,ref:o},{...a,...n.$attrs},{as:`input`}),null,16))}}),y=l({inheritAttrs:!1,__name:`VisuallyHiddenInput`,props:{name:{type:String,required:!0},value:{type:null,required:!0},checked:{type:Boolean,required:!1,default:void 0},required:{type:Boolean,required:!1},disabled:{type:Boolean,required:!1},feature:{type:String,required:!1,default:`fully-hidden`}},setup(n){let l=n,u=i(()=>typeof l.value==`object`&&Array.isArray(l.value)&&l.value.length===0&&l.required),d=i(()=>typeof l.value==`string`||typeof l.value==`number`||typeof l.value==`boolean`||l.value===null||l.value===void 0?[{name:l.name,value:l.value}]:typeof l.value==`object`&&Array.isArray(l.value)?l.value.flatMap((e,t)=>typeof e==`object`?Object.entries(e).map(([e,n])=>({name:`${l.name}[${t}][${e}]`,value:n})):{name:`${l.name}[${t}]`,value:e}):l.value!==null&&typeof l.value==`object`&&!Array.isArray(l.value)?Object.entries(l.value).map(([e,t])=>({name:`${l.name}[${e}]`,value:t})):[]);return(n,i)=>(t(),a(s,null,[o(` We render single input if it's required `),u.value?(t(),c(v,e({key:n.name},{...l,...n.$attrs},{name:n.name,value:n.value}),null,16,[`name`,`value`])):(t(!0),a(s,{key:1},r(d.value,r=>(t(),c(v,e({key:r.name},{ref_for:!0},{...l,...n.$attrs},{name:r.name,value:r.value}),null,16,[`name`,`value`]))),128))],2112))}});export{_ as a,h as i,p as n,g as r,y as t}; import{D as e,F as t,J as n,L as r,d as i,h as a,m as o,o as s,p as c,y as l}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{h as u,n as d}from"./usePortal-BgeZHop8.js";import{n as f}from"./Collection-Dmox1UHc.js";var p={ArrowLeft:`prev`,ArrowUp:`prev`,ArrowRight:`next`,ArrowDown:`next`,PageUp:`first`,Home:`first`,PageDown:`last`,End:`last`};function m(e,t){return t===`rtl`?e===`ArrowLeft`?`ArrowRight`:e===`ArrowRight`?`ArrowLeft`:e:e}function h(e,t,n){let r=m(e.key,n);if(!(t===`vertical`&&[`ArrowLeft`,`ArrowRight`].includes(r))&&!(t===`horizontal`&&[`ArrowUp`,`ArrowDown`].includes(r)))return p[r]}function g(e,t=!1){let n=u();for(let r of e)if(r===n||(r.focus({preventScroll:t}),u()!==n))return}function _(e,t){return e.map((n,r)=>e[(t+r)%e.length])}var v=l({inheritAttrs:!1,__name:`VisuallyHiddenInputBubble`,props:{name:{type:String,required:!0},value:{type:null,required:!0},checked:{type:Boolean,required:!1,default:void 0},required:{type:Boolean,required:!1},disabled:{type:Boolean,required:!1},feature:{type:String,required:!1,default:`fully-hidden`}},setup(r){let a=r,{primitiveElement:o,currentElement:s}=f();return n(i(()=>a.checked??a.value),(e,t)=>{if(!s.value)return;let n=s.value,r=window.HTMLInputElement.prototype,i=Object.getOwnPropertyDescriptor(r,`value`).set;if(i&&e!==t){let t=new Event(`input`,{bubbles:!0}),r=new Event(`change`,{bubbles:!0});i.call(n,e),n.dispatchEvent(t),n.dispatchEvent(r)}}),(n,r)=>(t(),c(d,e({ref_key:`primitiveElement`,ref:o},{...a,...n.$attrs},{as:`input`}),null,16))}}),y=l({inheritAttrs:!1,__name:`VisuallyHiddenInput`,props:{name:{type:String,required:!0},value:{type:null,required:!0},checked:{type:Boolean,required:!1,default:void 0},required:{type:Boolean,required:!1},disabled:{type:Boolean,required:!1},feature:{type:String,required:!1,default:`fully-hidden`}},setup(n){let l=n,u=i(()=>typeof l.value==`object`&&Array.isArray(l.value)&&l.value.length===0&&l.required),d=i(()=>typeof l.value==`string`||typeof l.value==`number`||typeof l.value==`boolean`||l.value===null||l.value===void 0?[{name:l.name,value:l.value}]:typeof l.value==`object`&&Array.isArray(l.value)?l.value.flatMap((e,t)=>typeof e==`object`?Object.entries(e).map(([e,n])=>({name:`${l.name}[${t}][${e}]`,value:n})):{name:`${l.name}[${t}]`,value:e}):l.value!==null&&typeof l.value==`object`&&!Array.isArray(l.value)?Object.entries(l.value).map(([e,t])=>({name:`${l.name}[${e}]`,value:t})):[]);return(n,i)=>(t(),a(s,null,[o(` We render single input if it's required `),u.value?(t(),c(v,e({key:n.name},{...l,...n.$attrs},{name:n.name,value:n.value}),null,16,[`name`,`value`])):(t(!0),a(s,{key:1},r(d.value,r=>(t(),c(v,e({key:r.name},{ref_for:!0},{...l,...n.$attrs},{name:r.name,value:r.value}),null,16,[`name`,`value`]))),128))],2112))}});export{_ as a,h as i,p as n,g as r,y as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import"./useFetchJson-4WJQFaEO.js";import{t as e}from"./auth-hZSBdvj-.js";export{e as useAuthStore};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import"./useFetchJson-BTB9doG4.js";import{t as e}from"./auth-DHyg2egq.js";export{e as useAuthStore};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{Q as o,t as s}from"./Icon-Chkiq2IE.js";import{t as c}from"./Card-DPC9xXwj.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`cs_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Podmínky použití`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Poslední aktualizace: březen 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Přijetí podmínek`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Používáním aplikace TimeTracker souhlasíte a zavazujete se dodržovat podmínky a ustanovení této dohody. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Popis služby`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker je aplikace pro sledování času, která uživatelům umožňuje sledovat pracovní hodiny, spravovat projekty a generovat reporty. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. Odpovědnosti uživatele`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`Souhlasíte s:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Poskytováním přesných a úplných informací`),r(`li`,null,`Udržováním bezpečnosti svého účtu`),r(`li`,null,`Nesdílením přihlašovacích údajů s ostatními`),r(`li`,null,`Používáním služby v souladu s platnými zákony`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Ochrana osobních údajů`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jsme odhodláni chránit vaše soukromí. Vaše osobní údaje budou zpracovány v souladu s naší Zásadami ochrany osobních údajů a příslušnými zákony o ochraně dat. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Duševní vlastnictví`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Služba TimeTracker a veškerý její obsah, včetně mimo jiné textů, grafiky, loga a softwaru, je majetkem TimeTracker a je chráněn zákony o duševním vlastnictví. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Omezení odpovědnosti`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker neodpovídá za jakékoli nepřímé, náhodné, zvláštní, následné nebo trestné škody vzniklé v důsledku vašeho používání nebo neschopnosti používat službu. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Ukončení`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Vyhrazujeme si právo ukončit nebo pozastavit váš účet kdykoli, bez předchozího upozornění, za chování, které por tyto Podmušujeínky použití nebo je škodlivé pro ostatní uživatele nebo službu. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Změny podmínek`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Vyhrazujeme si právo kdykoli upravit tyto Podmínky použití. Vaše další používání TimeTracker po jakýchkoli změnách znamená přijetí nových podmínek. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Kontaktní informace`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Máte-li jakékoli dotazy ohledně těchto Podmínek použití, kontaktujte nás na adrese support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

View File

@@ -0,0 +1 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{$ as o,s}from"./tv-uB0-NqWK.js";import{t as c}from"./Card-DJGrWflS.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`cs_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Podmínky použití`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Poslední aktualizace: březen 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Přijetí podmínek`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Používáním aplikace TimeTracker souhlasíte a zavazujete se dodržovat podmínky a ustanovení této dohody. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Popis služby`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker je aplikace pro sledování času, která uživatelům umožňuje sledovat pracovní hodiny, spravovat projekty a generovat reporty. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. Odpovědnosti uživatele`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`Souhlasíte s:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Poskytováním přesných a úplných informací`),r(`li`,null,`Udržováním bezpečnosti svého účtu`),r(`li`,null,`Nesdílením přihlašovacích údajů s ostatními`),r(`li`,null,`Používáním služby v souladu s platnými zákony`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Ochrana osobních údajů`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jsme odhodláni chránit vaše soukromí. Vaše osobní údaje budou zpracovány v souladu s naší Zásadami ochrany osobních údajů a příslušnými zákony o ochraně dat. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Duševní vlastnictví`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Služba TimeTracker a veškerý její obsah, včetně mimo jiné textů, grafiky, loga a softwaru, je majetkem TimeTracker a je chráněn zákony o duševním vlastnictví. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Omezení odpovědnosti`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker neodpovídá za jakékoli nepřímé, náhodné, zvláštní, následné nebo trestné škody vzniklé v důsledku vašeho používání nebo neschopnosti používat službu. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Ukončení`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Vyhrazujeme si právo ukončit nebo pozastavit váš účet kdykoli, bez předchozího upozornění, za chování, které por tyto Podmušujeínky použití nebo je škodlivé pro ostatní uživatele nebo službu. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Změny podmínek`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Vyhrazujeme si právo kdykoli upravit tyto Podmínky použití. Vaše další používání TimeTracker po jakýchkoli změnách znamená přijetí nových podmínek. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Kontaktní informace`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Máte-li jakékoli dotazy ohledně těchto Podmínek použití, kontaktujte nás na adrese support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{Q as o,t as s}from"./Icon-Chkiq2IE.js";import{t as c}from"./Card-DPC9xXwj.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`en_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Terms and Conditions`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Last updated: March 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Acceptance of Terms`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` By accessing and using TimeTracker, you accept and agree to be bound by the terms and provision of this agreement. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Description of Service`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker is a time tracking application that allows users to track their working hours, manage projects, and generate reports. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. User Responsibilities`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`You agree to:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Provide accurate and complete information`),r(`li`,null,`Maintain the security of your account`),r(`li`,null,`Not share your login credentials with others`),r(`li`,null,`Use the service in compliance with applicable laws`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Privacy and Data Protection`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We are committed to protecting your privacy. Your personal data will be processed in accordance with our Privacy Policy and applicable data protection laws. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Intellectual Property`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` The TimeTracker service and all its contents, including but not limited to text, graphics, logos, and software, are the property of TimeTracker and are protected by intellectual property laws. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Limitation of Liability`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker shall not be liable for any indirect, incidental, special, consequential, or punitive damages resulting from your use of or inability to use the service. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Termination`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We reserve the right to terminate or suspend your account at any time, without prior notice, for conduct that we believe violates these Terms and Conditions or is harmful to other users or the service. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Changes to Terms`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We reserve the right to modify these Terms and Conditions at any time. Your continued use of TimeTracker after any changes indicates your acceptance of the new terms. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Contact Information`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` If you have any questions about these Terms and Conditions, please contact us at support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

View File

@@ -0,0 +1 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{$ as o,s}from"./tv-uB0-NqWK.js";import{t as c}from"./Card-DJGrWflS.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`en_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Terms and Conditions`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Last updated: March 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Acceptance of Terms`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` By accessing and using TimeTracker, you accept and agree to be bound by the terms and provision of this agreement. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Description of Service`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker is a time tracking application that allows users to track their working hours, manage projects, and generate reports. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. User Responsibilities`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`You agree to:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Provide accurate and complete information`),r(`li`,null,`Maintain the security of your account`),r(`li`,null,`Not share your login credentials with others`),r(`li`,null,`Use the service in compliance with applicable laws`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Privacy and Data Protection`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We are committed to protecting your privacy. Your personal data will be processed in accordance with our Privacy Policy and applicable data protection laws. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Intellectual Property`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` The TimeTracker service and all its contents, including but not limited to text, graphics, logos, and software, are the property of TimeTracker and are protected by intellectual property laws. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Limitation of Liability`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker shall not be liable for any indirect, incidental, special, consequential, or punitive damages resulting from your use of or inability to use the service. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Termination`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We reserve the right to terminate or suspend your account at any time, without prior notice, for conduct that we believe violates these Terms and Conditions or is harmful to other users or the service. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Changes to Terms`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` We reserve the right to modify these Terms and Conditions at any time. Your continued use of TimeTracker after any changes indicates your acceptance of the new terms. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Contact Information`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` If you have any questions about these Terms and Conditions, please contact us at support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{Q as o,t as s}from"./Icon-Chkiq2IE.js";import{t as c}from"./Card-DPC9xXwj.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`pl_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Regulamin`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Ostatnia aktualizacja: marzec 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Akceptacja Regulaminu`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Korzystając z aplikacji TimeTracker, akceptujesz i zgadzasz się na przestrzeganie warunków i postanowień niniejszej umowy. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Opis Usługi`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker to aplikacja do śledzenia czasu pracy, która umożliwia użytkownikom śledzenie godzin pracy, zarządzanie projektami oraz generowanie raportów. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. Obowiązki Użytkownika`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`Zgadzasz się na:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Podawanie dokładnych i kompletnych informacji`),r(`li`,null,`Utrzymywanie bezpieczeństwa swojego konta`),r(`li`,null,`Nieudostępnianie danych logowania innym osobom`),r(`li`,null,`Korzystanie z usługi zgodnie z obowiązującymi przepisami prawa`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Prywatność i Ochrona Danych`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jesteśmy zobowiązani do ochrony Twojej prywatności. Twoje dane osobowe będą przetwarzane zgodnie z naszą Polityką Prywatności oraz obowiązującymi przepisami o ochronie danych. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Własność Intelektualna`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Usługa TimeTracker oraz wszystkie jej treści, w tym między innymi teksty, grafika, logo i oprogramowanie, stanowią własność TimeTracker i są chronione przepisami o własności intelektualnej. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Ograniczenie Odpowiedzialności`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker nie ponosi odpowiedzialności za jakiekolwiek pośrednie, przypadkowe, specjalne, następcze lub karne szkody wynikające z korzystania lub niemożności korzystania z usługi. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Rozwiązanie Umowy`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` zastrzegamy sobie prawo do rozwiązania lub zawieszenia Twojego konta w dowolnym momencie, bez wcześniejszego powiadomienia, za zachowanie, które narusza niniejszy Regulamin lub jest szkodliwe dla innych użytkowników lub usługi. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Zmiany w Regulaminie`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` zastrzegamy sobie prawo do modyfikacji niniejszego Regulaminu w dowolnym momencie. Dalsze korzystanie z TimeTracker po wprowadzeniu zmian oznacza akceptację nowych warunków. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Informacje Kontaktowe`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jeśli masz jakiekolwiek pytania dotyczące niniejszego Regulaminu, skontaktuj się z nami pod adresem support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

View File

@@ -0,0 +1 @@
import{F as e,Q as t,_ as n,f as r,h as i,y as a}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{$ as o,s}from"./tv-uB0-NqWK.js";import{t as c}from"./Card-DJGrWflS.js";var l={class:`min-h-screen bg-gradient-to-br from-primary-50 via-white to-primary-100 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8`},u={class:`max-w-4xl mx-auto`},d={class:`text-center mb-12`},f={class:`inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500 text-white mb-4 shadow-lg shadow-primary-500/30`},p=a({__name:`pl_TermsAndConditionsView`,setup(a){let{t:p}=o();return(a,o)=>{let p=s,m=c;return e(),i(`div`,l,[r(`div`,u,[r(`div`,d,[r(`div`,f,[n(p,{name:`i-heroicons-document-text`,class:`w-8 h-8`})]),o[0]||=r(`h1`,{class:`text-3xl font-bold text-gray-900 dark:text-white`},`Regulamin`,-1),o[1]||=r(`p`,{class:`mt-2 text-sm text-gray-600 dark:text-gray-400`},`Ostatnia aktualizacja: marzec 2026`,-1)]),n(m,{class:`shadow-xl shadow-gray-200/50 dark:shadow-gray-900/50`},{footer:t(()=>[...o[2]||=[r(`div`,{class:`flex justify-center`},null,-1)]]),default:t(()=>[o[3]||=r(`div`,{class:`prose prose-sm sm:prose dark:prose-invert max-w-none space-y-6`},[r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`1. Akceptacja Regulaminu`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Korzystając z aplikacji TimeTracker, akceptujesz i zgadzasz się na przestrzeganie warunków i postanowień niniejszej umowy. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`2. Opis Usługi`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker to aplikacja do śledzenia czasu pracy, która umożliwia użytkownikom śledzenie godzin pracy, zarządzanie projektami oraz generowanie raportów. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`3. Obowiązki Użytkownika`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},`Zgadzasz się na:`),r(`ul`,{class:`list-disc list-inside space-y-2 text-gray-600 dark:text-gray-400 ml-4`},[r(`li`,null,`Podawanie dokładnych i kompletnych informacji`),r(`li`,null,`Utrzymywanie bezpieczeństwa swojego konta`),r(`li`,null,`Nieudostępnianie danych logowania innym osobom`),r(`li`,null,`Korzystanie z usługi zgodnie z obowiązującymi przepisami prawa`)])]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`4. Prywatność i Ochrona Danych`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jesteśmy zobowiązani do ochrony Twojej prywatności. Twoje dane osobowe będą przetwarzane zgodnie z naszą Polityką Prywatności oraz obowiązującymi przepisami o ochronie danych. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`5. Własność Intelektualna`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Usługa TimeTracker oraz wszystkie jej treści, w tym między innymi teksty, grafika, logo i oprogramowanie, stanowią własność TimeTracker i są chronione przepisami o własności intelektualnej. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`6. Ograniczenie Odpowiedzialności`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` TimeTracker nie ponosi odpowiedzialności za jakiekolwiek pośrednie, przypadkowe, specjalne, następcze lub karne szkody wynikające z korzystania lub niemożności korzystania z usługi. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`7. Rozwiązanie Umowy`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` zastrzegamy sobie prawo do rozwiązania lub zawieszenia Twojego konta w dowolnym momencie, bez wcześniejszego powiadomienia, za zachowanie, które narusza niniejszy Regulamin lub jest szkodliwe dla innych użytkowników lub usługi. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`8. Zmiany w Regulaminie`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` zastrzegamy sobie prawo do modyfikacji niniejszego Regulaminu w dowolnym momencie. Dalsze korzystanie z TimeTracker po wprowadzeniu zmian oznacza akceptację nowych warunków. `)]),r(`section`,null,[r(`h2`,{class:`text-xl font-semibold text-gray-900 dark:text-white`},`9. Informacje Kontaktowe`),r(`p`,{class:`text-gray-600 dark:text-gray-400`},` Jeśli masz jakiekolwiek pytania dotyczące niniejszego Regulaminu, skontaktuj się z nami pod adresem support@timetracker.com. `)])],-1)]),_:1})])])}}});export{p as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import"./useFetchJson-4WJQFaEO.js";import"./useForwardExpose-BgPOLLFN.js";import"./Icon-Chkiq2IE.js";import"./auth-hZSBdvj-.js";import{t as e}from"./router-CoYWQDRi.js";import"./Button-jwL-tYHc.js";import"./settings-BcOmX106.js";export{e as default};

View File

@@ -0,0 +1 @@
import"./useFetchJson-BTB9doG4.js";import"./Button-Dys5wjZc.js";import"./tv-uB0-NqWK.js";import"./auth-DHyg2egq.js";import{t as e}from"./router-DDV1eCGp.js";import"./settings-84EZt-NQ.js";export{e as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auth-CdHmhksw.js","assets/auth-hZSBdvj-.js","assets/vue.runtime.esm-bundler-BM5WPBHd.js"])))=>i.map(i=>d[i]); const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auth-O--VICRG.js","assets/auth-DHyg2egq.js","assets/vue.runtime.esm-bundler-BM5WPBHd.js"])))=>i.map(i=>d[i]);
var e=`modulepreload`,t=function(e){return`/`+e},n={};const r=function(r,i,a){let o=Promise.resolve();if(i&&i.length>0){let r=document.getElementsByTagName(`link`),s=document.querySelector(`meta[property=csp-nonce]`),c=s?.nonce||s?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}o=l(i.map(i=>{if(i=t(i,a),i in n)return;n[i]=!0;let o=i.endsWith(`.css`),s=o?`[rel="stylesheet"]`:``;if(a)for(let e=r.length-1;e>=0;e--){let t=r[e];if(t.href===i&&(!o||t.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${i}"]${s}`))return;let l=document.createElement(`link`);if(l.rel=o?`stylesheet`:e,o||(l.as=`script`),l.crossOrigin=``,l.href=i,c&&l.setAttribute(`nonce`,c),document.head.appendChild(l),o)return new Promise((e,t)=>{l.addEventListener(`load`,e),l.addEventListener(`error`,()=>t(Error(`Unable to preload CSS for ${i}`)))})}))}function s(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return o.then(e=>{for(let t of e||[])t.status===`rejected`&&s(t.reason);return r().catch(s)})};async function i(e,t){let n=a(``,e),i=new Headers(t?.headers);i.has(`Content-Type`)||i.set(`Content-Type`,`application/json`);let o={...t,headers:i,credentials:`same-origin`};try{let e=await fetch(n,o);if(!(e.headers.get(`content-type`)??``).includes(`application/json`))throw{message:`this is not proper json format`};let t=await e.json();if(e.status===401){let{useAuthStore:e}=await r(async()=>{let{useAuthStore:e}=await import(`./auth-CdHmhksw.js`);return{useAuthStore:e}},__vite__mapDeps([0,1,2])),i=e();if(await i.refreshAccessToken()){let e=await fetch(n,o);if(!(e.headers.get(`content-type`)??``).includes(`application/json`))throw{message:`this is not proper json format`};let t=await e.json();if(!e.ok)throw t;return t}throw i.logout(),t}if(!e.ok)throw t;return t}catch(e){throw e}}function a(...e){let t=e.filter(Boolean).join(`/`).replace(/\/{2,}/g,`/`);return t.startsWith(`/`)?t:`/${t}`}export{r as n,i as t}; var e=`modulepreload`,t=function(e){return`/`+e},n={};const r=function(r,i,a){let o=Promise.resolve();if(i&&i.length>0){let r=document.getElementsByTagName(`link`),s=document.querySelector(`meta[property=csp-nonce]`),c=s?.nonce||s?.getAttribute(`nonce`);function l(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}o=l(i.map(i=>{if(i=t(i,a),i in n)return;n[i]=!0;let o=i.endsWith(`.css`),s=o?`[rel="stylesheet"]`:``;if(a)for(let e=r.length-1;e>=0;e--){let t=r[e];if(t.href===i&&(!o||t.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${i}"]${s}`))return;let l=document.createElement(`link`);if(l.rel=o?`stylesheet`:e,o||(l.as=`script`),l.crossOrigin=``,l.href=i,c&&l.setAttribute(`nonce`,c),document.head.appendChild(l),o)return new Promise((e,t)=>{l.addEventListener(`load`,e),l.addEventListener(`error`,()=>t(Error(`Unable to preload CSS for ${i}`)))})}))}function s(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return o.then(e=>{for(let t of e||[])t.status===`rejected`&&s(t.reason);return r().catch(s)})};async function i(e,t){let n=a(``,e),i=new Headers(t?.headers);i.has(`Content-Type`)||i.set(`Content-Type`,`application/json`);let o={...t,headers:i,credentials:`same-origin`};try{let e=await fetch(n,o);if(!(e.headers.get(`content-type`)??``).includes(`application/json`))throw{message:`this is not proper json format`};let t=await e.json();if(e.status===401){let{useAuthStore:e}=await r(async()=>{let{useAuthStore:e}=await import(`./auth-O--VICRG.js`);return{useAuthStore:e}},__vite__mapDeps([0,1,2])),i=e();if(await i.refreshAccessToken()){let e=await fetch(n,o);if(!(e.headers.get(`content-type`)??``).includes(`application/json`))throw{message:`this is not proper json format`};let t=await e.json();if(!e.ok)throw t;return t}throw i.logout(),t}if(!e.ok)throw t;return t}catch(e){throw e}}function a(...e){let t=e.filter(Boolean).join(`/`).replace(/\/{2,}/g,`/`);return t.startsWith(`/`)?t:`/${t}`}export{r as n,i as t};

View File

@@ -1 +0,0 @@
import{J as e,b as t,ct as n,d as r,ut as i}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{t as a}from"./useFetchJson-4WJQFaEO.js";import{E as o,Z as s}from"./Icon-Chkiq2IE.js";const c=()=>{function e(e){let t=document.cookie?document.cookie.split(`; `):[];for(let n of t){let[t,...r]=n.split(`=`);if(t===e)return decodeURIComponent(r.join(`=`))}return null}function t(e,t,n){let r=`${e}=${encodeURIComponent(t)}`;if(n?.days){let e=new Date;e.setTime(e.getTime()+n.days*24*60*60*1e3),r+=`; expires=${e.toUTCString()}`}r+=`; path=${n?.path??`/`}`,n?.domain&&(r+=`; domain=${n.domain}`),n?.secure&&(r+=`; Secure`),n?.sameSite&&(r+=`; SameSite=${n.sameSite}`),document.cookie=r}function n(e,t=`/`,n){let r=`${e}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${t}`;n&&(r+=`; domain=${n}`),document.cookie=r}return{getCookie:e,setCookie:t,deleteCookie:n}},l=n([]),u=i();var d=i(),f=c();async function p(){try{let{items:e}=await a(`/api/v1/langs`);l.push(...e);let t=null,n=f.getCookie(`lang_id`);n&&(t=l.find(e=>e.id==parseInt(n))),d.value=e.find(e=>e.is_default==1),u.value=t??d.value}catch(e){console.error(`Failed to fetch languages:`,e)}}const m=s({legacy:!1,locale:`en`,lazy:!0,messages:{},messageResolver:(e,t)=>{let n=t.split(`.`).reduce((e,t)=>e?.[t],e);return n===``||n==null?null:n}}),h=m.global;var g=[];e(h.locale,async e=>{if(!g.includes(e)){let t=l.find(t=>t.iso_code==e);if(!t)return;g.push(e);let n=await a(`/api/v1/translations?lang_id=${t?.id}&scope=backoffice`);h.setLocaleMessage(e,n.items[t.id].backoffice)}},{});function _(){let e=t(),n=i(),a=r(()=>[`#text`,`#comment`].includes(n.value?.$el.nodeName)?n.value?.$el.nextElementSibling:o(n)),s=Object.assign({},e.exposed),c={};for(let t in e.props)Object.defineProperty(c,t,{enumerable:!0,configurable:!0,get:()=>e.props[t]});if(Object.keys(s).length>0)for(let e in s)Object.defineProperty(c,e,{enumerable:!0,configurable:!0,get:()=>s[e]});Object.defineProperty(c,`$el`,{enumerable:!0,configurable:!0,get:()=>e.vnode.el}),e.exposed=c;function l(t){if(n.value=t,t&&(Object.defineProperty(c,`$el`,{enumerable:!0,configurable:!0,get:()=>t instanceof Element?t:t.$el}),!(t instanceof Element)&&!Object.prototype.hasOwnProperty.call(t,`$el`))){let n=t.$.exposed,r=Object.assign({},c);for(let e in n)Object.defineProperty(r,e,{enumerable:!0,configurable:!0,get:()=>n[e]});e.exposed=r}}return{forwardRef:l,currentRef:n,currentElement:a}}export{p as a,u as i,h as n,l as o,m as r,c as s,_ as t};

View File

@@ -0,0 +1 @@
import{b as e,d as t,ut as n}from"./vue.runtime.esm-bundler-BM5WPBHd.js";import{E as r}from"./tv-uB0-NqWK.js";function i(){let i=e(),a=n(),o=t(()=>[`#text`,`#comment`].includes(a.value?.$el.nodeName)?a.value?.$el.nextElementSibling:r(a)),s=Object.assign({},i.exposed),c={};for(let e in i.props)Object.defineProperty(c,e,{enumerable:!0,configurable:!0,get:()=>i.props[e]});if(Object.keys(s).length>0)for(let e in s)Object.defineProperty(c,e,{enumerable:!0,configurable:!0,get:()=>s[e]});Object.defineProperty(c,`$el`,{enumerable:!0,configurable:!0,get:()=>i.vnode.el}),i.exposed=c;function l(e){if(a.value=e,e&&(Object.defineProperty(c,`$el`,{enumerable:!0,configurable:!0,get:()=>e instanceof Element?e:e.$el}),!(e instanceof Element)&&!Object.prototype.hasOwnProperty.call(e,`$el`))){let t=e.$.exposed,n=Object.assign({},c);for(let e in t)Object.defineProperty(n,e,{enumerable:!0,configurable:!0,get:()=>t[e]});i.exposed=n}}return{forwardRef:l,currentRef:a,currentElement:o}}export{i as t};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
import{h as e}from"./usePortal-Zddbph8M.js";var t=[`Enter`,` `],n=[`ArrowDown`,`PageUp`,`Home`],r=[`ArrowUp`,`PageDown`,`End`];[...n,...r],[...t],[...t];function i(e){return e?`open`:`closed`}function a(t){let n=e();for(let r of t)if(r===n||(r.focus(),e()!==n))return}export{i as n,a as t}; import{h as e}from"./usePortal-BgeZHop8.js";var t=[`Enter`,` `],n=[`ArrowDown`,`PageUp`,`Home`],r=[`ArrowUp`,`PageDown`,`End`];[...n,...r],[...t],[...t];function i(e){return e?`open`:`closed`}function a(t){let n=e();for(let r of t)if(r===n||(r.focus(),e()!==n))return}export{i as n,a as t};

View File

@@ -16,21 +16,20 @@
var pageName = "default"; var pageName = "default";
globalThis.appInit = []; globalThis.appInit = [];
</script> </script>
<script type="module" crossorigin src="/assets/index-BqfKAJS4.js"></script> <script type="module" crossorigin src="/assets/index-DpkssS-Q.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vue.runtime.esm-bundler-BM5WPBHd.js"> <link rel="modulepreload" crossorigin href="/assets/vue.runtime.esm-bundler-BM5WPBHd.js">
<link rel="modulepreload" crossorigin href="/assets/useFetchJson-4WJQFaEO.js"> <link rel="modulepreload" crossorigin href="/assets/useFetchJson-BTB9doG4.js">
<link rel="modulepreload" crossorigin href="/assets/Icon-Chkiq2IE.js"> <link rel="modulepreload" crossorigin href="/assets/tv-uB0-NqWK.js">
<link rel="modulepreload" crossorigin href="/assets/Button-jwL-tYHc.js"> <link rel="modulepreload" crossorigin href="/assets/Button-Dys5wjZc.js">
<link rel="modulepreload" crossorigin href="/assets/HomeView-CdMOMcn8.js"> <link rel="modulepreload" crossorigin href="/assets/useForwardExpose-CEpqU5vT.js">
<link rel="modulepreload" crossorigin href="/assets/useForwardExpose-BgPOLLFN.js"> <link rel="modulepreload" crossorigin href="/assets/usePortal-BgeZHop8.js">
<link rel="modulepreload" crossorigin href="/assets/usePortal-Zddbph8M.js"> <link rel="modulepreload" crossorigin href="/assets/esm-BmwkJimY.js">
<link rel="modulepreload" crossorigin href="/assets/PopperArrow-CcUKYeE0.js"> <link rel="modulepreload" crossorigin href="/assets/settings-84EZt-NQ.js">
<link rel="modulepreload" crossorigin href="/assets/settings-BcOmX106.js"> <link rel="modulepreload" crossorigin href="/assets/auth-DHyg2egq.js">
<link rel="modulepreload" crossorigin href="/assets/auth-hZSBdvj-.js"> <link rel="modulepreload" crossorigin href="/assets/Collection-Dmox1UHc.js">
<link rel="modulepreload" crossorigin href="/assets/Collection-BkGqWqUl.js"> <link rel="modulepreload" crossorigin href="/assets/VisuallyHiddenInput-DPrwdEvl.js">
<link rel="modulepreload" crossorigin href="/assets/VisuallyHiddenInput-BH1aLUkb.js"> <link rel="modulepreload" crossorigin href="/assets/router-DDV1eCGp.js">
<link rel="modulepreload" crossorigin href="/assets/router-CoYWQDRi.js"> <link rel="stylesheet" crossorigin href="/assets/index-DLyy94LM.css">
<link rel="stylesheet" crossorigin href="/assets/index-UnLOO1Sq.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

Binary file not shown.

4
bo/components.d.ts vendored
View File

@@ -11,18 +11,17 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Button: typeof import('./src/components/custom/Button.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default'] En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default'] En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
Input: typeof import('./src/components/custom/Input.vue')['default']
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default'] LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default'] Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default'] Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default'] ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default']
TopBar: typeof import('./src/components/TopBar.vue')['default']
TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default'] TopBarLogin: typeof import('./src/components/TopBarLogin.vue')['default']
UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default'] UAlert: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default']
UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default'] UButton: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
@@ -37,5 +36,6 @@ declare module 'vue' {
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default'] UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default'] USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default'] USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
} }
} }

View File

@@ -11,14 +11,6 @@ export const uiOptions: NuxtUIOptions = {
root: '', root: '',
} }
}, },
// selectMenu: {
// variants: {
// size: {
// xxl: {
// group: 'mt-20!'
// },
// },
// },
button: { button: {
slots: { slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
@@ -27,115 +19,29 @@ export const uiOptions: NuxtUIOptions = {
input: { input: {
slots: { slots: {
base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
error: 'text-red-600!'
}, },
}, },
// variants: {
// size: {
// xxl: {
// base: 'h-8 sm:h-[38px] px-[10px] py-[10px] border! border-(--border-light)! dark:border-(--border-dark)!',
// trailingIcon: 'px-6 !text-base',
// root: 'w-full',
// },
// },
// },
// defaultVariants: {
// size: 'xxl',
// },
// },
// textarea: {
// slots: {
// base: 'disabled:!opacity-100 text-(--black) dark:text-white disabled:text-(--gray) !text-base placeholder:text-(--gray)/50! dark:placeholder:text-(--gray)!',
// trailingIcon: 'shrink-0 pr-4 !text-base',
// error: 'text-sm! !sm:text-[15px] leading-none! mt-1!',
// },
// variants: {
// size: {
// xxl: {
// base: 'px-[25px] py-[15px]',
// trailingIcon: 'px-6 !text-base',
// root: 'w-full',
// },
// },
// },
// defaultVariants: {
// size: 'xxl',
// },
// },
// formField: {
// slots: {
// base: 'flex !flex-col border! border-(--border-light)! dark:border-(--border-dark)!',
// label: 'text-[15px] text-(--gray)! dark:text-(--gray-dark)! pl-6! leading-none! font-normal! mb-1 sm:mb-1',
// error: 'text-sm! !sm:text-[15px] leading-none! mt-1!',
// },
// variants: {
// size: {
// xxl: 'w-full',
// label: '!label !mb-1',
// },
// },
// defaultVariants: {
// size: 'xxl',
// },
// },
// defaultVariants: {
// size: 'xxl',
// },
// },
select: { select: {
slots: { slots: {
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
itemLabel: 'text-black! dark:text-white!', itemLabel: 'text-black! dark:text-white!',
itemTrailingIcon: 'text-black! dark:text-white!' itemTrailingIcon: 'text-black! dark:text-white!'
}, },
// variants: { },
// size: { formField: {
// xxl: { slots: {
// base: ' h-12 sm:h-[54px] px-[25px]', error: 'mt-1! text-[14px] text-error text-red-600! dark:text-red-400!',
// item: 'py-2 px-2', label: 'text-[16px]'
// trailingIcon: 'px-6 !text-base', },
// leading: '!px-[25px]', },
// itemLabel: 'text-black dark:text-white', selectMenu: {
// }, slots: {
// }, base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
// }, content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
// defaultVariants: { itemLeadingIcon: 'text-(--black)! dark:text-white!'
// size: 'xxl', }
// },
// },
// inputDate: {
// slots: {
// leadingIcon: 'border-none! outline-0! ring-0!',
// },
// defaultVariants: {
// size: 'xxl',
// },
// },
// checkbox: {
// slots: {
// label: 'block !font-normal',
// indicator: '!bg-(--accent-brown)',
// },
// },
// radioGroup: {
// slots: {
// label: 'block !font-normal text-base font-normal leading-none text-(--black) dark:text-(--second-light)',
// indicator: '!bg-(--accent-brown)',
// size: 'xxl',
// },
// },
// modal: {
// slots: {
// overlay: 'dark:bg-(--main-dark)/90',
// },
// },
// tooltip: {
// slots: {
// content: 'max-w-60 sm:max-w-100 bg-(--main-light)! dark:bg-(--black)! w-full h-full',
// text: 'whitespace-normal',
// },
} }
} }
} }

View File

@@ -25,14 +25,6 @@ body {
--gray: #6B6B6B; --gray: #6B6B6B;
--gray-dark: #A3A3A3; --gray-dark: #A3A3A3;
--accent-green: #004F3D;
--accent-green-dark: #00A882;
--accent-brown: #9A7F62;
--accent-red: #B72D2D;
--dark-red: #F94040;
--accent-orange: #E68D2B;
--accent-blue: #002B4F;
/* borders */ /* borders */
--border-light: #E8E7E0; --border-light: #E8E7E0;
--border-dark: #3F3E3D; --border-dark: #3F3E3D;
@@ -44,16 +36,9 @@ body {
--placeholder: #8C8C8A; --placeholder: #8C8C8A;
--ui-bg: var(--main-light); --ui-bg: var(--main-light);
--ui-primary: var(--color-gray-300);
--ui-secondary: var(--accent-green);
--ui-border-accented: var(--border-light); --ui-border-accented: var(--border-light);
--ui-text-dimmed: var(--gray);
--ui-bg-elevated: var(--color-gray-300);
--ui-border: var(--border-light); --ui-border: var(--border-light);
--ui-color-neutral-700: var(--black); --ui-color-neutral-700: var(--black);
--ui-error: var(--accent-red);
--border: var(--border-light);
--tw-border-style: var(--border-light);
} }
.dark { .dark {

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import LangSwitch from './inner/langSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
</script>
<template>
<header
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<div class="container px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14">
<!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="i-heroicons-clock" class="w-5 h-5" />
</div>
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink>
<!-- Right Side Actions -->
<div class="flex items-center gap-2">
<!-- Language Switcher -->
<LangSwitch />
<!-- Theme Switcher -->
<ThemeSwitch />
<!-- Logout Button (only when authenticated) -->
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark)">
Logout
</button>
</div>
</div>
</div>
</header>
</template>

View File

@@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import HomeView from '@/views/HomeView.vue';
import LangSwitch from './inner/langSwitch.vue' import LangSwitch from './inner/langSwitch.vue'
import ThemeSwitch from './inner/themeSwitch.vue' import ThemeSwitch from './inner/themeSwitch.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@@ -20,17 +19,11 @@ const authStore = useAuthStore()
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
</RouterLink> </RouterLink>
<!-- Right Side Actions --> <!-- Right Side Actions -->
<HomeView />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Language Switcher --> <!-- Language Switcher -->
<LangSwitch /> <LangSwitch />
<!-- Theme Switcher --> <!-- Theme Switcher -->
<ThemeSwitch /> <ThemeSwitch />
<!-- Logout Button (only when authenticated) -->
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 hover:text-primary dark:hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
Logout
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,15 +0,0 @@
<template>
<button :type="type" :disabled="disabled"
:class="['px-[25px] h-[43px] leading-none rounded-md text-[15px] sm:text-[16px] dark:text-white text-black',
fillType === 'border' ? 'border border-(--border-light) dark:border-(--border-dark)' : false,]">
<slot />
</button>
</template>
<script setup lang="ts">
withDefaults(defineProps<{ type?: 'button' | 'submit', fillType?: 'border', disabled?: boolean }>(), {
type: 'button',
fillType: 'border',
disabled: false,
})
</script>

View File

@@ -1,6 +1,5 @@
<template> <template>
<USelectMenu v-model="locale" :items="langs" <USelectMenu v-model="locale" :items="langs" class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
valueKey="iso_code" :searchInput="false"> valueKey="iso_code" :searchInput="false">
<template #default="{ modelValue }"> <template #default="{ modelValue }">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
@@ -22,7 +21,7 @@ import { langs, currentLang } from '@/router/langs'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useCookie } from '@/composable/useCookie' import { useCookie } from '@/composable/useCookie'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
import { i18n } from '@/plugins/i18n' import { i18n } from '@/plugins/02_i18n'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -36,28 +35,23 @@ const locale = computed({
i18n.locale.value = value i18n.locale.value = value
currentLang.value = langs.find((x) => x.iso_code == value) currentLang.value = langs.find((x) => x.iso_code == value)
// Update URL to reflect language change
const currentPath = route.path const currentPath = route.path
const pathParts = currentPath.split('/').filter(Boolean) const pathParts = currentPath.split('/').filter(Boolean)
cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' }) cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
if (pathParts.length > 0) { if (pathParts.length > 0) {
// Check if first part is a locale
const isLocale = langs.some((l) => l.lang_code === pathParts[0]) const isLocale = langs.some((l) => l.lang_code === pathParts[0])
if (isLocale) { if (isLocale) {
// Replace existing locale
pathParts[0] = value pathParts[0] = value
router.replace({ path: '/' + pathParts.join('/'), query: route.query }) router.replace({ path: '/' + pathParts.join('/'), query: route.query })
} else { } else {
// Add locale to path
router.replace({ path: '/' + value + currentPath, query: route.query }) router.replace({ path: '/' + value + currentPath, query: route.query })
} }
} }
}, },
}) })
// Sync i18n locale with router locale on initial load
watch( watch(
() => route.params.locale, () => route.params.locale,
(newLocale) => { (newLocale) => {

View File

@@ -1,7 +1,6 @@
import { useFetchJson } from './useFetchJson' import { useFetchJson } from './useFetchJson'
import type { Resp } from '@/types/response'
const API_PREFIX = '/api/v1/repo' const API_PREFIX = '/api/v1/restricted/repo'
export interface QuarterData { export interface QuarterData {
quarter: string quarter: string
@@ -35,7 +34,6 @@ export async function getRepos(): Promise<any> {
// export async function getYears(repoID: number): Promise<any> { // export async function getYears(repoID: number): Promise<any> {
// return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`) // return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`)
// } // }
// console.log(getYears(), 'leraaaaaa')
export async function getYears(repoID: number): Promise<any> { export async function getYears(repoID: number): Promise<any> {
return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`); return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`);
@@ -61,7 +59,6 @@ export async function getQuarters(repoID: number, year: number): Promise<any> {
// } // }
// async function logYears() { // async function logYears() {
// const years = await getIssues(7); // pass a repoID // const years = await getIssues(7); // pass a repoID
// console.log(years, 'leraaaaaa');
// } // }
export async function getIssues( export async function getIssues(
repoID: number, repoID: number,
@@ -74,14 +71,3 @@ export async function getIssues(
`${API_PREFIX}/get-issues?repoID=${repoID}&year=${year}&quarter=${quarter}&page_number=${page}&elements_per_page=${pageSize}` `${API_PREFIX}/get-issues?repoID=${repoID}&year=${year}&quarter=${quarter}&page_number=${page}&elements_per_page=${pageSize}`
); );
} }
// Correct logging function
async function logIssues() {
const repoID = 7;
const year = 2026; // example year
const quarter = 1; // example quarter
const issues = await getIssues(repoID, year, quarter);
console.log(issues, 'leraaaaaa');
}
logIssues();

View File

@@ -1,7 +1,7 @@
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { FormError } from '@nuxt/ui' import type { FormError } from '@nuxt/ui'
import { settings } from '@/router/settings' import { settings } from '@/router/settings'
import { i18n } from '@/plugins/i18n' import { i18n } from '@/plugins/02_i18n'
export const useValidation = () => { export const useValidation = () => {
const errors = [] as FormError[] const errors = [] as FormError[]

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import TopBarLogin from '@/components/TopBarLogin.vue' import TopBar from '@/components/TopBar.vue';
</script> </script>
<template> <template>
@@ -7,6 +8,7 @@ import TopBarLogin from '@/components/TopBarLogin.vue'
<!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> --> <!-- <header class="w-full bg-gray-100 text-primary shadow border-b-gray-300 p-4 mb-8">Header</header> -->
<UContainer> <UContainer>
<main class="p-10"> <main class="p-10">
<TopBar/>
<router-view /> <router-view />
</main> </main>
</UContainer> </UContainer>

Some files were not shown because too many files have changed in this diff Show More