timetracker update
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "timeTracker API",
|
||||
"description": "Authentication and user management API",
|
||||
"description": "Authentication, user management, and repository time tracking API",
|
||||
"version": "1.0.0",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
@@ -22,15 +22,15 @@
|
||||
},
|
||||
{
|
||||
"name": "Auth",
|
||||
"description": "Authentication endpoints"
|
||||
"description": "Authentication endpoints (under /api/v1/public/auth)"
|
||||
},
|
||||
{
|
||||
"name": "Languages",
|
||||
"description": "Language and translation endpoints"
|
||||
},
|
||||
{
|
||||
"name": "Protected",
|
||||
"description": "Protected routes requiring authentication"
|
||||
"name": "Repo",
|
||||
"description": "Repository time tracking data endpoints (under /api/v1/restricted/repo, requires authentication)"
|
||||
},
|
||||
{
|
||||
"name": "Admin",
|
||||
@@ -208,11 +208,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/login": {
|
||||
"/api/v1/public/auth/login": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -239,12 +239,12 @@
|
||||
"schema": {
|
||||
"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": {
|
||||
"description": "Invalid request body",
|
||||
"description": "Invalid request body or missing fields",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -276,11 +276,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/register": {
|
||||
"/api/v1/public/auth/register": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -310,7 +310,17 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -322,11 +332,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/complete-registration": {
|
||||
"/api/v1/public/auth/complete-registration": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -347,10 +357,18 @@
|
||||
"$ref": "#/components/schemas/AuthResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"Set-Cookie": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "HTTPOnly cookies: access_token, refresh_token, is_authenticated"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid token",
|
||||
"description": "Invalid or expired token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -362,11 +380,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/forgot-password": {
|
||||
"/api/v1/public/auth/forgot-password": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -404,7 +422,7 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request",
|
||||
"description": "Invalid request or missing email",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -416,11 +434,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/reset-password": {
|
||||
"/api/v1/public/auth/reset-password": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -450,7 +468,7 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid or expired token",
|
||||
"description": "Invalid or expired token, or invalid password format",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -462,11 +480,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/logout": {
|
||||
"/api/v1/public/auth/logout": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"summary": "User logout",
|
||||
"description": "Clear authentication cookies",
|
||||
"description": "Revokes the refresh token from the database and clears all authentication cookies.",
|
||||
"operationId": "logout",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -488,11 +506,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/refresh": {
|
||||
"/api/v1/public/auth/refresh": {
|
||||
"post": {
|
||||
"tags": ["Auth"],
|
||||
"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",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -502,7 +520,7 @@
|
||||
"properties": {
|
||||
"refresh_token": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"headers": {
|
||||
"Set-Cookie": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Rotated HTTPOnly cookies: access_token, refresh_token, is_authenticated"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
@@ -543,27 +569,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/protected/dashboard": {
|
||||
"/api/v1/public/auth/me": {
|
||||
"get": {
|
||||
"tags": ["Protected"],
|
||||
"summary": "Get dashboard data",
|
||||
"description": "Protected route requiring authentication",
|
||||
"tags": ["Auth"],
|
||||
"summary": "Get current user",
|
||||
"description": "Returns the currently authenticated user's session information. Requires authentication via cookie.",
|
||||
"operationId": "getMe",
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Dashboard data",
|
||||
"description": "Current user info",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserSession"
|
||||
}
|
||||
@@ -585,29 +609,144 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/users": {
|
||||
"/api/v1/public/auth/google": {
|
||||
"get": {
|
||||
"tags": ["Admin"],
|
||||
"summary": "Get all users",
|
||||
"description": "Admin-only endpoint for user management",
|
||||
"tags": ["Auth"],
|
||||
"summary": "Google OAuth2 login",
|
||||
"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": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
"CookieAuth": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of users",
|
||||
"description": "List of repository IDs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint"
|
||||
},
|
||||
"example": [1, 2, 5]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid user session",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -620,9 +759,235 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Admin access required",
|
||||
"/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]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"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": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -675,7 +1040,13 @@
|
||||
},
|
||||
"RegisterRequest": {
|
||||
"type": "object",
|
||||
"required": ["email", "password", "confirm_password"],
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"confirm_password",
|
||||
"first_name",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
@@ -685,7 +1056,7 @@
|
||||
"password": {
|
||||
"type": "string",
|
||||
"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": {
|
||||
"type": "string",
|
||||
@@ -694,15 +1065,15 @@
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string",
|
||||
"description": "User's first name"
|
||||
"description": "User's first name (required)"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string",
|
||||
"description": "User's last name"
|
||||
"description": "User's last name (required)"
|
||||
},
|
||||
"lang": {
|
||||
"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": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "Email verification token"
|
||||
"description": "Email verification token received via email"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -722,12 +1093,12 @@
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "Password reset token"
|
||||
"description": "Password reset token received via email"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"description": "New password"
|
||||
"description": "New password (must meet complexity requirements)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -738,17 +1109,13 @@
|
||||
"type": "string",
|
||||
"description": "JWT access token"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string",
|
||||
"description": "JWT refresh token"
|
||||
},
|
||||
"token_type": {
|
||||
"type": "string",
|
||||
"example": "Bearer"
|
||||
},
|
||||
"expires_in": {
|
||||
"type": "integer",
|
||||
"description": "Token expiration in seconds"
|
||||
"description": "Access token expiration in seconds"
|
||||
},
|
||||
"user": {
|
||||
"$ref": "#/components/schemas/UserSession"
|
||||
@@ -780,6 +1147,10 @@
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"lang": {
|
||||
"type": "string",
|
||||
"description": "User's preferred language ISO code (e.g., 'en', 'pl', 'cs')"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -788,7 +1159,7 @@
|
||||
"properties": {
|
||||
"error": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -872,6 +1312,10 @@
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"description": "Base URL of the application"
|
||||
},
|
||||
"password_regex": {
|
||||
"type": "string",
|
||||
"description": "Regular expression for password validation"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -893,7 +1337,7 @@
|
||||
"properties": {
|
||||
"jwt_expiration": {
|
||||
"type": "integer",
|
||||
"description": "JWT token expiration in seconds"
|
||||
"description": "JWT access token expiration in seconds"
|
||||
},
|
||||
"refresh_expiration": {
|
||||
"type": "integer",
|
||||
@@ -919,25 +1363,32 @@
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Application version"
|
||||
"description": "Application version (git tag or commit hash)"
|
||||
},
|
||||
"commit": {
|
||||
"type": "string",
|
||||
"description": "Git commit hash"
|
||||
"description": "Short git commit hash"
|
||||
},
|
||||
"date": {
|
||||
"build_date": {
|
||||
"type": "string",
|
||||
"description": "Build date"
|
||||
"format": "date-time",
|
||||
"description": "Build date in RFC3339 format"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"CookieAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "cookie",
|
||||
"name": "access_token",
|
||||
"description": "HTTPOnly JWT access token cookie set during login, registration, or token refresh"
|
||||
},
|
||||
"BearerAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
"description": "JWT token obtained from login response"
|
||||
"description": "JWT token obtained from login response (alternative to cookie-based auth)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
log.Println("Translations loaded successfully on startup")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"strconv"
|
||||
"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"
|
||||
)
|
||||
|
||||
// LanguageMiddleware discovers client's language and stores it in context
|
||||
// Priority: Query param > Cookie > Accept-Language header > Default language
|
||||
func LanguageMiddleware() fiber.Handler {
|
||||
langService := langs.LangSrv
|
||||
langService := langsService.LangSrv
|
||||
|
||||
return func(c fiber.Ctx) error {
|
||||
var langID uint
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
package public
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type LangHandler struct {
|
||||
service langs.LangService
|
||||
service langsService.LangService
|
||||
}
|
||||
|
||||
func NewLangHandler() *LangHandler {
|
||||
return &LangHandler{
|
||||
service: *langs.LangSrv,
|
||||
service: *langsService.LangSrv,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LangHandler) InitLanguage(api fiber.Router, cfg *config.Config) {
|
||||
|
||||
api.Get("langs", h.GetLanguages)
|
||||
api.Get("translations", h.GetTranslations)
|
||||
api.Get("translations/reload", h.ReloadTranslations)
|
||||
api.Get("/langs", h.GetLanguages)
|
||||
api.Get("/translations", h.GetTranslations)
|
||||
api.Get("/translations/reload", h.ReloadTranslations)
|
||||
}
|
||||
|
||||
func (h *LangHandler) GetLanguages(c fiber.Ctx) error {
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"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/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"
|
||||
)
|
||||
@@ -56,22 +56,22 @@ func (h *AuthHandler) Login(c fiber.Ctx) error {
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Email == "" || req.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrEmailPasswordRequired),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrEmailPasswordRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
response, rawRefreshToken, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if req.Email == "" {
|
||||
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 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Token == "" || req.Password == "" {
|
||||
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)
|
||||
err := h.authService.ResetPassword(req.Token, req.Password)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
|
||||
|
||||
if rawRefreshToken == "" {
|
||||
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 refresh token is invalid/expired, clear cookies
|
||||
h.clearAuthCookies(c)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func (h *AuthHandler) Me(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
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 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.FirstName == "" || req.LastName == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrFirstLastNameRequired),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrFirstLastNameRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Email == "" || req.Password == "" {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Printf("Register error: %v", err)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrTokenRequired),
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrTokenRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt to complete registration
|
||||
response, rawRefreshToken, err := h.authService.CompleteRegistration(&req)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -398,8 +398,8 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
||||
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
|
||||
if err != nil {
|
||||
log.Printf("Google OAuth callback error: %v", err)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -412,5 +412,5 @@ func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang + "/chart")
|
||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package restricted
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"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/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"
|
||||
)
|
||||
@@ -41,15 +41,15 @@ func RepoHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
func (h *RepoHandler) GetRepoIDs(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetRepositoriesForUser(userID)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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 {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetYearsForUser(userID, uint(repoID))
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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 {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
year_attribute := c.Query("year")
|
||||
year, err := strconv.Atoi(year_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadYearAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadYearAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetQuartersForUser(userID, uint(repoID), uint(year))
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"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 {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
year_attribute := c.Query("year")
|
||||
year, err := strconv.Atoi(year_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadYearAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadYearAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
quarter_attribute := c.Query("quarter")
|
||||
quarter, err := strconv.Atoi(quarter_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadQuarterAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadQuarterAttribute),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadQuarterAttribute)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, responseErrors.ErrBadQuarterAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
page_number_attribute := c.Query("page_number")
|
||||
page_number, err := strconv.Atoi(page_number_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadPaging),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadPaging)).JSON(fiber.Map{
|
||||
"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)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadPaging),
|
||||
return c.Status(responseErrors.GetErrorStatus(responseErrors.ErrBadPaging)).JSON(fiber.Map{
|
||||
"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)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
return c.Status(responseErrors.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": responseErrors.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package general
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/assets"
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package general
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package general
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
@@ -1,4 +1,4 @@
|
||||
package public
|
||||
package general
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/api"
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"time"
|
||||
|
||||
"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/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/v3"
|
||||
@@ -26,6 +28,8 @@ type Server struct {
|
||||
app *fiber.App
|
||||
cfg *config.Config
|
||||
api fiber.Router
|
||||
public fiber.Router
|
||||
restricted fiber.Router
|
||||
}
|
||||
|
||||
// App returns the fiber app
|
||||
@@ -61,54 +65,57 @@ func (s *Server) Setup() error {
|
||||
s.app.Use(middleware.LanguageMiddleware())
|
||||
|
||||
// initialize healthcheck
|
||||
public.InitHealth(s.App(), s.Cfg())
|
||||
general.InitHealth(s.App(), s.Cfg())
|
||||
|
||||
// serve favicon
|
||||
public.Favicon(s.app, s.cfg)
|
||||
general.Favicon(s.app, s.cfg)
|
||||
|
||||
// initialize swagger endpoints
|
||||
general.InitSwagger(s.App())
|
||||
|
||||
// API routes
|
||||
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
|
||||
public.InitSwagger(s.App())
|
||||
// initialize language endpoints (general)
|
||||
api.NewLangHandler().InitLanguage(s.api, s.cfg)
|
||||
|
||||
// Settings endpoint (general)
|
||||
api.NewSettingsHandler().InitSettings(s.api, s.cfg)
|
||||
|
||||
// Auth routes (public)
|
||||
auth := s.api.Group("/auth")
|
||||
handler.AuthHandlerRoutes(auth)
|
||||
auth := s.public.Group("/auth")
|
||||
public.AuthHandlerRoutes(auth)
|
||||
|
||||
// Repo routes (public)
|
||||
repo := s.api.Group("/repo")
|
||||
repo.Use(middleware.AuthMiddleware())
|
||||
handler.RepoHandlerRoutes(repo)
|
||||
// Repo routes (restricted)
|
||||
repo := s.restricted.Group("/repo")
|
||||
restricted.RepoHandlerRoutes(repo)
|
||||
|
||||
// Protected routes example
|
||||
protected := s.api.Group("/restricted")
|
||||
protected.Use(middleware.AuthMiddleware())
|
||||
protected.Get("/dashboard", func(c fiber.Ctx) error {
|
||||
user := middleware.GetUser(c)
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "Welcome to the protected area",
|
||||
"user": user,
|
||||
})
|
||||
})
|
||||
// // Restricted routes example
|
||||
// restricted := s.api.Group("/restricted")
|
||||
// restricted.Use(middleware.AuthMiddleware())
|
||||
// restricted.Get("/dashboard", func(c fiber.Ctx) error {
|
||||
// user := middleware.GetUser(c)
|
||||
// return c.JSON(fiber.Map{
|
||||
// "message": "Welcome to the protected area",
|
||||
// "user": user,
|
||||
// })
|
||||
// })
|
||||
|
||||
// Admin routes example
|
||||
admin := s.api.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware())
|
||||
admin.Use(middleware.RequireAdmin())
|
||||
admin.Get("/users", func(c fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "Admin area - user management",
|
||||
})
|
||||
})
|
||||
|
||||
public.NewLangHandler().InitLanguage(s.api, s.cfg)
|
||||
|
||||
// Settings endpoint
|
||||
public.NewSettingsHandler().InitSettings(s.api, s.cfg)
|
||||
// // Admin routes example
|
||||
// admin := s.api.Group("/admin")
|
||||
// admin.Use(middleware.AuthMiddleware())
|
||||
// admin.Use(middleware.RequireAdmin())
|
||||
// admin.Get("/users", func(c fiber.Ctx) error {
|
||||
// return c.JSON(fiber.Map{
|
||||
// "message": "Admin area - user management",
|
||||
// })
|
||||
// })
|
||||
|
||||
// keep this at the end because its wilderange
|
||||
public.InitBo(s.App())
|
||||
general.InitBo(s.App())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -1,15 +1,35 @@
|
||||
package view
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/model"
|
||||
"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 {
|
||||
Years []uint
|
||||
Quarters []model.QuarterData
|
||||
Quarters []QuarterData
|
||||
QuartersJSON string
|
||||
Year uint
|
||||
}
|
||||
@@ -20,7 +40,7 @@ type TimeTrackedData struct {
|
||||
Quarter uint
|
||||
Step string
|
||||
TotalTime float64
|
||||
DailyData []model.DayData
|
||||
DailyData []DayData
|
||||
DailyDataJSON string
|
||||
Years []uint
|
||||
IssueSummaries *pagination.Found[IssueTimeSummary]
|
||||
@@ -29,8 +49,6 @@ type TimeTrackedData struct {
|
||||
type IssueTimeSummary struct {
|
||||
IssueID uint `gorm:"column:issue_id"`
|
||||
IssueName string `gorm:"column:issue_name"`
|
||||
UserID uint `gorm:"column:user_id"`
|
||||
Initials string `gorm:"column:initials"`
|
||||
CreatedDate time.Time `gorm:"column:created_date"`
|
||||
CreatedDate time.Time `gorm:"column:issue_created_at"`
|
||||
TotalHoursSpent float64 `gorm:"column:total_hours_spent"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/model"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/emailService"
|
||||
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/golang-jwt/jwt/v5"
|
||||
@@ -60,23 +60,23 @@ func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, strin
|
||||
// Find user by email
|
||||
if err := s.db.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", view.ErrInvalidCredentials
|
||||
return nil, "", responseErrors.ErrInvalidCredentials
|
||||
}
|
||||
return nil, "", fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
// Check if user is active
|
||||
if !user.IsActive {
|
||||
return nil, "", view.ErrUserInactive
|
||||
return nil, "", responseErrors.ErrUserInactive
|
||||
}
|
||||
|
||||
// Check if email is verified
|
||||
if !user.EmailVerified {
|
||||
return nil, "", view.ErrEmailNotVerified
|
||||
return nil, "", responseErrors.ErrEmailNotVerified
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return nil, "", view.ErrInvalidCredentials
|
||||
return nil, "", responseErrors.ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
@@ -109,17 +109,17 @@ func (s *AuthService) Register(req *model.RegisterRequest) error {
|
||||
// Check if email already exists
|
||||
var existingUser model.Customer
|
||||
if err := s.db.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
return view.ErrEmailExists
|
||||
return responseErrors.ErrEmailExists
|
||||
}
|
||||
|
||||
// Validate passwords match
|
||||
if req.Password != req.ConfirmPassword {
|
||||
return view.ErrPasswordsDoNotMatch
|
||||
return responseErrors.ErrPasswordsDoNotMatch
|
||||
}
|
||||
|
||||
// Validate password strength
|
||||
if err := validatePassword(req.Password); err != nil {
|
||||
return view.ErrInvalidPassword
|
||||
return responseErrors.ErrInvalidPassword
|
||||
}
|
||||
|
||||
// Hash password
|
||||
@@ -176,14 +176,14 @@ func (s *AuthService) CompleteRegistration(req *model.CompleteRegistrationReques
|
||||
var user model.Customer
|
||||
if err := s.db.Where("email_verification_token = ?", req.Token).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", view.ErrInvalidVerificationToken
|
||||
return nil, "", responseErrors.ErrInvalidVerificationToken
|
||||
}
|
||||
return nil, "", fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
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
|
||||
@@ -283,19 +283,19 @@ func (s *AuthService) ResetPassword(token, newPassword string) error {
|
||||
var user model.Customer
|
||||
if err := s.db.Where("password_reset_token = ?", token).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return view.ErrInvalidResetToken
|
||||
return responseErrors.ErrInvalidResetToken
|
||||
}
|
||||
return fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if user.PasswordResetExpires == nil || user.PasswordResetExpires.Before(time.Now()) {
|
||||
return view.ErrResetTokenExpired
|
||||
return responseErrors.ErrResetTokenExpired
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
if err := validatePassword(newPassword); err != nil {
|
||||
return view.ErrInvalidPassword
|
||||
return responseErrors.ErrInvalidPassword
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
@@ -330,14 +330,14 @@ func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) {
|
||||
|
||||
if err != nil {
|
||||
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)
|
||||
if !ok || !token.Valid {
|
||||
return nil, view.ErrInvalidToken
|
||||
return nil, responseErrors.ErrInvalidToken
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
@@ -352,7 +352,7 @@ func (s *AuthService) RefreshToken(rawToken string) (*model.AuthResponse, string
|
||||
var rt model.RefreshToken
|
||||
if err := s.db.Where("token_hash = ?", tokenHash).First(&rt).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", view.ErrInvalidToken
|
||||
return nil, "", responseErrors.ErrInvalidToken
|
||||
}
|
||||
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()) {
|
||||
// Clean up expired token
|
||||
s.db.Delete(&rt)
|
||||
return nil, "", view.ErrTokenExpired
|
||||
return nil, "", responseErrors.ErrTokenExpired
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
var user model.Customer
|
||||
if err := s.db.First(&user, rt.CustomerID).Error; err != nil {
|
||||
return nil, "", view.ErrUserNotFound
|
||||
return nil, "", responseErrors.ErrUserNotFound
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
return nil, "", view.ErrUserInactive
|
||||
return nil, "", responseErrors.ErrUserInactive
|
||||
}
|
||||
|
||||
if !user.EmailVerified {
|
||||
return nil, "", view.ErrEmailNotVerified
|
||||
return nil, "", responseErrors.ErrEmailNotVerified
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := s.db.First(&user, userID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, view.ErrUserNotFound
|
||||
return nil, responseErrors.ErrUserNotFound
|
||||
}
|
||||
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
|
||||
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, view.ErrUserNotFound
|
||||
return nil, responseErrors.ErrUserNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("database error: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"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/utils/responseErrors"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/view"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
@@ -19,17 +20,6 @@ import (
|
||||
|
||||
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
|
||||
func googleOAuthConfig() *oauth2.Config {
|
||||
cfg := config.Get().OAuth.Google
|
||||
@@ -81,7 +71,7 @@ func (s *AuthService) HandleGoogleCallback(code string) (*model.AuthResponse, st
|
||||
}
|
||||
|
||||
if !userInfo.VerifiedEmail {
|
||||
return nil, "", view.ErrEmailNotVerified
|
||||
return nil, "", responseErrors.ErrEmailNotVerified
|
||||
}
|
||||
|
||||
// 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,
|
||||
// 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
|
||||
|
||||
// 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
|
||||
func fetchGoogleUserInfo(client *http.Client) (*GoogleUserInfo, error) {
|
||||
func fetchGoogleUserInfo(client *http.Client) (*view.GoogleUserInfo, error) {
|
||||
resp, err := client.Get(googleUserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -195,7 +185,7 @@ func fetchGoogleUserInfo(client *http.Client) (*GoogleUserInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userInfo GoogleUserInfo
|
||||
var userInfo view.GoogleUserInfo
|
||||
if err := json.Unmarshal(body, &userInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/utils/i18n"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/view"
|
||||
@@ -32,7 +32,7 @@ func getLangID(isoCode string) uint {
|
||||
isoCode = "en"
|
||||
}
|
||||
|
||||
lang, err := langs.LangSrv.GetLanguageByISOCode(isoCode)
|
||||
lang, err := langsService.LangSrv.GetLanguageByISOCode(isoCode)
|
||||
if err != nil || lang == nil {
|
||||
return 1 // Default to English (ID 1)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package langs
|
||||
package langsService
|
||||
|
||||
import (
|
||||
langs_repo "git.ma-al.com/goc_marek/timetracker/app/langs"
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"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/utils/pagination"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/view"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/responseErrors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (s *RepoService) UserHasAccessToRepo(userID uint, repoID uint) (bool, error
|
||||
}
|
||||
|
||||
if !slices.Contains(repositories, repoID) {
|
||||
return false, view.ErrInvalidRepoID
|
||||
return false, responseErrors.ErrInvalidRepoID
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -147,140 +147,19 @@ func (s *RepoService) GetQuarters(repo uint, year uint) ([]model.QuarterData, er
|
||||
Find(&quarters).
|
||||
Error
|
||||
if err != nil {
|
||||
fmt.Printf("err: %v\n", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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(
|
||||
userID uint,
|
||||
repoID uint,
|
||||
year uint,
|
||||
quarter uint,
|
||||
p pagination.Paging,
|
||||
) (*pagination.Found[view.IssueTimeSummary], error) {
|
||||
) (*pagination.Found[model.IssueTimeSummary], error) {
|
||||
if ok, err := s.UserHasAccessToRepo(userID, repoID); !ok {
|
||||
return nil, err
|
||||
}
|
||||
@@ -293,21 +172,14 @@ func (s *RepoService) GetIssues(
|
||||
year uint,
|
||||
quarter uint,
|
||||
p pagination.Paging,
|
||||
) (*pagination.Found[view.IssueTimeSummary], error) {
|
||||
) (*pagination.Found[model.IssueTimeSummary], error) {
|
||||
|
||||
query := db.Get().Debug().
|
||||
query := db.Get().
|
||||
Table("issue i").
|
||||
Select(`
|
||||
i.id AS issue_id,
|
||||
i.name AS issue_name,
|
||||
u.id AS user_id,
|
||||
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,
|
||||
to_timestamp(i.created_unix) AS issue_created_at,
|
||||
ROUND(SUM(tt.time) / 3600.0, 2) AS total_hours_spent
|
||||
`).
|
||||
Joins(`JOIN tracked_time tt ON tt.issue_id = i.id`).
|
||||
@@ -321,12 +193,11 @@ func (s *RepoService) GetIssues(
|
||||
i.id,
|
||||
i.name,
|
||||
u.id,
|
||||
u.full_name,
|
||||
created_date
|
||||
u.full_name
|
||||
`).
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package pagination
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -55,9 +52,3 @@ func Paginate[T any](paging Paging, stmt *gorm.DB) (Found[T], error) {
|
||||
Count: uint(count),
|
||||
}, 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)}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package view
|
||||
package responseErrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
12
app/view/google_oauth.go
Normal file
12
app/view/google_oauth.go
Normal 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"`
|
||||
}
|
||||
1
assets/public/dist/assets/Alert-BNRo6CMI.js
vendored
1
assets/public/dist/assets/Alert-BNRo6CMI.js
vendored
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/Alert-CvejfPQL.js
vendored
Normal file
1
assets/public/dist/assets/Alert-CvejfPQL.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/Button-Dys5wjZc.js
vendored
Normal file
1
assets/public/dist/assets/Button-Dys5wjZc.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/Button-jwL-tYHc.js
vendored
1
assets/public/dist/assets/Button-jwL-tYHc.js
vendored
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/Card-DJGrWflS.js
vendored
Normal file
1
assets/public/dist/assets/Card-DJGrWflS.js
vendored
Normal 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};
|
||||
1
assets/public/dist/assets/Card-DPC9xXwj.js
vendored
1
assets/public/dist/assets/Card-DPC9xXwj.js
vendored
@@ -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};
|
||||
@@ -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};
|
||||
1
assets/public/dist/assets/Collection-Dmox1UHc.js
vendored
Normal file
1
assets/public/dist/assets/Collection-Dmox1UHc.js
vendored
Normal 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};
|
||||
@@ -1 +0,0 @@
|
||||
import{t as e}from"./HomeView-CdMOMcn8.js";export{e as default};
|
||||
@@ -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
2
assets/public/dist/assets/LoginView-pygehg15.js
vendored
Normal file
2
assets/public/dist/assets/LoginView-pygehg15.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
1
assets/public/dist/assets/PasswordRecoveryView-CN8F23mF.js
vendored
Normal file
1
assets/public/dist/assets/PasswordRecoveryView-CN8F23mF.js
vendored
Normal 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
2
assets/public/dist/assets/RegisterView-yS4kTlT5.js
vendored
Normal file
2
assets/public/dist/assets/RegisterView-yS4kTlT5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
assets/public/dist/assets/RepoChartView-BFH_TJNr.js
vendored
Normal file
6
assets/public/dist/assets/RepoChartView-BFH_TJNr.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/ResetPasswordForm-BCNXh6U8.js
vendored
Normal file
1
assets/public/dist/assets/ResetPasswordForm-BCNXh6U8.js
vendored
Normal 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};
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
1
assets/public/dist/assets/VerifyEmailView-DVBDwc2x.js
vendored
Normal file
1
assets/public/dist/assets/VerifyEmailView-DVBDwc2x.js
vendored
Normal 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};
|
||||
@@ -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};
|
||||
1
assets/public/dist/assets/_rolldown_dynamic_import_helper-DRJ9pbLn.js
vendored
Normal file
1
assets/public/dist/assets/_rolldown_dynamic_import_helper-DRJ9pbLn.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/auth-CdHmhksw.js
vendored
1
assets/public/dist/assets/auth-CdHmhksw.js
vendored
@@ -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
1
assets/public/dist/assets/auth-O--VICRG.js
vendored
Normal file
1
assets/public/dist/assets/auth-O--VICRG.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import"./useFetchJson-BTB9doG4.js";import{t as e}from"./auth-DHyg2egq.js";export{e as useAuthStore};
|
||||
1
assets/public/dist/assets/cs_PrivacyPolicyView-BGqYh296.js
vendored
Normal file
1
assets/public/dist/assets/cs_PrivacyPolicyView-BGqYh296.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
1
assets/public/dist/assets/cs_TermsAndConditionsView-WbJq946A.js
vendored
Normal file
1
assets/public/dist/assets/cs_TermsAndConditionsView-WbJq946A.js
vendored
Normal 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
1
assets/public/dist/assets/en_PrivacyPolicyView-CfiPJGCI.js
vendored
Normal file
1
assets/public/dist/assets/en_PrivacyPolicyView-CfiPJGCI.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
1
assets/public/dist/assets/en_TermsAndConditionsView-_ZvYbgsB.js
vendored
Normal file
1
assets/public/dist/assets/en_TermsAndConditionsView-_ZvYbgsB.js
vendored
Normal 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};
|
||||
4
assets/public/dist/assets/esm-BmwkJimY.js
vendored
Normal file
4
assets/public/dist/assets/esm-BmwkJimY.js
vendored
Normal file
File diff suppressed because one or more lines are too long
15
assets/public/dist/assets/index-BqfKAJS4.js
vendored
15
assets/public/dist/assets/index-BqfKAJS4.js
vendored
File diff suppressed because one or more lines are too long
2
assets/public/dist/assets/index-DLyy94LM.css
vendored
Normal file
2
assets/public/dist/assets/index-DLyy94LM.css
vendored
Normal file
File diff suppressed because one or more lines are too long
15
assets/public/dist/assets/index-DpkssS-Q.js
vendored
Normal file
15
assets/public/dist/assets/index-DpkssS-Q.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
assets/public/dist/assets/index-UnLOO1Sq.css
vendored
2
assets/public/dist/assets/index-UnLOO1Sq.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/pl_PrivacyPolicyView-D0Y-tZB4.js
vendored
Normal file
1
assets/public/dist/assets/pl_PrivacyPolicyView-D0Y-tZB4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
1
assets/public/dist/assets/pl_TermsAndConditionsView-DPrfyQYl.js
vendored
Normal file
1
assets/public/dist/assets/pl_TermsAndConditionsView-DPrfyQYl.js
vendored
Normal 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};
|
||||
5
assets/public/dist/assets/router-CoYWQDRi.js
vendored
5
assets/public/dist/assets/router-CoYWQDRi.js
vendored
File diff suppressed because one or more lines are too long
2
assets/public/dist/assets/router-DDV1eCGp.js
vendored
Normal file
2
assets/public/dist/assets/router-DDV1eCGp.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/router-Wd6OrXcf.js
vendored
1
assets/public/dist/assets/router-Wd6OrXcf.js
vendored
@@ -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};
|
||||
1
assets/public/dist/assets/router-gBqs4bkw.js
vendored
Normal file
1
assets/public/dist/assets/router-gBqs4bkw.js
vendored
Normal 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};
|
||||
1
assets/public/dist/assets/settings-84EZt-NQ.js
vendored
Normal file
1
assets/public/dist/assets/settings-84EZt-NQ.js
vendored
Normal file
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
@@ -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]);
|
||||
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};
|
||||
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-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};
|
||||
@@ -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};
|
||||
1
assets/public/dist/assets/useForwardExpose-CEpqU5vT.js
vendored
Normal file
1
assets/public/dist/assets/useForwardExpose-CEpqU5vT.js
vendored
Normal 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};
|
||||
3
assets/public/dist/assets/usePortal-BgeZHop8.js
vendored
Normal file
3
assets/public/dist/assets/usePortal-BgeZHop8.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
assets/public/dist/assets/useValidation-pSaoyCcB.js
vendored
Normal file
1
assets/public/dist/assets/useValidation-pSaoyCcB.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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};
|
||||
27
assets/public/dist/index.html
vendored
27
assets/public/dist/index.html
vendored
@@ -16,21 +16,20 @@
|
||||
var pageName = "default";
|
||||
globalThis.appInit = [];
|
||||
</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/useFetchJson-4WJQFaEO.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/Icon-Chkiq2IE.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/Button-jwL-tYHc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/HomeView-CdMOMcn8.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useForwardExpose-BgPOLLFN.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/usePortal-Zddbph8M.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/PopperArrow-CcUKYeE0.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-BcOmX106.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/auth-hZSBdvj-.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/Collection-BkGqWqUl.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/VisuallyHiddenInput-BH1aLUkb.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-CoYWQDRi.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-UnLOO1Sq.css">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useFetchJson-BTB9doG4.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/tv-uB0-NqWK.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/Button-Dys5wjZc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/useForwardExpose-CEpqU5vT.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/usePortal-BgeZHop8.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/esm-BmwkJimY.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/settings-84EZt-NQ.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/auth-DHyg2egq.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/Collection-Dmox1UHc.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/VisuallyHiddenInput-DPrwdEvl.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/router-DDV1eCGp.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DLyy94LM.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
BIN
bin/timetracker
BIN
bin/timetracker
Binary file not shown.
4
bo/components.d.ts
vendored
4
bo/components.d.ts
vendored
@@ -11,18 +11,17 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Button: typeof import('./src/components/custom/Button.vue')['default']
|
||||
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.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_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']
|
||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
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']
|
||||
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']
|
||||
@@ -37,5 +36,6 @@ declare module 'vue' {
|
||||
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']
|
||||
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']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,6 @@ export const uiOptions: NuxtUIOptions = {
|
||||
root: '',
|
||||
}
|
||||
},
|
||||
// selectMenu: {
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// group: 'mt-20!'
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
button: {
|
||||
slots: {
|
||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
@@ -27,115 +19,29 @@ export const uiOptions: NuxtUIOptions = {
|
||||
input: {
|
||||
slots: {
|
||||
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: {
|
||||
slots: {
|
||||
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||
itemLabel: 'text-black! dark:text-white!',
|
||||
itemTrailingIcon: 'text-black! dark:text-white!'
|
||||
},
|
||||
// variants: {
|
||||
// size: {
|
||||
// xxl: {
|
||||
// base: ' h-12 sm:h-[54px] px-[25px]',
|
||||
// item: 'py-2 px-2',
|
||||
// trailingIcon: 'px-6 !text-base',
|
||||
// leading: '!px-[25px]',
|
||||
// itemLabel: 'text-black dark:text-white',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// defaultVariants: {
|
||||
// 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',
|
||||
// },
|
||||
}
|
||||
}
|
||||
},
|
||||
formField: {
|
||||
slots: {
|
||||
error: 'mt-1! text-[14px] text-error text-red-600! dark:text-red-400!',
|
||||
label: 'text-[16px]'
|
||||
},
|
||||
},
|
||||
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!',
|
||||
itemLeadingIcon: 'text-(--black)! dark:text-white!'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,6 @@ body {
|
||||
--gray: #6B6B6B;
|
||||
--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 */
|
||||
--border-light: #E8E7E0;
|
||||
--border-dark: #3F3E3D;
|
||||
@@ -44,16 +36,9 @@ body {
|
||||
--placeholder: #8C8C8A;
|
||||
|
||||
--ui-bg: var(--main-light);
|
||||
--ui-primary: var(--color-gray-300);
|
||||
--ui-secondary: var(--accent-green);
|
||||
--ui-border-accented: var(--border-light);
|
||||
--ui-text-dimmed: var(--gray);
|
||||
--ui-bg-elevated: var(--color-gray-300);
|
||||
--ui-border: var(--border-light);
|
||||
--ui-color-neutral-700: var(--black);
|
||||
--ui-error: var(--accent-red);
|
||||
--border: var(--border-light);
|
||||
--tw-border-style: var(--border-light);
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
36
bo/src/components/TopBar.vue
Normal file
36
bo/src/components/TopBar.vue
Normal 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>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import HomeView from '@/views/HomeView.vue';
|
||||
import LangSwitch from './inner/langSwitch.vue'
|
||||
import ThemeSwitch from './inner/themeSwitch.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -20,17 +19,11 @@ const authStore = useAuthStore()
|
||||
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span>
|
||||
</RouterLink>
|
||||
<!-- Right Side Actions -->
|
||||
<HomeView />
|
||||
<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-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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<USelectMenu v-model="locale" :items="langs"
|
||||
class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||
<USelectMenu v-model="locale" :items="langs" class="w-40 bg-white dark:bg-(--black) rounded-md shadow-sm hover:none!"
|
||||
valueKey="iso_code" :searchInput="false">
|
||||
<template #default="{ modelValue }">
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -22,7 +21,7 @@ import { langs, currentLang } from '@/router/langs'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useCookie } from '@/composable/useCookie'
|
||||
import { computed, watch } from 'vue'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -36,28 +35,23 @@ const locale = computed({
|
||||
i18n.locale.value = value
|
||||
currentLang.value = langs.find((x) => x.iso_code == value)
|
||||
|
||||
// Update URL to reflect language change
|
||||
const currentPath = route.path
|
||||
const pathParts = currentPath.split('/').filter(Boolean)
|
||||
|
||||
cookie.setCookie('lang_id', `${langs.find((x) => x.iso_code == value)?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
|
||||
|
||||
if (pathParts.length > 0) {
|
||||
// Check if first part is a locale
|
||||
const isLocale = langs.some((l) => l.lang_code === pathParts[0])
|
||||
if (isLocale) {
|
||||
// Replace existing locale
|
||||
pathParts[0] = value
|
||||
router.replace({ path: '/' + pathParts.join('/'), query: route.query })
|
||||
} else {
|
||||
// Add locale to path
|
||||
router.replace({ path: '/' + value + currentPath, query: route.query })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Sync i18n locale with router locale on initial load
|
||||
watch(
|
||||
() => route.params.locale,
|
||||
(newLocale) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 {
|
||||
quarter: string
|
||||
@@ -35,7 +34,6 @@ export async function getRepos(): Promise<any> {
|
||||
// export async function getYears(repoID: number): Promise<any> {
|
||||
// return useFetchJson<number[]>(`${API_PREFIX}/get-years?repoID=${repoID}`)
|
||||
// }
|
||||
// console.log(getYears(), 'leraaaaaa')
|
||||
|
||||
export async function getYears(repoID: number): Promise<any> {
|
||||
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() {
|
||||
// const years = await getIssues(7); // pass a repoID
|
||||
// console.log(years, 'leraaaaaa');
|
||||
// }
|
||||
export async function getIssues(
|
||||
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}`
|
||||
);
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type { FormError } from '@nuxt/ui'
|
||||
import { settings } from '@/router/settings'
|
||||
import { i18n } from '@/plugins/i18n'
|
||||
import { i18n } from '@/plugins/02_i18n'
|
||||
|
||||
export const useValidation = () => {
|
||||
const errors = [] as FormError[]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import TopBarLogin from '@/components/TopBarLogin.vue'
|
||||
import TopBar from '@/components/TopBar.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<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> -->
|
||||
<UContainer>
|
||||
<main class="p-10">
|
||||
<TopBar/>
|
||||
<router-view />
|
||||
</main>
|
||||
</UContainer>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user