- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
-> Ticket system list -> Ticket system create ticket -> Ticket system translation!
Showing
22 changed files
with
1,564 additions
and
1,200 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of MythicalClient. | ||
* Please view the LICENSE file that was distributed with this source code. | ||
* | ||
* # MythicalSystems License v2.0 | ||
* | ||
* ## Copyright (c) 2021–2025 MythicalSystems and Cassian Gherman | ||
* | ||
* Breaking any of the following rules will result in a permanent ban from the MythicalSystems community and all of its services. | ||
*/ | ||
|
||
use MythicalClient\App; | ||
use MythicalClient\Chat\Session; | ||
use MythicalClient\Chat\Tickets; | ||
use MythicalClient\Chat\columns\UserColumns; | ||
|
||
$router->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, | ||
]); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
backend/storage/migrations/2025-01-21-12.50-add-status-tickets.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ALTER TABLE `mythicalclient_tickets` ADD `status` ENUM('open','closed','waiting','replied','inprogress') NOT NULL DEFAULT 'open' AFTER `priority`; |
563 changes: 306 additions & 257 deletions
563
frontend/src/components/client/Dashboard/Main/Announcements.vue
Large diffs are not rendered by default.
Oops, something went wrong.
45 changes: 23 additions & 22 deletions
45
frontend/src/components/client/Dashboard/Main/BillingInfo.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,28 @@ | ||
<template> | ||
<CardComponent> | ||
<h2 class="text-white text-lg font-semibold mb-4">Billing Summary</h2> | ||
<div class="space-y-2 text-sm"> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Current Balance:</span> | ||
<span class="text-white font-medium">$250.00</span> | ||
</div> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Next Invoice:</span> | ||
<span class="text-white font-medium">$120.00</span> | ||
</div> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Due Date:</span> | ||
<span class="text-white font-medium">15 Jul 2023</span> | ||
</div> | ||
</div> | ||
<RouterLink to="/billing" | ||
class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm"> | ||
View Invoices | ||
</RouterLink> | ||
</CardComponent> | ||
<CardComponent> | ||
<h2 class="text-white text-lg font-semibold mb-4">Billing Summary</h2> | ||
<div class="space-y-2 text-sm"> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Current Balance:</span> | ||
<span class="text-white font-medium">$250.00</span> | ||
</div> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Next Invoice:</span> | ||
<span class="text-white font-medium">$120.00</span> | ||
</div> | ||
<div class="flex justify-between"> | ||
<span class="text-purple-200">Due Date:</span> | ||
<span class="text-white font-medium">15 Jul 2023</span> | ||
</div> | ||
</div> | ||
<RouterLink | ||
to="/billing" | ||
class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm" | ||
> | ||
View Invoices | ||
</RouterLink> | ||
</CardComponent> | ||
</template> | ||
<script lang="ts" setup> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,26 @@ | ||
<template> | ||
<div class="flex justify-between items-center"> | ||
<div> | ||
<h1 class="text-3xl font-bold text-white mb-2">My Dashboard</h1> | ||
<div class="text-purple-200 text-sm"> | ||
<RouterLink to="/" class="hover:text-white transition-colors">Portal Home</RouterLink> | ||
<span class="mx-2">/</span> | ||
<span>Client Area</span> | ||
</div> | ||
<div class="flex justify-between items-center"> | ||
<div> | ||
<h1 class="text-3xl font-bold text-white mb-2">My Dashboard</h1> | ||
<div class="text-purple-200 text-sm"> | ||
<RouterLink to="/" class="hover:text-white transition-colors">Portal Home</RouterLink> | ||
<span class="mx-2">/</span> | ||
<span>Client Area</span> | ||
</div> | ||
</div> | ||
<div class="flex items-center space-x-4"> | ||
<div class="text-right"> | ||
<p class="text-purple-200 text-sm">Welcome back,</p> | ||
<p class="text-white font-semibold">{{ Session.getInfo('first_name') }}</p> | ||
</div> | ||
<img | ||
:src="`${Session.getInfo('avatar')}?height=40&width=40`" | ||
alt="Profile" | ||
class="w-10 h-10 rounded-full border-2 border-purple-500" | ||
/> | ||
</div> | ||
</div> | ||
<div class="flex items-center space-x-4"> | ||
<div class="text-right"> | ||
<p class="text-purple-200 text-sm">Welcome back,</p> | ||
<p class="text-white font-semibold">{{ Session.getInfo('first_name') }}</p> | ||
</div> | ||
<img :src="`${Session.getInfo('avatar')}?height=40&width=40`" alt="Profile" | ||
class="w-10 h-10 rounded-full border-2 border-purple-500" /> | ||
</div> | ||
</div> | ||
</template> | ||
<script setup lang="ts"> | ||
import Session from '@/mythicalclient/Session'; | ||
</script> |
73 changes: 38 additions & 35 deletions
73
frontend/src/components/client/Dashboard/Main/ProductList.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,48 @@ | ||
<template> | ||
<CardComponent> | ||
<div class="flex items-center justify-between mb-4"> | ||
<h2 class="text-lg font-semibold text-white">Your Active Products/Services</h2> | ||
<button class="text-purple-500 hover:text-white transition-colors"> | ||
<MenuIcon class="w-5 h-5" /> | ||
</button> | ||
</div> | ||
<div class="space-y-4"> | ||
<div v-for="(server, index) in servers" :key="index" | ||
class="flex items-center justify-between py-3 border-b border-purple-700 last:border-0"> | ||
<div> | ||
<div class="font-medium text-white">{{ server.name }}</div> | ||
<div class="text-sm text-purple-500">{{ server.hostname }}</div> | ||
</div> | ||
<div class="flex items-center gap-3"> | ||
<span class="px-2 py-1 bg-emerald-500/20 text-emerald-400 rounded text-xs font-medium"> | ||
Active | ||
</span> | ||
<button | ||
class="px-3 py-1 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm"> | ||
Manage | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</CardComponent> | ||
|
||
<CardComponent> | ||
<div class="flex items-center justify-between mb-4"> | ||
<h2 class="text-lg font-semibold text-white">Your Active Products/Services</h2> | ||
<button class="text-purple-500 hover:text-white transition-colors"> | ||
<MenuIcon class="w-5 h-5" /> | ||
</button> | ||
</div> | ||
<div class="space-y-4"> | ||
<div | ||
v-for="(server, index) in servers" | ||
:key="index" | ||
class="flex items-center justify-between py-3 border-b border-purple-700 last:border-0" | ||
> | ||
<div> | ||
<div class="font-medium text-white">{{ server.name }}</div> | ||
<div class="text-sm text-purple-500">{{ server.hostname }}</div> | ||
</div> | ||
<div class="flex items-center gap-3"> | ||
<span class="px-2 py-1 bg-emerald-500/20 text-emerald-400 rounded text-xs font-medium"> | ||
Active | ||
</span> | ||
<button | ||
class="px-3 py-1 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm" | ||
> | ||
Manage | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</CardComponent> | ||
</template> | ||
<script setup lang="ts"> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
import { ref } from 'vue'; | ||
import { MenuIcon } from 'lucide-vue-next'; | ||
const servers = ref([ | ||
{ | ||
name: 'Storage Root Server Frankfurt - Storage KVM S', | ||
hostname: 'backup2.mythical.systems', | ||
}, | ||
{ | ||
name: 'Storage Root Server Frankfurt - Storage KVM S', | ||
hostname: 'backup.mythical.systems', | ||
}, | ||
{ | ||
name: 'Storage Root Server Frankfurt - Storage KVM S', | ||
hostname: 'backup2.mythical.systems', | ||
}, | ||
{ | ||
name: 'Storage Root Server Frankfurt - Storage KVM S', | ||
hostname: 'backup.mythical.systems', | ||
}, | ||
]); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,18 @@ | ||
<template> | ||
<div class="grid grid-cols-3 gap-4"> | ||
<CardComponent v-for="(stat, index) in stats" :key="index"> | ||
<component :is="stat.icon" class="w-6 h-6 text-purple-500 mb-2" /> | ||
<div class="text-3xl font-bold text-white mb-1">{{ stat.value }}</div> | ||
<div class="text-purple-200 text-sm">{{ stat.label }}</div> | ||
</CardComponent> | ||
</div> | ||
<div class="grid grid-cols-3 gap-4"> | ||
<CardComponent v-for="(stat, index) in stats" :key="index"> | ||
<component :is="stat.icon" class="w-6 h-6 text-purple-500 mb-2" /> | ||
<div class="text-3xl font-bold text-white mb-1">{{ stat.value }}</div> | ||
<div class="text-purple-200 text-sm">{{ stat.label }}</div> | ||
</CardComponent> | ||
</div> | ||
</template> | ||
<script lang="ts" setup> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
import { | ||
Server as ServerIcon, | ||
FileText as FileTextIcon, | ||
Ticket as TicketIcon, | ||
} from 'lucide-vue-next'; | ||
import { Server as ServerIcon, FileText as FileTextIcon, Ticket as TicketIcon } from 'lucide-vue-next'; | ||
const stats = [ | ||
{ icon: ServerIcon, value: '2', label: 'Services' }, | ||
{ icon: FileTextIcon, value: '0', label: 'Unpaid Invoices' }, | ||
{ icon: TicketIcon, value: '1', label: 'Tickets' }, | ||
{ icon: ServerIcon, value: '2', label: 'Services' }, | ||
{ icon: FileTextIcon, value: '0', label: 'Unpaid Invoices' }, | ||
{ icon: TicketIcon, value: '1', label: 'Tickets' }, | ||
]; | ||
</script> |
114 changes: 55 additions & 59 deletions
114
frontend/src/components/client/Dashboard/Main/SupportPin.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,74 @@ | ||
<template> | ||
<!-- Support PIN --> | ||
<CardComponent> | ||
<h2 class="text-purple-200 text-sm font-medium mb-2">{{ t('Components.SupportPin.title') }}</h2> | ||
<div class="flex items-center gap-2"> | ||
<span class="text-emerald-400 text-2xl font-mono font-bold">{{ Session.getInfo('support_pin') }}</span> | ||
<button @click="copyPin" class="text-purple-500 hover:text-white transition-colors"> | ||
<CopyIcon class="w-4 h-4" /> | ||
</button> | ||
<button @click="resetPin" class="text-purple-500 hover:text-white transition-colors"> | ||
<RefreshCcwIcon class="w-4 h-4 transition-transform duration-500 hover:rotate-180" /> | ||
</button> | ||
|
||
</div> | ||
</CardComponent> | ||
<!-- Support PIN --> | ||
<CardComponent> | ||
<h2 class="text-purple-200 text-sm font-medium mb-2">{{ t('Components.SupportPin.title') }}</h2> | ||
<div class="flex items-center gap-2"> | ||
<span class="text-emerald-400 text-2xl font-mono font-bold">{{ Session.getInfo('support_pin') }}</span> | ||
<button @click="copyPin" class="text-purple-500 hover:text-white transition-colors"> | ||
<CopyIcon class="w-4 h-4" /> | ||
</button> | ||
<button @click="resetPin" class="text-purple-500 hover:text-white transition-colors"> | ||
<RefreshCcwIcon class="w-4 h-4 transition-transform duration-500 hover:rotate-180" /> | ||
</button> | ||
</div> | ||
</CardComponent> | ||
</template> | ||
<script setup lang="ts"> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
import Auth from '@/mythicalclient/Auth'; | ||
import Session from '@/mythicalclient/Session'; | ||
import Swal from 'sweetalert2'; | ||
import { useI18n } from 'vue-i18n'; | ||
import { | ||
RefreshCcw as RefreshCcwIcon, | ||
Copy as CopyIcon, | ||
} from 'lucide-vue-next'; | ||
import { RefreshCcw as RefreshCcwIcon, Copy as CopyIcon } from 'lucide-vue-next'; | ||
const { t } = useI18n(); | ||
const copyPin = async () => { | ||
const pin = Session.getInfo('support_pin'); | ||
try { | ||
if (!navigator.clipboard) { | ||
const textArea = document.createElement('textarea') | ||
textArea.value = pin | ||
document.body.appendChild(textArea) | ||
textArea.select() | ||
document.execCommand('copy') | ||
document.body.removeChild(textArea) | ||
} else { | ||
await navigator.clipboard.writeText(pin) | ||
} | ||
const pin = Session.getInfo('support_pin'); | ||
try { | ||
if (!navigator.clipboard) { | ||
const textArea = document.createElement('textarea'); | ||
textArea.value = pin; | ||
document.body.appendChild(textArea); | ||
textArea.select(); | ||
document.execCommand('copy'); | ||
document.body.removeChild(textArea); | ||
} else { | ||
await navigator.clipboard.writeText(pin); | ||
} | ||
Swal.fire({ | ||
icon: 'success', | ||
title: t('Components.Global.Navigation.Copy.Title'), | ||
text: t('Components.Global.Navigation.Copy.Success'), | ||
footer: t('Components.Global.Navigation.Copy.Footer'), | ||
}) | ||
} catch (err) { | ||
console.error('Failed to copy command to clipboard', err) | ||
} | ||
Swal.fire({ | ||
icon: 'success', | ||
title: t('Components.Global.Navigation.Copy.Title'), | ||
text: t('Components.Global.Navigation.Copy.Success'), | ||
footer: t('Components.Global.Navigation.Copy.Footer'), | ||
}); | ||
} catch (err) { | ||
console.error('Failed to copy command to clipboard', err); | ||
} | ||
}; | ||
const resetPin = async () => { | ||
try { | ||
const ping = await Auth.resetPin(); | ||
const pinElement = document.querySelector('span.text-emerald-400'); | ||
if (pinElement) { | ||
pinElement.textContent = ping.toString(); | ||
Swal.fire({ | ||
title: t('Components.SupportPin.alerts.success.title'), | ||
text: t('Components.SupportPin.alerts.success.pin_success'), | ||
icon: 'success', | ||
footer: t('Components.SupportPin.alerts.success.footer'), | ||
}); | ||
try { | ||
const ping = await Auth.resetPin(); | ||
const pinElement = document.querySelector('span.text-emerald-400'); | ||
if (pinElement) { | ||
pinElement.textContent = ping.toString(); | ||
Swal.fire({ | ||
title: t('Components.SupportPin.alerts.success.title'), | ||
text: t('Components.SupportPin.alerts.success.pin_success'), | ||
icon: 'success', | ||
footer: t('Components.SupportPin.alerts.success.footer'), | ||
}); | ||
} | ||
} catch (error) { | ||
console.error('Failed to reset support pin:', error); | ||
Swal.fire({ | ||
title: t('Components.SupportPin.alerts.error.title'), | ||
text: t('Components.SupportPin.alerts.error.generic'), | ||
icon: 'error', | ||
footer: t('Components.SupportPin.alerts.error.footer'), | ||
}); | ||
} | ||
} catch (error) { | ||
console.error('Failed to reset support pin:', error); | ||
Swal.fire({ | ||
title: t('Components.SupportPin.alerts.error.title'), | ||
text: t('Components.SupportPin.alerts.error.generic'), | ||
icon: 'error', | ||
footer: t('Components.SupportPin.alerts.error.footer'), | ||
}); | ||
} | ||
}; | ||
</script> |
63 changes: 34 additions & 29 deletions
63
frontend/src/components/client/Dashboard/Main/TicketList.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,45 @@ | ||
<template> | ||
<!-- Recent Tickets --> | ||
<CardComponent> | ||
<h2 class="text-lg font-semibold text-white mb-4">Recent Tickets</h2> | ||
<div class="space-y-3"> | ||
<div v-for="ticket in recentTickets" :key="ticket.id" | ||
class="flex items-center justify-between py-2 border-b border-purple-700 last:border-0"> | ||
<div> | ||
<div class="font-medium text-white">{{ ticket.title }}</div> | ||
<div class="text-sm text-purple-500">{{ ticket.date }}</div> | ||
<!-- Recent Tickets --> | ||
<CardComponent> | ||
<h2 class="text-lg font-semibold text-white mb-4">Recent Tickets</h2> | ||
<div class="space-y-3"> | ||
<div | ||
v-for="ticket in recentTickets" | ||
:key="ticket.id" | ||
class="flex items-center justify-between py-2 border-b border-purple-700 last:border-0" | ||
> | ||
<div> | ||
<div class="font-medium text-white">{{ ticket.title }}</div> | ||
<div class="text-sm text-purple-500">{{ ticket.date }}</div> | ||
</div> | ||
<span | ||
:class="[ | ||
'px-2 py-1 rounded text-xs font-medium', | ||
ticket.status === 'Open' | ||
? 'bg-yellow-500/20 text-yellow-400' | ||
: 'bg-emerald-500/20 text-emerald-400', | ||
]" | ||
> | ||
{{ ticket.status }} | ||
</span> | ||
</div> | ||
</div> | ||
<span :class="[ | ||
'px-2 py-1 rounded text-xs font-medium', | ||
ticket.status === 'Open' | ||
? 'bg-yellow-500/20 text-yellow-400' | ||
: 'bg-emerald-500/20 text-emerald-400', | ||
]"> | ||
{{ ticket.status }} | ||
</span> | ||
</div> | ||
</div> | ||
<RouterLink to="/support" | ||
class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm"> | ||
View All Tickets | ||
</RouterLink> | ||
</CardComponent> | ||
|
||
<RouterLink | ||
to="/support" | ||
class="mt-4 block w-full px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-center text-sm" | ||
> | ||
View All Tickets | ||
</RouterLink> | ||
</CardComponent> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
import { ref } from 'vue'; | ||
const recentTickets = ref([ | ||
{ id: 1, title: 'Server Performance Issue', date: '2023-07-01', status: 'Open' }, | ||
{ id: 2, title: 'Billing Inquiry', date: '2023-06-28', status: 'Closed' }, | ||
{ id: 3, title: 'Domain Transfer Request', date: '2023-06-25', status: 'Open' }, | ||
{ id: 1, title: 'Server Performance Issue', date: '2023-07-01', status: 'Open' }, | ||
{ id: 2, title: 'Billing Inquiry', date: '2023-06-28', status: 'Closed' }, | ||
{ id: 3, title: 'Domain Transfer Request', date: '2023-06-25', status: 'Open' }, | ||
]); | ||
</script> |
60 changes: 31 additions & 29 deletions
60
frontend/src/components/client/Dashboard/Main/UserInfo.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,39 @@ | ||
<template> | ||
<!-- Profile Card --> | ||
<CardComponent> | ||
<div class="flex flex-col items-center text-center"> | ||
<MarketIcon class="w-20 h-20 text-purple-500 mb-4" /> | ||
<div class="text-xl text-white mb-2"> | ||
{{ Session.getInfo('company_name') }} | ||
</div> | ||
<div class="text-purple-200 text-sm space-y-1 mb-4"> | ||
<div>{{ Session.getInfo('vat_number') }}</div> | ||
<div>{{ Session.getInfo('address1') }}</div> | ||
<div> | ||
{{ Session.getInfo('city') }} ({{ Session.getInfo('postcode') }}), | ||
{{ Session.getInfo('country') }} | ||
<!-- Profile Card --> | ||
<CardComponent> | ||
<div class="flex flex-col items-center text-center"> | ||
<MarketIcon class="w-20 h-20 text-purple-500 mb-4" /> | ||
<div class="text-xl text-white mb-2"> | ||
{{ Session.getInfo('company_name') }} | ||
</div> | ||
<div class="text-purple-200 text-sm space-y-1 mb-4"> | ||
<div>{{ Session.getInfo('vat_number') }}</div> | ||
<div>{{ Session.getInfo('address1') }}</div> | ||
<div> | ||
{{ Session.getInfo('city') }} ({{ Session.getInfo('postcode') }}), | ||
{{ Session.getInfo('country') }} | ||
</div> | ||
</div> | ||
<div class="flex gap-2 w-full"> | ||
<RouterLink | ||
to="/account" | ||
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm" | ||
> | ||
Update | ||
</RouterLink> | ||
<a | ||
href="/api/auth/logout" | ||
class="flex-1 px-4 py-2 bg-purple-800 hover:bg-purple-700 text-white rounded transition-colors text-sm" | ||
> | ||
Logout | ||
</a> | ||
</div> | ||
</div> | ||
</div> | ||
<div class="flex gap-2 w-full"> | ||
<RouterLink to="/account" | ||
class="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded transition-colors text-sm"> | ||
Update | ||
</RouterLink> | ||
<a href="/api/auth/logout" | ||
class="flex-1 px-4 py-2 bg-purple-800 hover:bg-purple-700 text-white rounded transition-colors text-sm"> | ||
Logout | ||
</a> | ||
</div> | ||
</div> | ||
</CardComponent> | ||
</CardComponent> | ||
</template> | ||
<script lang="ts" setup> | ||
import CardComponent from '@/components/client/ui/Card/CardComponent.vue'; | ||
import Session from '@/mythicalclient/Session'; | ||
import { | ||
BookMarkedIcon as MarketIcon, | ||
} from 'lucide-vue-next'; | ||
import { BookMarkedIcon as MarketIcon } from 'lucide-vue-next'; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,27 @@ | ||
<template> | ||
<textarea v-model="inputValue" :class="inputClass" :placeholder="placeholder"></textarea> | ||
<textarea v-model="inputValue" :class="inputClass" :placeholder="placeholder"></textarea> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { computed } from 'vue'; | ||
const props = defineProps({ | ||
modelValue: String, | ||
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 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50 focus:outline-none', | ||
}, | ||
placeholder: { | ||
type: String, | ||
default: '', | ||
}, | ||
modelValue: String, | ||
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 placeholder-gray-500 focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/50 focus:outline-none', | ||
}, | ||
placeholder: { | ||
type: String, | ||
default: '', | ||
}, | ||
}); | ||
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), | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,99 +1,237 @@ | ||
<script setup lang="ts"> | ||
import { ref, onMounted, onErrorCaptured, computed, h } from 'vue'; | ||
import { useRouter } from 'vue-router'; | ||
import { useI18n } from 'vue-i18n'; | ||
import { format } from 'date-fns'; | ||
import LayoutDashboard from '@/components/client/LayoutDashboard.vue'; | ||
import TableTanstack from '@/components/client/ui/Table/TableTanstack.vue'; | ||
import { h } from 'vue'; | ||
import { useRouter } from 'vue-router'; | ||
import Tickets from '@/mythicalclient/Tickets'; | ||
import { AlertCircle, Plus, Ticket } from 'lucide-vue-next'; | ||
const router = useRouter(); | ||
const { t } = useI18n(); | ||
const Tickets = [ | ||
{ | ||
id: 1, | ||
department: 'IT Support', | ||
subject: 'Computer not starting', | ||
status: 'Open', | ||
category: 'Hardware', | ||
created_at: '2023-10-01', | ||
}, | ||
{ | ||
id: 2, | ||
department: 'HR', | ||
subject: 'Payroll issue', | ||
status: 'In Progress', | ||
category: 'Payroll', | ||
created_at: '2023-10-01', | ||
}, | ||
{ | ||
id: 3, | ||
department: 'IT Support', | ||
subject: 'Email not working', | ||
status: 'Closed', | ||
category: 'Software', | ||
created_at: '2023-10-01', | ||
}, | ||
{ | ||
id: 4, | ||
department: 'Facilities', | ||
subject: 'Broken AC', | ||
status: 'Open', | ||
category: 'Maintenance', | ||
created_at: '2023-10-01', | ||
}, | ||
]; | ||
interface Department { | ||
id: number; | ||
name: string; | ||
description: string; | ||
time_open: string; | ||
time_close: string; | ||
enabled: string; | ||
deleted: string; | ||
locked: string; | ||
date: string; | ||
} | ||
interface Ticket { | ||
id: number; | ||
user: string; | ||
department: Department; | ||
priority: string; | ||
status: string; | ||
service: number | null; | ||
title: string; | ||
description: string; | ||
deleted: string; | ||
locked: string; | ||
date: string; | ||
department_id: number; | ||
} | ||
const tickets = ref<Ticket[]>([]); | ||
const loading = ref(true); | ||
const error = ref<string | null>(null); | ||
const fetchTickets = async () => { | ||
try { | ||
const response = await Tickets.getTickets(); | ||
if (response.success && Array.isArray(response.tickets)) { | ||
tickets.value = response.tickets; | ||
} else { | ||
throw new Error(response.error || 'Failed to fetch tickets'); | ||
} | ||
} catch (err) { | ||
error.value = err instanceof Error ? err.message : t('account.pages.tickets.alerts.error.generic'); | ||
} finally { | ||
loading.value = false; | ||
} | ||
}; | ||
onMounted(fetchTickets); | ||
onErrorCaptured((err) => { | ||
error.value = t('account.pages.tickets.alerts.error.generic'); | ||
console.error('Error captured:', err); | ||
return false; | ||
}); | ||
const filteredTickets = computed(() => { | ||
return tickets.value.filter((ticket) => ticket.status.toUpperCase() !== 'CLOSED'); | ||
}); | ||
const columnsTickets = [ | ||
{ | ||
accessorKey: 'department', | ||
header: 'Department', | ||
accessorKey: 'title', | ||
header: t('account.pages.tickets.table.subject'), | ||
}, | ||
{ | ||
accessorKey: 'subject', | ||
header: 'Subject', | ||
accessorKey: 'status', | ||
header: t('account.pages.tickets.table.status'), | ||
cell: (info: { getValue: () => string }) => { | ||
const status = info.getValue().toUpperCase(); | ||
let statusClass = ''; | ||
switch (status) { | ||
case 'OPEN': | ||
statusClass = 'bg-green-100 text-green-800'; | ||
break; | ||
case 'CLOSED': | ||
statusClass = 'bg-red-100 text-red-800'; | ||
break; | ||
case 'WAITING': | ||
statusClass = 'bg-yellow-100 text-yellow-800'; | ||
break; | ||
case 'REPLIED': | ||
statusClass = 'bg-blue-100 text-blue-800'; | ||
break; | ||
case 'INPROGRESS': | ||
statusClass = 'bg-orange-100 text-orange-800'; | ||
break; | ||
default: | ||
statusClass = 'bg-gray-100 text-gray-800'; | ||
} | ||
return h( | ||
'span', | ||
{ class: `px-2 py-1 rounded-full text-xs font-semibold ${statusClass} dark:bg-opacity-50` }, | ||
status === 'INPROGRESS' ? 'IN PROGRESS' : status, | ||
); | ||
}, | ||
}, | ||
{ | ||
accessorKey: 'status', | ||
header: 'Status', | ||
accessorKey: 'priority', | ||
header: t('account.pages.tickets.table.priority'), | ||
cell: (info: { getValue: () => string }) => { | ||
const priority = info.getValue().toUpperCase(); | ||
let priorityClass = ''; | ||
switch (priority) { | ||
case 'LOW': | ||
priorityClass = 'bg-green-100 text-green-800'; | ||
break; | ||
case 'MEDIUM': | ||
priorityClass = 'bg-yellow-100 text-yellow-800'; | ||
break; | ||
case 'HIGH': | ||
priorityClass = 'bg-orange-100 text-orange-800'; | ||
break; | ||
case 'URGENT': | ||
priorityClass = 'bg-red-100 text-red-800'; | ||
break; | ||
default: | ||
priorityClass = 'bg-gray-100 text-gray-800'; | ||
} | ||
return h( | ||
'span', | ||
{ class: `px-2 py-1 rounded-full text-xs font-semibold ${priorityClass} dark:bg-opacity-50` }, | ||
priority, | ||
); | ||
}, | ||
}, | ||
{ | ||
accessorKey: 'category', | ||
header: 'Category', | ||
accessorKey: 'department.name', | ||
header: t('account.pages.tickets.table.department'), | ||
}, | ||
{ | ||
accessorKey: 'created_at', | ||
header: 'Created', | ||
accessorKey: 'date', | ||
header: t('account.pages.tickets.table.created'), | ||
cell: (info: { getValue: () => string | number | Date }) => format(new Date(info.getValue()), 'MMM d, yyyy'), | ||
}, | ||
{ | ||
accessorKey: 'actions', | ||
header: 'Actions', | ||
header: t('account.pages.tickets.table.actions'), | ||
enableSorting: false, | ||
cell: ({ row }: { row: { original: { id: number } } }) => | ||
h( | ||
'button', | ||
{ | ||
onClick: () => callMovetoTicket(row.original.id), | ||
onClick: () => viewTicket(row.original.id), | ||
class: 'text-purple-500 hover:underline', | ||
}, | ||
'View', | ||
t('account.pages.tickets.actions.view'), | ||
), | ||
}, | ||
]; | ||
function callMovetoTicket(id: number) { | ||
console.log('Move to ticket with ID:', id); | ||
function viewTicket(id: number) { | ||
router.push(`/ticket/${id}`); | ||
} | ||
function createNewTicket() { | ||
router.push('/ticket/create'); | ||
} | ||
</script> | ||
|
||
<template> | ||
<LayoutDashboard> | ||
<div class="space-y-6"> | ||
<div class="space-y-6 p-6"> | ||
<div class="flex justify-between items-center"> | ||
<h1 class="text-2xl font-semibold text-gray-100">Support Tickets</h1> | ||
<h1 class="text-3xl font-bold text-gray-100">{{ t('account.pages.tickets.title') }}</h1> | ||
<button | ||
class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg text-sm font-medium transition-colors" | ||
@click="createNewTicket" | ||
class="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg text-sm font-medium transition-colors flex items-center space-x-2" | ||
> | ||
New Ticket | ||
<Plus class="w-4 h-4" /> | ||
<span>{{ t('account.pages.tickets.actions.newTicket') }}</span> | ||
</button> | ||
</div> | ||
<TableTanstack :data="Tickets" :columns="columnsTickets" tableName="Your tickets" /> | ||
|
||
<Transition name="fade" mode="out-in"> | ||
<div v-if="loading" class="space-y-4" key="loading"> | ||
<div v-for="i in 5" :key="i" class="bg-gray-800 rounded-lg p-4 animate-pulse"> | ||
<div class="h-6 bg-gray-700 rounded w-3/4 mb-2"></div> | ||
<div class="h-4 bg-gray-700 rounded w-1/2"></div> | ||
</div> | ||
</div> | ||
|
||
<div | ||
v-else-if="error" | ||
class="bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded-lg flex items-center space-x-2" | ||
role="alert" | ||
key="error" | ||
> | ||
<AlertCircle class="w-5 h-5 flex-shrink-0" /> | ||
<p>{{ error }}</p> | ||
</div> | ||
|
||
<div v-else-if="tickets.length === 0" class="text-center py-12 bg-gray-800 rounded-lg" key="empty"> | ||
<Ticket class="w-16 h-16 text-gray-600 mx-auto mb-4" /> | ||
<p class="text-xl font-semibold text-gray-300">{{ t('account.pages.tickets.noTickets') }}</p> | ||
<p class="text-gray-400 mt-2">{{ t('account.pages.tickets.createNewTicket') }}</p> | ||
<button | ||
@click="createNewTicket" | ||
class="mt-4 px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-lg text-sm font-medium transition-colors" | ||
> | ||
{{ t('account.pages.tickets.actions.newTicket') }} | ||
</button> | ||
</div> | ||
|
||
<div v-else key="table"> | ||
<TableTanstack | ||
:data="filteredTickets" | ||
:columns="columnsTickets" | ||
:tableName="t('account.pages.tickets.table.title')" | ||
/> | ||
</div> | ||
</Transition> | ||
</div> | ||
</LayoutDashboard> | ||
</template> | ||
|
||
<style scoped> | ||
.fade-enter-active, | ||
.fade-leave-active { | ||
transition: opacity 0.5s ease; | ||
} | ||
.fade-enter-from, | ||
.fade-leave-to { | ||
opacity: 0; | ||
} | ||
</style> |