diff --git a/backend/app/Api/User/Ticket/Create.php b/backend/app/Api/User/Ticket/Create.php index ffd5929..fa5ba91 100755 --- a/backend/app/Api/User/Ticket/Create.php +++ b/backend/app/Api/User/Ticket/Create.php @@ -48,12 +48,14 @@ if (isset($_POST['department_id']) && $_POST['department_id'] != '') { $departmentId = $_POST['department_id']; if (Departments::exists((int) $departmentId)) { + /** + * Make that every info needed is provided. + */ if (isset($_POST['service_id']) && $_POST['service_id'] != '') { $serviceId = $_POST['service_id']; } else { $serviceId = null; } - if (isset($_POST['subject']) && $_POST['subject'] != '') { $subject = $_POST['subject']; } else { @@ -73,8 +75,19 @@ } // TODO: Check if service exists - // TODO: Limit to 3 open tickets user - + /** + * Check if the user has more than 3 open tickets. + */ + $userTickets = Tickets::getAllTicketsByUser($session->getInfo(UserColumns::UUID, false), 150); + $openTickets = array_filter($userTickets, function ($ticket) { + return in_array($ticket['status'], ['open', 'waiting', 'replied', 'inprogress']); + }); + if (count($openTickets) >= 3) { + $appInstance->BadRequest('You have reached the limit of 3 open tickets!', ['error_code' => 'LIMIT_REACHED']); + } + /** + * Create the ticket. + */ $ticketId = Tickets::create($session->getInfo(UserColumns::UUID, false), $departmentId, $serviceId, $subject, $message, $priority); if ($ticketId == 0) { $appInstance->BadRequest('Failed to create ticket!', ['error_code' => 'FAILED_TO_CREATE_TICKET']); diff --git a/backend/app/Api/User/Ticket/List.php b/backend/app/Api/User/Ticket/List.php new file mode 100755 index 0000000..ac3c09a --- /dev/null +++ b/backend/app/Api/User/Ticket/List.php @@ -0,0 +1,31 @@ +get('/api/user/ticket/list', function () { + App::init(); + $appInstance = App::getInstance(true); + $appInstance->allowOnlyGET(); + $s = new Session($appInstance); + + $tickets = Tickets::getAllTicketsByUser($s->getInfo(UserColumns::UUID, false), 150); + + $appInstance->OK('Tickets', [ + 'tickets' => $tickets, + ]); + +}); diff --git a/backend/app/Chat/Departments.php b/backend/app/Chat/Departments.php index 80e5f58..f65ab72 100755 --- a/backend/app/Chat/Departments.php +++ b/backend/app/Chat/Departments.php @@ -47,7 +47,7 @@ public static function update( string $close, ): void { try { - if (self::exists($id)) { + if (!self::exists($id)) { App::getInstance(true)->getLogger()->warning('Department does not exist but tried to update it.', true); return; @@ -72,7 +72,7 @@ public static function exists(int $id): bool $con = self::getPdoConnection(); $sql = 'SELECT id FROM ' . self::TABLE_NAME . ' WHERE id = :id'; $stmt = $con->prepare($sql); - $stmt->bindParam(':id', $id); + $stmt->bindParam(':id', $id, \PDO::PARAM_INT); $stmt->execute(); return $stmt->rowCount() > 0; @@ -86,8 +86,8 @@ public static function exists(int $id): bool public static function delete(int $id): bool { try { - if (self::exists($id)) { - App::getInstance(true)->getLogger()->warning('Department does not exist but tried to update it.', true); + if (!self::exists($id)) { + App::getInstance(true)->getLogger()->warning('Department does not exist but tried to delete it.', true); return false; } @@ -124,8 +124,8 @@ public static function getAll(): array public static function get(int $id): array { try { - if (self::exists($id)) { - App::getInstance(true)->getLogger()->warning('Department does not exist but tried to update it.', true); + if (!self::exists($id)) { + App::getInstance(true)->getLogger()->warning('Department does not exist but tried to get it: ' . $id . '.', true); return []; } diff --git a/backend/app/Chat/Tickets.php b/backend/app/Chat/Tickets.php index 00843c1..b985fdb 100755 --- a/backend/app/Chat/Tickets.php +++ b/backend/app/Chat/Tickets.php @@ -96,6 +96,44 @@ public static function getAllTickets(int $limit = 150): array } } + public static function getAllTicketsByUser(string $uuid, int $limit = 150): array + { + try { + $con = self::getPdoConnection(); + $sql = 'SELECT * FROM ' . self::TABLE_NAME . ' WHERE user = :uuid ORDER BY id DESC LIMIT ' . $limit; + $stmt = $con->prepare($sql); + $stmt->bindParam('uuid', $uuid, \PDO::PARAM_STR); + $stmt->execute(); + + $tickets = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + foreach ($tickets as $key => $ticket) { + $tickets[$key]['department_id'] = $ticket['department']; + $tickets[$key]['department'] = Departments::get((int) $ticket['department']); + + if (empty($tickets[$key]['department'])) { + $tickets[$key]['department'] = [ + 'id' => 0, + 'name' => 'Deleted Department', + 'description' => 'This department has been deleted.', + 'time_open' => '08:30', + 'time_close' => '17:30', + 'enabled' => 'true', + 'deleted' => 'false', + 'locked' => 'false', + 'date' => '2024-12-25 22:25:09', + ]; + } + } + + return $tickets; + } catch (\Exception $e) { + self::db_Error('Failed to get all tickets by user: ' . $e->getMessage()); + + return []; + } + } + public static function getTicket(int $id): array { try { diff --git a/backend/storage/migrations/2025-01-21-12.50-add-status-tickets.sql b/backend/storage/migrations/2025-01-21-12.50-add-status-tickets.sql new file mode 100755 index 0000000..d6077b4 --- /dev/null +++ b/backend/storage/migrations/2025-01-21-12.50-add-status-tickets.sql @@ -0,0 +1 @@ +ALTER TABLE `mythicalclient_tickets` ADD `status` ENUM('open','closed','waiting','replied','inprogress') NOT NULL DEFAULT 'open' AFTER `priority`; \ No newline at end of file diff --git a/frontend/src/components/client/Dashboard/Main/Announcements.vue b/frontend/src/components/client/Dashboard/Main/Announcements.vue index 0c23479..63d308f 100755 --- a/frontend/src/components/client/Dashboard/Main/Announcements.vue +++ b/frontend/src/components/client/Dashboard/Main/Announcements.vue @@ -1,343 +1,392 @@ diff --git a/frontend/src/components/client/Dashboard/Main/BillingInfo.vue b/frontend/src/components/client/Dashboard/Main/BillingInfo.vue index 4a784a2..7216495 100755 --- a/frontend/src/components/client/Dashboard/Main/BillingInfo.vue +++ b/frontend/src/components/client/Dashboard/Main/BillingInfo.vue @@ -1,27 +1,28 @@ diff --git a/frontend/src/components/client/Dashboard/Main/Header.vue b/frontend/src/components/client/Dashboard/Main/Header.vue index a60d289..b204b11 100755 --- a/frontend/src/components/client/Dashboard/Main/Header.vue +++ b/frontend/src/components/client/Dashboard/Main/Header.vue @@ -1,25 +1,26 @@ diff --git a/frontend/src/components/client/Dashboard/Main/ProductList.vue b/frontend/src/components/client/Dashboard/Main/ProductList.vue index 30b59ba..fc4f024 100755 --- a/frontend/src/components/client/Dashboard/Main/ProductList.vue +++ b/frontend/src/components/client/Dashboard/Main/ProductList.vue @@ -1,31 +1,34 @@ diff --git a/frontend/src/components/client/Dashboard/Main/Stats.vue b/frontend/src/components/client/Dashboard/Main/Stats.vue index 41f140b..ad9a92b 100755 --- a/frontend/src/components/client/Dashboard/Main/Stats.vue +++ b/frontend/src/components/client/Dashboard/Main/Stats.vue @@ -1,22 +1,18 @@ diff --git a/frontend/src/components/client/Dashboard/Main/SupportPin.vue b/frontend/src/components/client/Dashboard/Main/SupportPin.vue index e8ddf05..9598b94 100755 --- a/frontend/src/components/client/Dashboard/Main/SupportPin.vue +++ b/frontend/src/components/client/Dashboard/Main/SupportPin.vue @@ -1,18 +1,17 @@ diff --git a/frontend/src/components/client/Dashboard/Main/TicketList.vue b/frontend/src/components/client/Dashboard/Main/TicketList.vue index 696ee48..df28aee 100755 --- a/frontend/src/components/client/Dashboard/Main/TicketList.vue +++ b/frontend/src/components/client/Dashboard/Main/TicketList.vue @@ -1,30 +1,36 @@ diff --git a/frontend/src/components/client/Dashboard/Main/UserInfo.vue b/frontend/src/components/client/Dashboard/Main/UserInfo.vue index ea3be51..11ae2c5 100755 --- a/frontend/src/components/client/Dashboard/Main/UserInfo.vue +++ b/frontend/src/components/client/Dashboard/Main/UserInfo.vue @@ -1,37 +1,39 @@ diff --git a/frontend/src/components/client/ui/TextForms/SelectInput.vue b/frontend/src/components/client/ui/TextForms/SelectInput.vue index 3aa4200..0115c6d 100755 --- a/frontend/src/components/client/ui/TextForms/SelectInput.vue +++ b/frontend/src/components/client/ui/TextForms/SelectInput.vue @@ -3,23 +3,23 @@ import { ref, onMounted, computed, onBeforeUnmount, watch, nextTick } from 'vue' import TextInput from './TextInput.vue'; const props = defineProps({ - modelValue: String, - options: { - type: Array as () => Array<{ value: string; label: string }>, - default: () => [], - }, - inputClass: { - type: String, - default: - 'w-full bg-gray-800/50 border border-gray-700/50 rounded-lg pl-4 pr-10 py-2 text-sm text-gray-100 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50 focus:outline-none', - }, + modelValue: String, + options: { + type: Array as () => Array<{ value: string; label: string }>, + default: () => [], + }, + inputClass: { + type: String, + default: + 'w-full bg-gray-800/50 border border-gray-700/50 rounded-lg pl-4 pr-10 py-2 text-sm text-gray-100 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50 focus:outline-none', + }, }); const emit = defineEmits(['update:modelValue']); const inputValue = computed({ - get: () => props.modelValue, - set: (value) => emit('update:modelValue', value), + get: () => props.modelValue, + set: (value) => emit('update:modelValue', value), }); const searchQuery = ref(''); @@ -29,112 +29,117 @@ const optionsRef = ref(null); const selectedIndex = ref(-1); const filteredOptions = computed(() => { - return props.options.filter(option => - option.label.toLowerCase().includes(searchQuery.value.toLowerCase()) - ); + return props.options.filter((option) => option.label.toLowerCase().includes(searchQuery.value.toLowerCase())); }); onMounted(() => { - if (!props.modelValue && props.options.length > 0) { - emit('update:modelValue', props.options[0].value); - } + if (!props.modelValue && props.options.length > 0) { + emit('update:modelValue', props.options[0].value); + } - document.addEventListener('click', handleClickOutside); + document.addEventListener('click', handleClickOutside); }); onBeforeUnmount(() => { - document.removeEventListener('click', handleClickOutside); + document.removeEventListener('click', handleClickOutside); }); const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.closest('.dropdown-container')) { - isDropdownOpen.value = false; - } + const target = event.target as HTMLElement; + if (!target.closest('.dropdown-container')) { + isDropdownOpen.value = false; + } }; const selectOption = (value: string) => { - inputValue.value = value; - isDropdownOpen.value = false; + inputValue.value = value; + isDropdownOpen.value = false; }; const handleKeydown = (event: KeyboardEvent) => { - if (event.key === 'ArrowDown') { - event.preventDefault(); - selectedIndex.value = Math.min(selectedIndex.value + 1, filteredOptions.value.length - 1); - scrollToSelectedOption(); - } else if (event.key === 'ArrowUp') { - event.preventDefault(); - selectedIndex.value = Math.max(selectedIndex.value - 1, -1); - scrollToSelectedOption(); - } else if (event.key === 'Enter') { - event.preventDefault(); - if (selectedIndex.value >= 0 && selectedIndex.value < filteredOptions.value.length) { - selectOption(filteredOptions.value[selectedIndex.value].value); + if (event.key === 'ArrowDown') { + event.preventDefault(); + selectedIndex.value = Math.min(selectedIndex.value + 1, filteredOptions.value.length - 1); + scrollToSelectedOption(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + selectedIndex.value = Math.max(selectedIndex.value - 1, -1); + scrollToSelectedOption(); + } else if (event.key === 'Enter') { + event.preventDefault(); + if (selectedIndex.value >= 0 && selectedIndex.value < filteredOptions.value.length) { + selectOption(filteredOptions.value[selectedIndex.value].value); + } } - } }; const scrollToSelectedOption = () => { - nextTick(() => { - const selectedElement = optionsRef.value?.children[selectedIndex.value] as HTMLElement; - if (selectedElement) { - selectedElement.scrollIntoView({ block: 'nearest' }); - } - }); + nextTick(() => { + const selectedElement = optionsRef.value?.children[selectedIndex.value] as HTMLElement; + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest' }); + } + }); }; watch(isDropdownOpen, async (newValue) => { - if (newValue) { - await nextTick(); - searchInputRef.value?.focus(); - selectedIndex.value = -1; - } else { - searchQuery.value = ''; - } + if (newValue) { + await nextTick(); + searchInputRef.value?.focus(); + selectedIndex.value = -1; + } else { + searchQuery.value = ''; + } }); watch(filteredOptions, () => { - selectedIndex.value = -1; + selectedIndex.value = -1; }); diff --git a/frontend/src/components/client/ui/TextForms/TextArea.vue b/frontend/src/components/client/ui/TextForms/TextArea.vue index ba19f08..9cceeb4 100755 --- a/frontend/src/components/client/ui/TextForms/TextArea.vue +++ b/frontend/src/components/client/ui/TextForms/TextArea.vue @@ -1,27 +1,27 @@ diff --git a/frontend/src/locale/en.yml b/frontend/src/locale/en.yml index c2a902d..d9262bc 100755 --- a/frontend/src/locale/en.yml +++ b/frontend/src/locale/en.yml @@ -1,5 +1,4 @@ --- - #-------------------------------------------------------------# # # Global Components @@ -8,34 +7,35 @@ # #-------------------------------------------------------------# Components: - ErrorsPage: - Buttons: - GoBack: "Go Back" - GoHome: "Go Home" - Global: - Navigation: - Loading: "Loading..." - Copy: - Title: "Copied" - Success: "Copied to clipboard just like a boss." - Footer: "Yay it worked ;)" - Error: "An error occurred. Please try again later." - Announcements: - Title: "Announcements" - Card: - ReadMore: "Read More" - SupportPin: - title: "Support PIN" - copy: "Copy" - alerts: - success: - title: "Success" - pin_success: "Support PIN generated" - footer: "Your support PIN has been generated" - error: - title: "Error" - generic: "An error occurred. Please try again later" - footer: "Please contact support for assistance" + ErrorsPage: + Buttons: + GoBack: 'Go Back' + GoHome: 'Go Home' + Global: + Navigation: + Loading: 'Loading...' + Copy: + Title: 'Copied' + Success: 'Copied to clipboard just like a boss.' + Footer: 'Yay it worked ;)' + Error: 'An error occurred. Please try again later.' + Announcements: + Title: 'Announcements' + Card: + ReadMore: 'Read More' + SupportPin: + title: 'Support PIN' + copy: 'Copy' + alerts: + success: + title: 'Success' + pin_success: 'Support PIN generated' + footer: 'Your support PIN has been generated' + error: + title: 'Error' + generic: 'An error occurred. Please try again later' + footer: 'Please contact support for assistance' + #-------------------------------------------------------------# # # Auth Pages (/auth/*) @@ -43,9 +43,8 @@ Components: # @public # #-------------------------------------------------------------# -dashboard: - title: Dashboard - +dashboard: + title: Dashboard auth: logic: @@ -282,6 +281,57 @@ errors: #-------------------------------------------------------------# account: pages: + tickets: + title: Tickets + actions: + newTicket: 'Tickets' + view: 'View' + alerts: + error: + generic: 'An error occurred. Please try again later' + table: + title: 'Tickets' + subject: 'Subject' + status: 'Status' + priority: 'Priority' + department: 'Department' + created: 'Created' + actions: 'Actions' + noTickets: 'No tickets found' + create_ticket: + title: 'New Ticket' + subTitle: 'Create a new ticket' + form: + service: 'Related Service (if any)' + subject: 'Subject' + department: 'Department' + priority: 'Priority' + message: 'Message' + submit: 'Submit' + loading: 'Submitting...' + reset: 'Reset' + back: 'Back' + types: + priority: + low: 'Low' + medium: 'Medium' + high: 'High' + urgent: 'Urgent' + alerts: + success: + title: 'Success' + ticket_success: 'Ticket created' + footer: 'Your ticket has been created' + error: + title: 'Error' + generic: 'An error occurred. Please try again later' + footer: 'Please contact support for assistance' + department_not_found: 'Department not found' + department_id_missing: 'Department ID is missing' + message_missing: 'Message is missing' + subject_missing: 'Subject is missing' + limit_reached: 'You have reached the limit of tickets you can create' + failed_to_create_ticket: 'Cluster is down, try again later' settings: alerts: success: diff --git a/frontend/src/mythicalclient/Auth.ts b/frontend/src/mythicalclient/Auth.ts index b20390f..640d628 100755 --- a/frontend/src/mythicalclient/Auth.ts +++ b/frontend/src/mythicalclient/Auth.ts @@ -23,243 +23,243 @@ * * ---------------------------*/ class Auth { - /** - * Logs the user in - * - * @param email The email to log in with - * @param turnstileResponse The turnstile response - * - * @returns The response from the server - */ - static async forgotPassword(email: string, turnstileResponse: string) { - const response = await fetch('/api/user/auth/forgot', { - method: 'POST', - body: new URLSearchParams({ - email: email, - turnstileResponse: turnstileResponse, - }), - }); - const data = await response.json(); - return data; - } + /** + * Logs the user in + * + * @param email The email to log in with + * @param turnstileResponse The turnstile response + * + * @returns The response from the server + */ + static async forgotPassword(email: string, turnstileResponse: string) { + const response = await fetch('/api/user/auth/forgot', { + method: 'POST', + body: new URLSearchParams({ + email: email, + turnstileResponse: turnstileResponse, + }), + }); + const data = await response.json(); + return data; + } - /** - * Resets the password - * - * @param confirmPassword The password to confirm - * @param password The new password - * @param resetCode The reset code - * @param turnstileResponse The turnstile response - * - * @returns The response from the server - */ - static async resetPassword( - confirmPassword: string, - password: string, - resetCode: string, - turnstileResponse: string, - ) { - const response = await fetch('/api/user/auth/reset', { - method: 'POST', - body: new URLSearchParams({ - password: password, - confirmPassword: confirmPassword, - email_code: resetCode || '', - turnstileResponse: turnstileResponse, - }), - }); - const data = await response.json(); - return data; - } + /** + * Resets the password + * + * @param confirmPassword The password to confirm + * @param password The new password + * @param resetCode The reset code + * @param turnstileResponse The turnstile response + * + * @returns The response from the server + */ + static async resetPassword( + confirmPassword: string, + password: string, + resetCode: string, + turnstileResponse: string, + ) { + const response = await fetch('/api/user/auth/reset', { + method: 'POST', + body: new URLSearchParams({ + password: password, + confirmPassword: confirmPassword, + email_code: resetCode || '', + turnstileResponse: turnstileResponse, + }), + }); + const data = await response.json(); + return data; + } - /** - * Verifies the login token - * - * @param code The code to verify - * - * @returns The response from the server - */ - static async isLoginVerifyTokenValid(code: string) { - const response = await fetch(`/api/user/auth/reset?code=${code}`, { - method: 'GET', - }); - const data = await response.json(); - return data; - } + /** + * Verifies the login token + * + * @param code The code to verify + * + * @returns The response from the server + */ + static async isLoginVerifyTokenValid(code: string) { + const response = await fetch(`/api/user/auth/reset?code=${code}`, { + method: 'GET', + }); + const data = await response.json(); + return data; + } - /** - * Registers the user - * - * @param firstName The first name - * @param lastName The last name - * @param email The email - * @param username The username - * @param password The password - * @param turnstileResponse The turnstile response - * - * @returns The response from the server - */ - static async register( - firstName: string, - lastName: string, - email: string, - username: string, - password: string, - turnstileResponse: string, - ) { - const response = await fetch('/api/user/auth/register', { - method: 'POST', - body: new URLSearchParams({ - firstName: firstName, - lastName: lastName, - email: email, - username: username, - password: password, - turnstileResponse: turnstileResponse, - }), - }); - const data = await response.json(); - return data; - } - /** - * Logs the user in - * - * @param login The users email or username - * @param password The users password - * @param turnstileResponse The turnstile response - * - * @returns - */ - static async login(login: string, password: string, turnstileResponse: string) { - const response = await fetch('/api/user/auth/login', { - method: 'POST', - body: new URLSearchParams({ - login: login, - password: password, - turnstileResponse: turnstileResponse, - }), - }); - const data = await response.json(); - return data; - } + /** + * Registers the user + * + * @param firstName The first name + * @param lastName The last name + * @param email The email + * @param username The username + * @param password The password + * @param turnstileResponse The turnstile response + * + * @returns The response from the server + */ + static async register( + firstName: string, + lastName: string, + email: string, + username: string, + password: string, + turnstileResponse: string, + ) { + const response = await fetch('/api/user/auth/register', { + method: 'POST', + body: new URLSearchParams({ + firstName: firstName, + lastName: lastName, + email: email, + username: username, + password: password, + turnstileResponse: turnstileResponse, + }), + }); + const data = await response.json(); + return data; + } + /** + * Logs the user in + * + * @param login The users email or username + * @param password The users password + * @param turnstileResponse The turnstile response + * + * @returns + */ + static async login(login: string, password: string, turnstileResponse: string) { + const response = await fetch('/api/user/auth/login', { + method: 'POST', + body: new URLSearchParams({ + login: login, + password: password, + turnstileResponse: turnstileResponse, + }), + }); + const data = await response.json(); + return data; + } - /** - * Update the users billing information - * - * @param company_name The company name - * @param vat_number The vat number - * @param address1 The address line 1 - * @param address2 The address line 2 - * @param city The city - * @param country The country - * @param state The state - * @param postcode The postcode - * - * @returns - */ - static async updateBilling( - company_name: string, - vat_number: string, - address1: string, - address2: string, - city: string, - country: string, - state: string, - postcode: string, - ) { - const response = await fetch('/api/user/session/billing/update', { - method: 'POST', - body: new URLSearchParams({ - company_name: company_name, - vat_number: vat_number, - address1: address1, - address2: address2, - city: city, - country: country, - state: state, - postcode: postcode, - }), - }); - const data = await response.json(); - return data; - } + /** + * Update the users billing information + * + * @param company_name The company name + * @param vat_number The vat number + * @param address1 The address line 1 + * @param address2 The address line 2 + * @param city The city + * @param country The country + * @param state The state + * @param postcode The postcode + * + * @returns + */ + static async updateBilling( + company_name: string, + vat_number: string, + address1: string, + address2: string, + city: string, + country: string, + state: string, + postcode: string, + ) { + const response = await fetch('/api/user/session/billing/update', { + method: 'POST', + body: new URLSearchParams({ + company_name: company_name, + vat_number: vat_number, + address1: address1, + address2: address2, + city: city, + country: country, + state: state, + postcode: postcode, + }), + }); + const data = await response.json(); + return data; + } - /** - * Update the users info - * - * @param first_name The first name - * @param last_name The last name - * @param email The email - * @param avatar The avatar - * @param background The background - * - * @returns - */ - static async updateUserInfo( - first_name: string, - last_name: string, - email: string, - avatar: string, - background: string, - ) { - const response = await fetch('/api/user/session/info/update', { - method: 'POST', - body: new URLSearchParams({ - first_name: first_name, - last_name: last_name, - email: email, - avatar: avatar, - background: background, - }), - }); - const data = await response.json(); - return data; - } - /** - * Setup 2fa - * - * @returns - */ - static async getTwoFactorSecret() { - const response = await fetch('/api/user/auth/2fa/setup', { - method: 'GET', - }); - const data = await response.json(); - return data; - } + /** + * Update the users info + * + * @param first_name The first name + * @param last_name The last name + * @param email The email + * @param avatar The avatar + * @param background The background + * + * @returns + */ + static async updateUserInfo( + first_name: string, + last_name: string, + email: string, + avatar: string, + background: string, + ) { + const response = await fetch('/api/user/session/info/update', { + method: 'POST', + body: new URLSearchParams({ + first_name: first_name, + last_name: last_name, + email: email, + avatar: avatar, + background: background, + }), + }); + const data = await response.json(); + return data; + } + /** + * Setup 2fa + * + * @returns + */ + static async getTwoFactorSecret() { + const response = await fetch('/api/user/auth/2fa/setup', { + method: 'GET', + }); + const data = await response.json(); + return data; + } - /** - * Verify 2fa - * - * @param code The code - * - * @returns - */ - static async verifyTwoFactor(code: string, turnstileResponse: string) { - const response = await fetch('/api/user/auth/2fa/setup', { - method: 'POST', - body: new URLSearchParams({ - code: code, - turnstileResponse: turnstileResponse, - }), - }); - const data = await response.json(); - return data; - } + /** + * Verify 2fa + * + * @param code The code + * + * @returns + */ + static async verifyTwoFactor(code: string, turnstileResponse: string) { + const response = await fetch('/api/user/auth/2fa/setup', { + method: 'POST', + body: new URLSearchParams({ + code: code, + turnstileResponse: turnstileResponse, + }), + }); + const data = await response.json(); + return data; + } - /** - * - * Reset the support pin! - * - * @returns The new pin - */ - static async resetPin(): Promise { - const response = await fetch('/api/user/session/newPin', { - method: 'POST', - }); - const data = await response.json(); - return parseInt(data.pin, 10); - } + /** + * + * Reset the support pin! + * + * @returns The new pin + */ + static async resetPin(): Promise { + const response = await fetch('/api/user/session/newPin', { + method: 'POST', + }); + const data = await response.json(); + return parseInt(data.pin, 10); + } } export default Auth; diff --git a/frontend/src/mythicalclient/Tickets.ts b/frontend/src/mythicalclient/Tickets.ts index d3641cb..321d9fc 100755 --- a/frontend/src/mythicalclient/Tickets.ts +++ b/frontend/src/mythicalclient/Tickets.ts @@ -1,31 +1,38 @@ class Tickets { - public static async getTicketCreateInfo() { - const response = await fetch('/api/user/ticket/create', { - method: 'GET', - }); - const data = await response.json(); - return data; - } - static async createTicket( - department_id: number, - subject: string, - message: string, - priority: string, - service: number, - ) { - const response = await fetch('/api/user/ticket/create', { - method: 'POST', - body: new URLSearchParams({ - department_id: department_id.toString(), - subject: subject, - message: message, - priority: priority, - service: service.toString() || '', - }), - }); - const data = await response.json(); - return data; - } + public static async getTicketCreateInfo() { + const response = await fetch('/api/user/ticket/create', { + method: 'GET', + }); + const data = await response.json(); + return data; + } + static async createTicket( + department_id: number, + subject: string, + message: string, + priority: string, + service: number, + ) { + const response = await fetch('/api/user/ticket/create', { + method: 'POST', + body: new URLSearchParams({ + department_id: department_id.toString(), + subject: subject, + message: message, + priority: priority, + service: service.toString() || '', + }), + }); + const data = await response.json(); + return data; + } + static async getTickets() { + const response = await fetch('/api/user/ticket/list', { + method: 'GET', + }); + const data = await response.json(); + return data; + } } export default Tickets; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4c7821c..0da1fb9 100755 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,110 +1,110 @@ import { createRouter, createWebHistory } from 'vue-router'; const routes = [ - { - path: '/auth/login', - name: 'Login', - component: () => import('@/views/client/auth/Login.vue'), - }, - { - path: '/auth/register', - name: 'Register', - component: () => import('@/views/client/auth/Register.vue'), - }, - { - path: '/auth/forgot-password', - name: 'Forgot Password', - component: () => import('@/views/client/auth/ForgotPassword.vue'), - }, - { - path: '/auth/reset-password', - name: 'Reset Password', - component: () => import('@/views/client/auth/ResetPassword.vue'), - }, - { - path: '/auth/2fa/setup', - name: 'Two Factor Setup', - component: () => import('@/views/client/auth/TwoFactorSetup.vue'), - }, - { - path: '/errors/403', - name: 'Forbidden', - component: () => import('@/views/client/errors/Forbidden.vue'), - }, - { - path: '/errors/500', - name: 'ServerError', - component: () => import('@/views/client/errors/ServerError.vue'), - }, - { - path: '/dashboard', - name: 'Dashboard', - component: () => import('@/views/client/Home.vue'), - }, - { - path: '/account', - name: 'Account', - component: () => import('@/views/client/Account.vue'), - }, - { - path: '/ticket', - name: 'Ticket', - component: () => import('@/views/client/ticket/List.vue'), - }, - { - path: '/ticket/create', - name: 'Create Ticket', - component: () => import('@/views/client/ticket/Create.vue'), - }, - { - path: '/ticket/:id', - name: 'Ticket Detail', - component: () => import('@/views/client/ticket/[id].vue'), - }, - { - path: '/auth/sso', - name: 'SSO', - component: () => import('@/views/client/auth/sso.vue'), - }, - { - path: '/auth/2fa/setup/disband', - redirect: () => { - window.location.href = '/api/auth/2fa/setup/kill'; - return '/api/auth/2fa/setup/kill'; - }, - }, - { - path: '/auth/logout', - redirect: () => { - window.location.href = '/api/user/auth/logout'; - return '/api/user/auth/logout'; - }, - }, - { - path: '/auth/2fa/verify', - name: 'Two Factor Verify', - component: () => import('@/views/client/auth/TwoFactorVerify.vue'), - }, - { - path: '/', - redirect: '/dashboard', - }, - { - path: '/mc-admin', - name: 'Admin Home', - component: () => import('@/views/admin/Home.vue'), - }, + { + path: '/auth/login', + name: 'Login', + component: () => import('@/views/client/auth/Login.vue'), + }, + { + path: '/auth/register', + name: 'Register', + component: () => import('@/views/client/auth/Register.vue'), + }, + { + path: '/auth/forgot-password', + name: 'Forgot Password', + component: () => import('@/views/client/auth/ForgotPassword.vue'), + }, + { + path: '/auth/reset-password', + name: 'Reset Password', + component: () => import('@/views/client/auth/ResetPassword.vue'), + }, + { + path: '/auth/2fa/setup', + name: 'Two Factor Setup', + component: () => import('@/views/client/auth/TwoFactorSetup.vue'), + }, + { + path: '/errors/403', + name: 'Forbidden', + component: () => import('@/views/client/errors/Forbidden.vue'), + }, + { + path: '/errors/500', + name: 'ServerError', + component: () => import('@/views/client/errors/ServerError.vue'), + }, + { + path: '/dashboard', + name: 'Dashboard', + component: () => import('@/views/client/Home.vue'), + }, + { + path: '/account', + name: 'Account', + component: () => import('@/views/client/Account.vue'), + }, + { + path: '/ticket', + name: 'Ticket', + component: () => import('@/views/client/ticket/List.vue'), + }, + { + path: '/ticket/create', + name: 'Create Ticket', + component: () => import('@/views/client/ticket/Create.vue'), + }, + { + path: '/ticket/:id', + name: 'Ticket Detail', + component: () => import('@/views/client/ticket/[id].vue'), + }, + { + path: '/auth/sso', + name: 'SSO', + component: () => import('@/views/client/auth/sso.vue'), + }, + { + path: '/auth/2fa/setup/disband', + redirect: () => { + window.location.href = '/api/auth/2fa/setup/kill'; + return '/api/auth/2fa/setup/kill'; + }, + }, + { + path: '/auth/logout', + redirect: () => { + window.location.href = '/api/user/auth/logout'; + return '/api/user/auth/logout'; + }, + }, + { + path: '/auth/2fa/verify', + name: 'Two Factor Verify', + component: () => import('@/views/client/auth/TwoFactorVerify.vue'), + }, + { + path: '/', + redirect: '/dashboard', + }, + { + path: '/mc-admin', + name: 'Admin Home', + component: () => import('@/views/admin/Home.vue'), + }, ]; routes.push({ - path: '/:pathMatch(.*)*', - name: 'NotFound', - component: () => import('@/views/client/errors/NotFound.vue'), + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('@/views/client/errors/NotFound.vue'), }); const router = createRouter({ - history: createWebHistory(), - routes, + history: createWebHistory(), + routes, }); export default router; diff --git a/frontend/src/views/client/Home.vue b/frontend/src/views/client/Home.vue index 235ad87..1e970d1 100755 --- a/frontend/src/views/client/Home.vue +++ b/frontend/src/views/client/Home.vue @@ -1,33 +1,31 @@ diff --git a/frontend/src/views/client/ticket/Create.vue b/frontend/src/views/client/ticket/Create.vue index f261d1a..c86771c 100755 --- a/frontend/src/views/client/ticket/Create.vue +++ b/frontend/src/views/client/ticket/Create.vue @@ -18,202 +18,232 @@ const { play: playError } = useSound(failedAlertSfx); const { play: playSuccess } = useSound(successAlertSfx); const router = useRouter(); +document.title = t('account.pages.create_ticket.title'); + interface Department { - id: number; - name: string; - description: string; - time_open: string; - time_close: string; - enabled: string; - deleted: string; - locked: string; - date: string; + id: number; + name: string; + description: string; + time_open: string; + time_close: string; + enabled: string; + deleted: string; + locked: string; + date: string; } interface Service { - id: number; - name: string; - active: boolean; + id: number; + name: string; + active: boolean; } interface TicketCreateInfo { - departments: Department[]; - services: Service[]; + departments: Department[]; + services: Service[]; } const ticketCreateInfo = ref(null); const loading = ref(false); const fetchTicketCreateInfo = async () => { - try { - const response = await Tickets.getTicketCreateInfo(); - if (response.success) { - ticketCreateInfo.value = { - departments: response.departments, - services: response.services - }; - } else { - console.error('Failed to fetch ticket create info:', response.error); + try { + const response = await Tickets.getTicketCreateInfo(); + if (response.success) { + ticketCreateInfo.value = { + departments: response.departments, + services: response.services, + }; + } else { + console.error('Failed to fetch ticket create info:', response.error); + } + } catch (error) { + console.error('Error fetching ticket create info:', error); } - } catch (error) { - console.error('Error fetching ticket create info:', error); - } }; onMounted(() => { - fetchTicketCreateInfo(); + fetchTicketCreateInfo(); }); const ticket = ref({ - service: '', - department: '', - priority: 'medium', - subject: '', - message: '' + service: '', + department: '', + priority: 'medium', + subject: '', + message: '', }); const priorities = [ - { value: 'low', label: 'Low' }, - { value: 'medium', label: 'Medium' }, - { value: 'high', label: 'High' }, - { value: 'urgent', label: 'Urgent' } + { value: 'low', label: t('account.pages.create_ticket.types.priority.low') }, + { value: 'medium', label: t('account.pages.create_ticket.types.priority.medium') }, + { value: 'high', label: t('account.pages.create_ticket.types.priority.high') }, + { value: 'urgent', label: t('account.pages.create_ticket.types.priority.urgent') }, ]; const submitTicket = async () => { - loading.value = true; - try { - // Handle ticket submission - console.log('Submitting ticket:', ticket.value); - - const response = await Tickets.createTicket(Number(ticket.value.department), ticket.value.subject, ticket.value.message, ticket.value.priority, Number(ticket.value.service)); - - if (!response.success) { - const error_code = response.error_code as keyof typeof errorMessages; - - const errorMessages = { - - }; - - if (errorMessages[error_code]) { - playError(); - Swal.fire({ - icon: 'error', - title: "Error while creating ticket!", - text: errorMessages[error_code], - footer: "Kinda funny since you tried to create a ticket", - showConfirmButton: true, - }); + loading.value = true; + try { + // Handle ticket submission + console.log('Submitting ticket:', ticket.value); + + const response = await Tickets.createTicket( + Number(ticket.value.department), + ticket.value.subject, + ticket.value.message, + ticket.value.priority, + Number(ticket.value.service), + ); + + if (!response.success) { + const error_code = response.error_code as keyof typeof errorMessages; + + const errorMessages = { + LIMIT_REACHED: t('account.pages.create_ticket.alerts.error.limit_reached'), + FAILED_TO_CREATE_TICKET: t('account.pages.create_ticket.alerts.error.generic'), + DEPARTMENT_NOT_FOUND: t('account.pages.create_ticket.alerts.error.department_not_found'), + DEPARTMENT_ID_MISSING: t('account.pages.create_ticket.alerts.error.department_id_missing'), + MESSAGE_MISSING: t('account.pages.create_ticket.alerts.error.message_missing'), + SUBJECT_MISSING: t('account.pages.create_ticket.alerts.error.subject_missing'), + }; + + if (errorMessages[error_code]) { + playError(); + Swal.fire({ + icon: 'error', + title: t('account.pages.create_ticket.alerts.error.title'), + text: errorMessages[error_code], + footer: t('account.pages.create_ticket.alerts.error.footer'), + showConfirmButton: true, + }); + loading.value = false; + throw new Error('Login failed'); + } else { + playError(); + Swal.fire({ + icon: 'error', + title: t('account.pages.create_ticket.alerts.error.title'), + text: t('account.pages.create_ticket.alerts.error.generic'), + footer: t('account.pages.create_ticket.alerts.error.footer'), + showConfirmButton: true, + }); + loading.value = false; + throw new Error('Ticket creation failed'); + } + } else { + playSuccess(); + Swal.fire({ + icon: 'success', + title: t('account.pages.create_ticket.alerts.success.title'), + text: t('account.pages.create_ticket.alerts.success.ticket_success'), + footer: t('account.pages.create_ticket.alerts.success.footer'), + showConfirmButton: true, + }); + loading.value = false; + setTimeout(() => { + router.push('/ticket'); + }, 1500); + console.log('Ticket submitted successfully:', response.ticket); + } + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } catch (error) { + console.error('Error submitting ticket:', error); + } finally { loading.value = false; - throw new Error('Login failed'); - } else { - playError(); - Swal.fire({ - icon: 'error', - title: "Error while creating ticket!", - text: "We never expected this to happen", - footer: "Kinda funny since you tried to create a ticket", - showConfirmButton: true, - }); - loading.value = false; - throw new Error('Ticket creation failed'); - } - } else { - playSuccess(); - Swal.fire({ - icon: 'success', - title: "Ticket submitted successfully!", - text: "Your ticket has been submitted successfully", - footer: "You will be redirected to the ticket page", - showConfirmButton: true, - }); - loading.value = false; - setTimeout(() => { - router.push('/ticket'); - }, 1500); - console.log('Ticket submitted successfully:', response.ticket); } - - await new Promise(resolve => setTimeout(resolve, 2000)); - } catch (error) { - console.error('Error submitting ticket:', error); - } finally { - loading.value = false; - } };