Repo rebuild

This commit is contained in:
Mark Gregory
2026-09-15 12:33:28 +01:00
commit be28c4feec
5629 changed files with 317074 additions and 0 deletions
@@ -0,0 +1,11 @@
<template>
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<slot name="logo" />
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,17 @@
<script setup>
import { Link } from '@inertiajs/vue3';
</script>
<template>
<Link :href="'/'">
<svg
class="size-16"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5" />
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5" />
</svg>
</Link>
</template>
+36
View File
@@ -0,0 +1,36 @@
<script setup>
import { computed } from 'vue';
const emit = defineEmits(['update:checked']);
const props = defineProps({
checked: {
type: [Array, Boolean],
default: false,
},
value: {
type: String,
default: null,
},
});
const proxyChecked = computed({
get() {
return props.checked;
},
set(val) {
emit('update:checked', val);
},
});
</script>
<template>
<input
v-model="proxyChecked"
type="checkbox"
:value="value"
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
>
</template>
@@ -0,0 +1,63 @@
<script setup>
import { ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const form = useForm({
password: '',
});
const passwordInput = ref(null);
const submit = () => {
form.post(route('password.confirm'), {
onFinish: () => {
form.reset();
passwordInput.value.focus();
},
});
};
</script>
<template>
<Head title="Secure Area" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
This is a secure area of the application. Please confirm your password before continuing.
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="password" value="Password" />
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="current-password"
autofocus
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="flex justify-end mt-4">
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Confirm
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
@@ -0,0 +1,61 @@
<script setup>
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
defineProps({
status: String,
});
const form = useForm({
email: '',
});
const submit = () => {
form.post(route('password.email'));
};
</script>
<template>
<Head title="Forgot Password" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.
</div>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Email Password Reset Link
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
+90
View File
@@ -0,0 +1,90 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
defineProps({
canResetPassword: Boolean,
status: String,
});
const form = useForm({
email: '',
password: '',
remember: false,
});
const submit = () => {
form.transform(data => ({
...data,
remember: form.remember ? 'on' : '',
})).post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<Head title="Log in" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="current-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="block mt-4">
<label class="flex items-center">
<Checkbox v-model:checked="form.remember" name="remember" />
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">Remember me</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
<Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
Forgot your password?
</Link>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Log in
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
+112
View File
@@ -0,0 +1,112 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
terms: false,
});
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<Head title="Register" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<form @submit.prevent="submit">
<div>
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
required
autofocus
autocomplete="name"
/>
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div class="mt-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password_confirmation" />
</div>
<div v-if="$page.props.jetstream.hasTermsAndPrivacyPolicyFeature" class="mt-4">
<InputLabel for="terms">
<div class="flex items-center">
<Checkbox id="terms" v-model:checked="form.terms" name="terms" required />
<div class="ms-2">
I agree to the <a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">Terms of Service</a> and <a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">Privacy Policy</a>
</div>
</div>
<InputError class="mt-2" :message="form.errors.terms" />
</InputLabel>
</div>
<div class="flex items-center justify-end mt-4">
<Link :href="route('login')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
Already registered?
</Link>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Register
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
+85
View File
@@ -0,0 +1,85 @@
<script setup>
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const props = defineProps({
email: String,
token: String,
});
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('password.update'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<Head title="Reset Password" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password_confirmation" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Reset Password
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
@@ -0,0 +1,104 @@
<script setup>
import { nextTick, ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const recovery = ref(false);
const form = useForm({
code: '',
recovery_code: '',
});
const recoveryCodeInput = ref(null);
const codeInput = ref(null);
const toggleRecovery = async () => {
recovery.value ^= true;
await nextTick();
if (recovery.value) {
recoveryCodeInput.value.focus();
form.code = '';
} else {
codeInput.value.focus();
form.recovery_code = '';
}
};
const submit = () => {
form.post(route('two-factor.login'));
};
</script>
<template>
<Head title="Two-factor Confirmation" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
<template v-if="! recovery">
Please confirm access to your account by entering the authentication code provided by your authenticator application.
</template>
<template v-else>
Please confirm access to your account by entering one of your emergency recovery codes.
</template>
</div>
<form @submit.prevent="submit">
<div v-if="! recovery">
<InputLabel for="code" value="Code" />
<TextInput
id="code"
ref="codeInput"
v-model="form.code"
type="text"
inputmode="numeric"
class="mt-1 block w-full"
autofocus
autocomplete="one-time-code"
/>
<InputError class="mt-2" :message="form.errors.code" />
</div>
<div v-else>
<InputLabel for="recovery_code" value="Recovery Code" />
<TextInput
id="recovery_code"
ref="recoveryCodeInput"
v-model="form.recovery_code"
type="text"
class="mt-1 block w-full"
autocomplete="one-time-code"
/>
<InputError class="mt-2" :message="form.errors.recovery_code" />
</div>
<div class="flex items-center justify-end mt-4">
<button type="button" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 underline cursor-pointer" @click.prevent="toggleRecovery">
<template v-if="! recovery">
Use a recovery code
</template>
<template v-else>
Use an authentication code
</template>
</button>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Log in
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>
+62
View File
@@ -0,0 +1,62 @@
<script setup>
import { computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
status: String,
});
const form = useForm({});
const submit = () => {
form.post(route('verification.send'));
};
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
</script>
<template>
<Head title="Email Verification" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.
</div>
<div v-if="verificationLinkSent" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
A new verification link has been sent to the email address you provided in your profile settings.
</div>
<form @submit.prevent="submit">
<div class="mt-4 flex items-center justify-between">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Resend Verification Email
</PrimaryButton>
<div>
<Link
:href="route('profile.show')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
>
Edit Profile</Link>
<Link
:href="route('logout')"
method="post"
as="button"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 ms-2"
>
Log Out
</Link>
</div>
</div>
</form>
</AuthenticationCard>
</template>
+304
View File
@@ -0,0 +1,304 @@
require('flatpickr');
import '@fortawesome/fontawesome-free/css/all.min.css';
import Chart from 'chart.js/auto';
import $ from 'jquery';
window.$ = $;
window.jQuery = $;
import select2Factory from 'select2';
import 'select2/dist/css/select2.min.css';
import '../css/wms-select2.css';
if (typeof select2Factory === 'function') {
select2Factory(window, $);
}
const chartColours = [
'rgb(8, 145, 178)',
'rgb(37, 99, 235)',
'rgb(79, 70, 229)',
'rgb(14, 116, 144)',
];
/**
* Parse JSON attribute safely.
*
* @param {HTMLElement} element
* @param {string} attribute
* @param {Array} fallback
* @returns {*}
*/
function parseJsonAttribute(element, attribute, fallback = []) {
try {
return JSON.parse(
element.getAttribute(attribute) || JSON.stringify(fallback)
);
} catch (error) {
return fallback;
}
}
/**
* Render dashboard charts.
*
* @returns {void}
*/
function renderWmsCharts() {
document.querySelectorAll('[data-wms-chart]').forEach((canvas) => {
if (canvas.dataset.rendered === 'true') {
return;
}
const type = canvas.getAttribute('data-wms-chart');
const labels = parseJsonAttribute(canvas, 'data-labels');
const series = parseJsonAttribute(canvas, 'data-series');
const values = parseJsonAttribute(canvas, 'data-values');
const datasets = series.length
? series.map((item, index) => ({
label: item.label,
data: item.data,
backgroundColor:
chartColours[index % chartColours.length],
borderColor:
chartColours[index % chartColours.length],
borderWidth: 2,
borderRadius: 8,
}))
: [
{
label: 'Total',
data: values,
backgroundColor: chartColours,
borderColor: 'rgb(255,255,255)',
borderWidth: 2,
},
];
new Chart(canvas, {
type,
data: {
labels,
datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: document.documentElement.classList.contains(
'dark'
)
? '#e4e4e7'
: '#3f3f46',
boxWidth: 12,
boxHeight: 12,
},
},
},
scales:
type === 'doughnut'
? {}
: {
x: {
ticks: {
color:
document.documentElement.classList.contains(
'dark'
)
? '#d4d4d8'
: '#52525b',
},
grid: {
color:
'rgba(113,113,122,0.18)',
},
},
y: {
beginAtZero: true,
ticks: {
precision: 0,
color:
document.documentElement.classList.contains(
'dark'
)
? '#d4d4d8'
: '#52525b',
},
grid: {
color:
'rgba(113,113,122,0.18)',
},
},
},
},
});
canvas.dataset.rendered = 'true';
});
}
function livewireComponentFor(element) {
let current = element;
while (current && current !== document.documentElement) {
if (current.hasAttribute && current.hasAttribute('wire:id')) {
if (!window.Livewire) {
return null;
}
return window.Livewire.find(current.getAttribute('wire:id'));
}
current = current.parentElement;
}
return null;
}
function setLivewireProperty(element, model, value) {
const component = livewireComponentFor(element);
if (!component || !model) {
return;
}
component.set(model, value === '' ? null : value);
}
function normaliseSelect2Options($select) {
const isMultiple = $select.prop('multiple');
const $field = $select.closest('.wms-select2-field');
if (!isMultiple && $select.find('option[value=""]').length === 0) {
$select.prepend(new Option('', '', false, false));
}
return {
theme: 'wms',
width: '100%',
allowClear: !isMultiple,
closeOnSelect: !isMultiple,
dropdownAutoWidth: false,
minimumResultsForSearch: 0,
dropdownParent: $field.length ? $field : window.jQuery(document.body),
placeholder:
$select.data('placeholder') ||
$select.attr('placeholder') ||
(isMultiple ? 'Select options' : 'Search and select'),
sorter(data) {
return data.sort((first, second) =>
(first.text || '').localeCompare(second.text || '', undefined, {
sensitivity: 'base',
ignorePunctuation: true,
})
);
},
};
}
function select2Selector() {
return 'select[data-wms-select2], select[data-helpdesk-select2], select[data-company-country-select2], select.wms-select2-control';
}
function initialiseSelect2Element(select) {
if (!window.jQuery?.fn?.select2 || !select) {
return false;
}
const $select = window.jQuery(select);
if ($select.hasClass('select2-hidden-accessible')) {
return true;
}
$select.select2(normaliseSelect2Options($select));
$select.off('change.wms-select2-livewire');
$select.on('change.wms-select2-livewire', function () {
setLivewireProperty(this, this.dataset.wireModel, $select.val());
});
$select.off('select2:open.wms-select2-focus');
$select.on('select2:open.wms-select2-focus', function () {
window.setTimeout(() => {
const search = document.querySelector('.select2-container--open .select2-search__field');
if (search) {
search.focus();
}
}, 0);
});
return true;
}
/**
* Initialise all WMS Select2 controls currently on the page.
*
* @returns {void}
*/
function initialiseSelect2() {
if (!window.jQuery?.fn?.select2) {
window.setTimeout(initialiseSelect2, 100);
return;
}
document.querySelectorAll(select2Selector()).forEach(initialiseSelect2Element);
}
/**
* Selects inside Livewire/Navigate content can appear after DOMContentLoaded.
* Initialise on first interaction as a safety net, then open Select2 immediately.
*/
document.addEventListener('mousedown', (event) => {
const select = event.target.closest?.(select2Selector());
if (!select || select.classList.contains('select2-hidden-accessible')) {
return;
}
if (initialiseSelect2Element(select)) {
event.preventDefault();
window.setTimeout(() => window.jQuery(select).select2('open'), 0);
}
}, true);
document.addEventListener('focusin', (event) => {
const select = event.target.closest?.(select2Selector());
if (select) {
initialiseSelect2Element(select);
}
}, true);
/**
* Boot page UI.
*
* @returns {void}
*/
function bootUi() {
renderWmsCharts();
initialiseSelect2();
window.setTimeout(initialiseSelect2, 50);
window.setTimeout(initialiseSelect2, 250);
window.setTimeout(initialiseSelect2, 750);
}
document.addEventListener('DOMContentLoaded', bootUi);
document.addEventListener('livewire:navigated', bootUi);
document.addEventListener('livewire:init', () => {
if (!window.Livewire?.hook) {
return;
}
window.Livewire.hook('morphed', () => {
window.setTimeout(initialiseSelect2, 0);
});
});
+135
View File
@@ -0,0 +1,135 @@
(function () {
'use strict';
function cssVar(name, fallback) {
var value = getComputedStyle(document.documentElement).getPropertyValue(name);
return value && value.trim() !== '' ? value.trim() : fallback;
}
function palette() {
return [
cssVar('--wms-chart-primary', '#0f766e'),
cssVar('--wms-chart-secondary', '#2563eb'),
cssVar('--wms-chart-accent', '#f59e0b'),
cssVar('--wms-chart-danger', '#dc2626'),
cssVar('--wms-chart-muted', '#64748b'),
cssVar('--wms-chart-purple', '#7c3aed')
];
}
function enrichDataset(dataset, index, type) {
var colours = palette();
var colour = colours[index % colours.length];
if (type === 'doughnut' || type === 'pie') {
dataset.backgroundColor = colours;
dataset.borderColor = '#ffffff';
dataset.borderWidth = dataset.borderWidth || 2;
return dataset;
}
dataset.borderColor = colour;
dataset.backgroundColor = type === 'line' ? 'rgba(15, 118, 110, 0.14)' : colour;
dataset.pointBackgroundColor = colour;
dataset.pointBorderColor = '#ffffff';
dataset.pointHoverRadius = 6;
dataset.borderWidth = 2;
return dataset;
}
function optionsFor(type) {
var textColour = cssVar('--wms-chart-text', '#334155');
var gridColour = cssVar('--wms-chart-grid', 'rgba(148, 163, 184, 0.25)');
var options = {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: {
position: type === 'doughnut' || type === 'pie' ? 'bottom' : 'top',
labels: {
color: textColour,
boxWidth: 12,
usePointStyle: true,
padding: 18
}
},
tooltip: {
backgroundColor: 'rgba(15, 23, 42, 0.94)',
padding: 12,
titleColor: '#ffffff',
bodyColor: '#ffffff',
cornerRadius: 10
}
}
};
if (type !== 'doughnut' && type !== 'pie') {
options.scales = {
x: {
ticks: {
color: textColour
},
grid: {
display: false
}
},
y: {
beginAtZero: true,
ticks: {
color: textColour,
precision: 0
},
grid: {
color: gridColour
}
}
};
}
if (type === 'doughnut') {
options.cutout = '68%';
}
return options;
}
function normaliseConfig(rawConfig, type) {
var config = rawConfig || {};
config.labels = config.labels || [];
config.datasets = (config.datasets || []).map(function (dataset, index) {
return enrichDataset(dataset, index, type);
});
return config;
}
document.addEventListener('DOMContentLoaded', function () {
if (typeof Chart === 'undefined') {
return;
}
document.querySelectorAll('[data-wms-chart]').forEach(function (canvas) {
var type = canvas.getAttribute('data-chart-type') || 'line';
var raw = canvas.getAttribute('data-chart-config') || '{}';
var config = {};
try {
config = JSON.parse(raw);
} catch (error) {
config = { labels: [], datasets: [] };
}
new Chart(canvas, {
type: type,
data: normaliseConfig(config, type),
options: optionsFor(type)
});
});
});
}());
+4
View File
@@ -0,0 +1,4 @@
import { Passkeys } from '@laravel/passkeys';
window.Passkeys = Passkeys;
window.dispatchEvent(new CustomEvent('passkeys:ready'));