Repo rebuild
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
:root {
|
||||
--wms-chart-primary: #0f766e;
|
||||
--wms-chart-secondary: #2563eb;
|
||||
--wms-chart-accent: #f59e0b;
|
||||
--wms-chart-danger: #dc2626;
|
||||
--wms-chart-muted: #64748b;
|
||||
--wms-chart-purple: #7c3aed;
|
||||
--wms-chart-text: #334155;
|
||||
--wms-chart-grid: rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
|
||||
.wms-dashboard {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #0f766e 0%, #0f172a 100%);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.18);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
padding: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero::after {
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.18), transparent 62%);
|
||||
content: '';
|
||||
height: 260px;
|
||||
position: absolute;
|
||||
right: -80px;
|
||||
top: -100px;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero__eyebrow {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero__title {
|
||||
font-size: clamp(2rem, 3vw, 3rem);
|
||||
font-weight: 800;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero__text {
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero__badge {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
padding: 10px 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wms-metric-card,
|
||||
.wms-chart-card {
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wms-metric-card__label {
|
||||
color: #64748b;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wms-metric-card__value {
|
||||
color: #0f172a;
|
||||
font-size: 2rem;
|
||||
font-weight: 850;
|
||||
line-height: 1.1;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.wms-metric-card__hint {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.wms-metric-card__icon {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba(15, 118, 110, 0.14), rgba(37, 99, 235, 0.12));
|
||||
border-radius: 18px;
|
||||
color: #0f766e;
|
||||
display: flex;
|
||||
font-size: 1.35rem;
|
||||
height: 56px;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.wms-chart-card__header {
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.22);
|
||||
padding: 22px 24px 12px;
|
||||
}
|
||||
|
||||
.wms-chart-card__eyebrow {
|
||||
color: #0f766e;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wms-chart-card__title {
|
||||
color: #0f172a;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.wms-chart-card__description {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wms-chart-card__canvas {
|
||||
min-height: 260px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.wms-dashboard-hero {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.wms-dashboard-hero__badge {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/* WMS Select2 professional dark theme. Imported after Select2's own CSS. */
|
||||
|
||||
.wms-select2-field .select2-container,
|
||||
.select2-container--wms {
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.wms-select2-field .select2-container {
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
.wms-select2-field select[data-wms-select2].select2-hidden-accessible,
|
||||
.wms-select2-field select.wms-select2-control.select2-hidden-accessible,
|
||||
select[data-wms-select2].select2-hidden-accessible,
|
||||
select.wms-select2-control.select2-hidden-accessible {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0 0 0 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single {
|
||||
height: 3rem !important;
|
||||
min-height: 3rem !important;
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
outline: none !important;
|
||||
transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease !important;
|
||||
}
|
||||
|
||||
.select2-container--wms.select2-container--focus .select2-selection--single,
|
||||
.select2-container--wms.select2-container--open .select2-selection--single {
|
||||
border-color: rgb(59 130 246) !important;
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.24) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__rendered {
|
||||
width: 100% !important;
|
||||
padding: 0 2.75rem 0 1rem !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 500 !important;
|
||||
line-height: 3rem !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__placeholder {
|
||||
color: rgb(161 161 170) !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__arrow {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
right: 0.875rem !important;
|
||||
width: 1.5rem !important;
|
||||
height: 100% !important;
|
||||
display: block !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__arrow b {
|
||||
position: absolute !important;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
margin: 0 !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
border-color: rgb(212 212 216) transparent transparent transparent !important;
|
||||
border-style: solid !important;
|
||||
border-width: 6px 5px 0 5px !important;
|
||||
}
|
||||
|
||||
.select2-container--wms.select2-container--open .select2-selection--single .select2-selection__arrow b {
|
||||
border-color: transparent transparent rgb(212 212 216) transparent !important;
|
||||
border-width: 0 5px 6px 5px !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__clear {
|
||||
position: absolute !important;
|
||||
right: 2.25rem !important;
|
||||
top: 0 !important;
|
||||
height: 100% !important;
|
||||
width: 1.25rem !important;
|
||||
color: rgb(161 161 170) !important;
|
||||
font-size: 1.25rem !important;
|
||||
line-height: 2.85rem !important;
|
||||
text-align: center !important;
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-selection--single .select2-selection__clear:hover {
|
||||
color: rgb(244 244 245) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-dropdown {
|
||||
overflow: hidden !important;
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
border-radius: 0.875rem !important;
|
||||
background: rgb(24 24 27) !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
box-shadow: 0 24px 60px rgb(0 0 0 / 0.45) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-dropdown--below {
|
||||
margin-top: 0.35rem !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-search--dropdown {
|
||||
padding: 0.75rem !important;
|
||||
background: rgb(24 24 27) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-search--dropdown .select2-search__field {
|
||||
height: 2.75rem !important;
|
||||
width: 100% !important;
|
||||
border: 1px solid rgb(63 63 70) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
background: rgb(9 9 11) !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
padding: 0 0.875rem !important;
|
||||
font-size: 0.875rem !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-search--dropdown .select2-search__field:focus {
|
||||
border-color: rgb(59 130 246) !important;
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.22) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-results__options {
|
||||
background: rgb(24 24 27) !important;
|
||||
max-height: 16rem !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-results__option {
|
||||
padding: 0.75rem 1rem !important;
|
||||
color: rgb(244 244 245) !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-results__option--highlighted.select2-results__option--selectable {
|
||||
background: rgb(37 99 235) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-results__option--selected {
|
||||
background: rgb(30 41 59) !important;
|
||||
color: rgb(191 219 254) !important;
|
||||
}
|
||||
|
||||
.select2-container--wms .select2-results__option[aria-disabled='true'],
|
||||
.select2-container--wms .select2-results__message {
|
||||
color: rgb(161 161 170) !important;
|
||||
}
|
||||
|
||||
/* Fallback before Select2 initialises. */
|
||||
.wms-select2-field > select[data-wms-select2],
|
||||
.wms-select2-field > select.wms-select2-control {
|
||||
margin-top: 0.5rem;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgb(63 63 70);
|
||||
background: rgb(9 9 11);
|
||||
color: rgb(244 244 245);
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wms-select2-field > select[data-wms-select2]:focus,
|
||||
.wms-select2-field > select.wms-select2-control:focus {
|
||||
border-color: rgb(59 130 246);
|
||||
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.24);
|
||||
outline: none;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
});
|
||||
}());
|
||||
@@ -0,0 +1,4 @@
|
||||
import { Passkeys } from '@laravel/passkeys';
|
||||
|
||||
window.Passkeys = Passkeys;
|
||||
window.dispatchEvent(new CustomEvent('passkeys:ready'));
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 42" {{ $attributes }}>
|
||||
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M17.2 5.633 8.6.855 0 5.633v26.51l16.2 9 16.2-9v-8.442l7.6-4.223V9.856l-8.6-4.777-8.6 4.777V18.3l-5.6 3.111V5.633ZM38 18.301l-5.6 3.11v-6.157l5.6-3.11V18.3Zm-1.06-7.856-5.54 3.078-5.54-3.079 5.54-3.078 5.54 3.079ZM24.8 18.3v-6.157l5.6 3.111v6.158L24.8 18.3Zm-1 1.732 5.54 3.078-13.14 7.302-5.54-3.078 13.14-7.3v-.002Zm-16.2 7.89 7.6 4.222V38.3L2 30.966V7.92l5.6 3.111v16.892ZM8.6 9.3 3.06 6.222 8.6 3.143l5.54 3.08L8.6 9.3Zm21.8 15.51-13.2 7.334V38.3l13.2-7.334v-6.156ZM9.6 11.034l5.6-3.11v14.6l-5.6 3.11v-14.6Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 683 B |
@@ -0,0 +1,19 @@
|
||||
@props([
|
||||
'sidebar' => false,
|
||||
])
|
||||
|
||||
@if ($sidebar)
|
||||
<flux:sidebar.brand name="Warehouse Management System" {{ $attributes }}>
|
||||
<x-slot name="logo"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
</x-slot>
|
||||
</flux:sidebar.brand>
|
||||
@else
|
||||
<flux:brand name="Warehouse Management System" {{ $attributes }}>
|
||||
<x-slot name="logo"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-md bg-accent-content text-accent-foreground">
|
||||
<x-app-logo-icon class="size-5 fill-current text-white dark:text-black" />
|
||||
</x-slot>
|
||||
</flux:brand>
|
||||
@endif
|
||||
@@ -0,0 +1,6 @@
|
||||
@props(['title', 'description'])
|
||||
|
||||
<div class="flex w-full flex-col text-center">
|
||||
<flux:heading size="xl">{{ $title }}</flux:heading>
|
||||
<flux:subheading>{{ $description }}</flux:subheading>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
@props(['status'])
|
||||
|
||||
@if ($status)
|
||||
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
|
||||
{{ $status }}
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,22 @@
|
||||
@props(['title', 'description' => '', 'type' => 'line', 'config' => [], 'height' => 300, 'icon' => 'fa-chart-line'])
|
||||
|
||||
<div class="card wms-chart-card mb-4 h-100">
|
||||
<div class="card-header wms-chart-card__header">
|
||||
<div>
|
||||
<div class="wms-chart-card__eyebrow">Operational insight</div>
|
||||
<h3 class="wms-chart-card__title">
|
||||
<i class="fas {{ $icon }} me-2"></i>{{ $title }}
|
||||
</h3>
|
||||
@if ($description !== '')
|
||||
<p class="wms-chart-card__description">{{ $description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="wms-chart-card__canvas" style="height: {{ (int) $height }}px;">
|
||||
<canvas data-wms-chart data-chart-type="{{ $type }}" data-chart-config='@json($config)'
|
||||
aria-label="{{ $title }} chart" role="img"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
@props(['label', 'value' => 0, 'hint' => '', 'icon' => 'fa-chart-simple'])
|
||||
|
||||
<div class="col-xl-3 col-md-6">
|
||||
<div class="card wms-metric-card mb-4">
|
||||
<div class="card-body d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div class="wms-metric-card__label">{{ $label }}</div>
|
||||
<div class="wms-metric-card__value">{{ is_numeric($value) ? number_format((int) $value) : $value }}</div>
|
||||
@if ($hint !== '')
|
||||
<div class="wms-metric-card__hint">{{ $hint }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="wms-metric-card__icon">
|
||||
<i class="fas {{ $icon }}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<flux:dropdown position="bottom" align="start">
|
||||
<flux:sidebar.profile :name="auth()->user()->name" :initials="auth()->user()->initials()"
|
||||
icon:trailing="chevrons-up-down" data-test="sidebar-menu-button" />
|
||||
|
||||
<flux:menu>
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||||
<flux:avatar :name="auth()->user()->name" :initials="auth()->user()->initials()" />
|
||||
<div class="grid flex-1 text-start text-sm leading-tight">
|
||||
<flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
|
||||
<flux:text class="truncate">{{ auth()->user()->email }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
|
||||
{{ __('Settings') }}
|
||||
</flux:menu.item>
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle"
|
||||
class="w-full cursor-pointer" data-test="logout-button">
|
||||
{{ __('Log out') }}
|
||||
</flux:menu.item>
|
||||
</form>
|
||||
</flux:menu.radio.group>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
@@ -0,0 +1,7 @@
|
||||
{{--
|
||||
Compatibility component for <x-layouts.app>.
|
||||
|
||||
The real WMS application/sidebar layout lives in resources/views/layouts/app/sidebar.blade.php.
|
||||
Keep this file as a thin delegate only; do not duplicate sidebar markup here.
|
||||
--}}
|
||||
@include('layouts.app.sidebar')
|
||||
@@ -0,0 +1,7 @@
|
||||
{{--
|
||||
Compatibility component for legacy <x-layouts.sidebar> references.
|
||||
|
||||
The real WMS application/sidebar layout lives in resources/views/layouts/app/sidebar.blade.php.
|
||||
Keep this file as a thin delegate only; do not duplicate sidebar markup here.
|
||||
--}}
|
||||
@include('layouts.app.sidebar')
|
||||
@@ -0,0 +1,76 @@
|
||||
@assets
|
||||
@vite('resources/js/passkeys.js')
|
||||
@endassets
|
||||
|
||||
<div x-data="{
|
||||
supported: false,
|
||||
showForm: false,
|
||||
name: '',
|
||||
loading: false,
|
||||
error: null,
|
||||
updateSupport() {
|
||||
this.supported = Boolean(window.Passkeys?.isSupported());
|
||||
},
|
||||
init() {
|
||||
this.updateSupport();
|
||||
|
||||
window.addEventListener('passkeys:ready', () => this.updateSupport(), { once: true });
|
||||
},
|
||||
async register() {
|
||||
if (!this.name.trim()) return;
|
||||
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
await window.Passkeys.register({ name: this.name });
|
||||
this.name = '';
|
||||
this.showForm = false;
|
||||
await $wire.loadPasskeys();
|
||||
} catch (e) {
|
||||
if (e.constructor?.name !== 'UserCancelledError') {
|
||||
this.error = e.message;
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
this.showForm = false;
|
||||
this.name = '';
|
||||
this.error = null;
|
||||
},
|
||||
}">
|
||||
<template x-if="!supported">
|
||||
<flux:text>{{ __('Passkeys are not supported in this browser.') }}</flux:text>
|
||||
</template>
|
||||
|
||||
<template x-if="supported && !showForm">
|
||||
<div>
|
||||
<flux:button variant="primary" icon="plus" x-on:click="showForm = true">
|
||||
{{ __('Add passkey') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="supported && showForm">
|
||||
<div class="space-y-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 p-4">
|
||||
<flux:input label="{{ __('Passkey name') }}" x-model="name"
|
||||
placeholder="{{ __('e.g., MacBook Pro, iPhone') }}" x-on:keydown.enter.prevent="register()"
|
||||
x-ref="passkeyNameInput" x-init="$nextTick(() => $refs.passkeyNameInput?.focus())" />
|
||||
<flux:text class="!mt-1">{{ __('Give this passkey a name to help you identify it later.') }}</flux:text>
|
||||
|
||||
<p x-show="error" x-text="error" x-cloak class="text-sm text-red-600 dark:text-red-400"></p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="primary" x-on:click="register()" x-bind:disabled="loading || !name.trim()">
|
||||
<span x-show="!loading">{{ __('Register passkey') }}</span>
|
||||
<span x-show="loading" x-cloak>{{ __('Registering...') }}</span>
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" x-on:click="cancel()">
|
||||
{{ __('Cancel') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
@props([
|
||||
'optionsRoute' => 'passkey.login-options',
|
||||
'submitRoute' => 'passkey.login',
|
||||
'label' => __('Sign in with a passkey'),
|
||||
'loadingLabel' => __('Authenticating...'),
|
||||
'separator' => __('Or continue with email'),
|
||||
])
|
||||
|
||||
@assets
|
||||
@vite('resources/js/passkeys.js')
|
||||
@endassets
|
||||
|
||||
<div x-data="{
|
||||
supported: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
updateSupport() {
|
||||
this.supported = Boolean(window.Passkeys?.isSupported());
|
||||
},
|
||||
init() {
|
||||
this.updateSupport();
|
||||
|
||||
window.addEventListener('passkeys:ready', () => this.updateSupport(), { once: true });
|
||||
},
|
||||
async verify() {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const response = await window.Passkeys.verify({
|
||||
routes: {
|
||||
options: '{{ route($optionsRoute) }}',
|
||||
submit: '{{ route($submitRoute) }}',
|
||||
},
|
||||
});
|
||||
Livewire.navigate(response.redirect || '/dashboard');
|
||||
} catch (e) {
|
||||
if (e.constructor?.name !== 'UserCancelledError') {
|
||||
this.error = e.message;
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
}">
|
||||
<template x-if="supported">
|
||||
<div>
|
||||
<div class="grid gap-2">
|
||||
<flux:button variant="outline" icon="finger-print" class="w-full" x-on:click="verify()"
|
||||
x-bind:disabled="loading">
|
||||
<span x-show="!loading">{{ $label }}</span>
|
||||
<span x-show="loading" x-cloak>{{ $loadingLabel }}</span>
|
||||
</flux:button>
|
||||
<p x-show="error" x-text="error" x-cloak class="text-sm text-center text-red-600 dark:text-red-400"></p>
|
||||
</div>
|
||||
|
||||
<div class="relative my-6">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-zinc-200 dark:border-zinc-700"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-xs uppercase">
|
||||
<span class="px-2 text-zinc-500 dark:text-zinc-400 bg-white dark:bg-zinc-900">
|
||||
{{ $separator }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
@props([
|
||||
'id' => uniqid(),
|
||||
])
|
||||
|
||||
<svg {{ $attributes }} fill="none">
|
||||
<defs>
|
||||
<pattern id="pattern-{{ $id }}" x="0" y="0" width="8" height="8" patternUnits="userSpaceOnUse">
|
||||
<path d="M-1 5L5 -1M3 9L8.5 3.5" stroke-width="0.5"></path>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect stroke="none" fill="url(#pattern-{{ $id }})" width="100%" height="100%"></rect>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="flex items-start max-md:flex-col">
|
||||
<div class="me-10 w-full pb-4 md:w-[220px]">
|
||||
<flux:navlist aria-label="{{ __('Settings') }}">
|
||||
<flux:navlist.item :href="route('profile.edit')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
|
||||
<flux:navlist.item :href="route('security.edit')" wire:navigate>{{ __('Security') }}</flux:navlist.item>
|
||||
<flux:navlist.item :href="route('appearance.edit')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
|
||||
</flux:navlist>
|
||||
</div>
|
||||
|
||||
<flux:separator class="md:hidden" />
|
||||
|
||||
<div class="flex-1 self-stretch max-md:pt-6">
|
||||
<flux:heading>{{ $heading ?? '' }}</flux:heading>
|
||||
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
|
||||
|
||||
<div class="mt-5 w-full max-w-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
@props([
|
||||
'eyebrow' => 'MPBA WMS',
|
||||
'title',
|
||||
'description' => null,
|
||||
'subtitle' => null,
|
||||
'icon' => null,
|
||||
'stats' => [],
|
||||
])
|
||||
|
||||
@php
|
||||
$heroDescription = $description ?? $subtitle ?? null;
|
||||
@endphp
|
||||
|
||||
<section {{ $attributes->merge(['class' => 'wms-page-hero']) }}>
|
||||
<div class="wms-page-hero-inner">
|
||||
<div class="wms-page-hero-row">
|
||||
<div class="wms-page-hero-copy">
|
||||
<p class="wms-page-hero-kicker">{{ $eyebrow }}</p>
|
||||
<h1 class="wms-page-hero-title">{{ $title }}</h1>
|
||||
|
||||
@if ($heroDescription)
|
||||
<p class="wms-page-hero-description">{{ $heroDescription }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (count($stats) > 0 || $icon || $slot->isNotEmpty())
|
||||
<div class="wms-page-hero-actions">
|
||||
@foreach ($stats as $stat)
|
||||
<article class="wms-page-hero-stat">
|
||||
<span>{{ $stat['label'] ?? '' }}</span>
|
||||
<strong>
|
||||
@if (!empty($stat['icon']))
|
||||
<i class="{{ $stat['icon'] }}" aria-hidden="true"></i>
|
||||
@endif
|
||||
{{ $stat['value'] ?? '' }}
|
||||
</strong>
|
||||
</article>
|
||||
@endforeach
|
||||
|
||||
@if ($icon && count($stats) === 0 && $slot->isEmpty())
|
||||
<article class="wms-page-hero-stat wms-page-hero-icon-card" aria-hidden="true">
|
||||
<strong><i class="{{ $icon }}"></i></strong>
|
||||
</article>
|
||||
@endif
|
||||
|
||||
@if ($slot->isNotEmpty())
|
||||
{{ $slot }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,30 @@
|
||||
<x-layouts::app :title="__('Dashboard')">
|
||||
<x-wms.page-hero eyebrow="Operations" title="Warehouse Command Centre"
|
||||
description="Open inventory, shipping, replenishment, analytics and administration workspaces from one clean operational dashboard."
|
||||
icon="fa-solid fa-gauge-high" />
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<section class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-cyan-600 dark:text-cyan-300">Operations</p>
|
||||
<h2 class="mt-3 text-2xl font-bold text-zinc-900 dark:text-white">Warehouse command centre</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-300">Use the navigation to open inventory,
|
||||
shipping, replenishment, analytics and administration workspaces.</p>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-emerald-600 dark:text-emerald-300">Health
|
||||
</p>
|
||||
<h2 class="mt-3 text-2xl font-bold text-zinc-900 dark:text-white">System OK</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-300">Core platform services are ready for
|
||||
module integration, testing and tenant configuration.</p>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="rounded-2xl border border-zinc-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-sky-600 dark:text-sky-300">Modules</p>
|
||||
<h2 class="mt-3 text-2xl font-bold text-zinc-900 dark:text-white">Enabled workspaces</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-zinc-600 dark:text-zinc-300">Placeholder modules are wired into the
|
||||
sidebar and ready for production controllers.</p>
|
||||
</section>
|
||||
</div>
|
||||
</x-layouts::app>
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Unauthorized'))
|
||||
@section('code', '401')
|
||||
@section('message', __('Unauthorized'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Payment Required'))
|
||||
@section('code', '402')
|
||||
@section('message', __('Payment Required'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Forbidden'))
|
||||
@section('code', '403')
|
||||
@section('message', __($exception->getMessage() ?: 'Forbidden'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Not Found'))
|
||||
@section('code', '404')
|
||||
@section('message', __('Not Found'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Page Expired'))
|
||||
@section('code', '419')
|
||||
@section('message', __('Page Expired'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Too Many Requests'))
|
||||
@section('code', '429')
|
||||
@section('message', __('Too Many Requests'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Server Error'))
|
||||
@section('code', '500')
|
||||
@section('message', __('Server Error'))
|
||||
@@ -0,0 +1,5 @@
|
||||
@extends('errors::minimal')
|
||||
|
||||
@section('title', __('Service Unavailable'))
|
||||
@section('code', '503')
|
||||
@section('message', __('Service Unavailable'))
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title')</title>
|
||||
|
||||
<!-- Styles -->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
background-color: #fff;
|
||||
color: #636b6f;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-weight: 100;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.position-ref {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<div class="flex-center position-ref full-height">
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
@yield('message')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,554 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title')</title>
|
||||
|
||||
<style>
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
html {
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: transparent
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
*,
|
||||
:after,
|
||||
:before {
|
||||
box-sizing: border-box;
|
||||
border: 0 solid #e2e8f0
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace
|
||||
}
|
||||
|
||||
svg,
|
||||
video {
|
||||
display: block;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--bg-opacity: 1;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #f7fafc;
|
||||
background-color: rgba(247, 250, 252, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--border-opacity: 1;
|
||||
border-color: #edf2f7;
|
||||
border-color: rgba(237, 242, 247, var(--border-opacity))
|
||||
}
|
||||
|
||||
.border-gray-400 {
|
||||
--border-opacity: 1;
|
||||
border-color: #cbd5e0;
|
||||
border-color: rgba(203, 213, 224, var(--border-opacity))
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px
|
||||
}
|
||||
|
||||
.border-r {
|
||||
border-right-width: 1px
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem
|
||||
}
|
||||
|
||||
.h-8 {
|
||||
height: 2rem
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: .875rem
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem
|
||||
}
|
||||
|
||||
.leading-7 {
|
||||
line-height: 1.75rem
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: .25rem
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: .5rem
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: .5rem
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: .5rem
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem
|
||||
}
|
||||
|
||||
.ml-12 {
|
||||
margin-left: 3rem
|
||||
}
|
||||
|
||||
.-mt-px {
|
||||
margin-top: -1px
|
||||
}
|
||||
|
||||
.max-w-xl {
|
||||
max-width: 36rem
|
||||
}
|
||||
|
||||
.max-w-6xl {
|
||||
max-width: 72rem
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem
|
||||
}
|
||||
|
||||
.py-4 {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem
|
||||
}
|
||||
|
||||
.px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem
|
||||
}
|
||||
|
||||
.pt-8 {
|
||||
padding-top: 2rem
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
--text-opacity: 1;
|
||||
color: #edf2f7;
|
||||
color: rgba(237, 242, 247, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-300 {
|
||||
--text-opacity: 1;
|
||||
color: #e2e8f0;
|
||||
color: rgba(226, 232, 240, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--text-opacity: 1;
|
||||
color: #cbd5e0;
|
||||
color: rgba(203, 213, 224, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
--text-opacity: 1;
|
||||
color: #a0aec0;
|
||||
color: rgba(160, 174, 192, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
--text-opacity: 1;
|
||||
color: #718096;
|
||||
color: rgba(113, 128, 150, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
--text-opacity: 1;
|
||||
color: #4a5568;
|
||||
color: rgba(74, 85, 104, var(--text-opacity))
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
--text-opacity: 1;
|
||||
color: #1a202c;
|
||||
color: rgba(26, 32, 44, var(--text-opacity))
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
.antialiased {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
}
|
||||
|
||||
.tracking-wider {
|
||||
letter-spacing: .05em
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem
|
||||
}
|
||||
|
||||
.w-8 {
|
||||
width: 2rem
|
||||
}
|
||||
|
||||
.w-auto {
|
||||
width: auto
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr))
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes ping {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
75%,
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ping {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
75%,
|
||||
to {
|
||||
transform: scale(2);
|
||||
opacity: 0
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
|
||||
0%,
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
to {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .5
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes bounce {
|
||||
|
||||
0%,
|
||||
to {
|
||||
transform: translateY(-25%);
|
||||
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
|
||||
0%,
|
||||
to {
|
||||
transform: translateY(-25%);
|
||||
-webkit-animation-timing-function: cubic-bezier(.8, 0, 1, 1);
|
||||
animation-timing-function: cubic-bezier(.8, 0, 1, 1)
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
-webkit-animation-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
animation-timing-function: cubic-bezier(0, 0, .2, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:640px) {
|
||||
.sm\:rounded-lg {
|
||||
border-radius: .5rem
|
||||
}
|
||||
|
||||
.sm\:block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.sm\:items-center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.sm\:justify-start {
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
.sm\:justify-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.sm\:h-20 {
|
||||
height: 5rem
|
||||
}
|
||||
|
||||
.sm\:ml-0 {
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
.sm\:px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem
|
||||
}
|
||||
|
||||
.sm\:pt-0 {
|
||||
padding-top: 0
|
||||
}
|
||||
|
||||
.sm\:text-left {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.sm\:text-right {
|
||||
text-align: right
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:768px) {
|
||||
.md\:border-t-0 {
|
||||
border-top-width: 0
|
||||
}
|
||||
|
||||
.md\:border-l {
|
||||
border-left-width: 1px
|
||||
}
|
||||
|
||||
.md\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr))
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:1024px) {
|
||||
.lg\:px-8 {
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme:dark) {
|
||||
.dark\:bg-gray-800 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #2d3748;
|
||||
background-color: rgba(45, 55, 72, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.dark\:bg-gray-900 {
|
||||
--bg-opacity: 1;
|
||||
background-color: #1a202c;
|
||||
background-color: rgba(26, 32, 44, var(--bg-opacity))
|
||||
}
|
||||
|
||||
.dark\:border-gray-700 {
|
||||
--border-opacity: 1;
|
||||
border-color: #4a5568;
|
||||
border-color: rgba(74, 85, 104, var(--border-opacity))
|
||||
}
|
||||
|
||||
.dark\:text-white {
|
||||
--text-opacity: 1;
|
||||
color: #fff;
|
||||
color: rgba(255, 255, 255, var(--text-opacity))
|
||||
}
|
||||
|
||||
.dark\:text-gray-300 {
|
||||
--text-opacity: 1;
|
||||
color: #e2e8f0;
|
||||
color: rgba(226, 232, 240, var(--text-opacity))
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-white text-black">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<div class="relative flex items-top justify-center min-h-screen bg-white text-black sm:items-center sm:pt-0"
|
||||
role="main">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="flex items-center pt-8 sm:justify-start sm:pt-0">
|
||||
<h1 class="px-4 text-lg text-black border-r border-gray-400">
|
||||
@yield('code')
|
||||
</h1>
|
||||
|
||||
<div class="ml-4 text-lg text-black">
|
||||
@yield('message')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg {{ $attributes->class($classes) }} data-flux-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path d="M12 7v14" />
|
||||
<path d="M16 12h2" />
|
||||
<path d="M16 8h2" />
|
||||
<path
|
||||
d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
|
||||
<path d="M6 12h2" />
|
||||
<path d="M6 8h2" />
|
||||
</svg>
|
||||
@@ -0,0 +1,33 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg {{ $attributes->class($classes) }} data-flux-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path d="m7 15 5 5 5-5" />
|
||||
<path d="m7 9 5-5 5 5" />
|
||||
</svg>
|
||||
@@ -0,0 +1,35 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg {{ $attributes->class($classes) }} data-flux-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v5" />
|
||||
<circle cx="13" cy="12" r="2" />
|
||||
<path d="M18 19c-2.8 0-5-2.2-5-5v8" />
|
||||
<circle cx="20" cy="19" r="2" />
|
||||
</svg>
|
||||
@@ -0,0 +1,35 @@
|
||||
{{-- Credit: Lucide (https://lucide.dev) --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'outline',
|
||||
])
|
||||
|
||||
@php
|
||||
if ($variant === 'solid') {
|
||||
throw new \Exception('The "solid" variant is not supported in Lucide.');
|
||||
}
|
||||
|
||||
$classes = Flux::classes('shrink-0')->add(
|
||||
match ($variant) {
|
||||
'outline' => '[:where(&)]:size-6',
|
||||
'solid' => '[:where(&)]:size-6',
|
||||
'mini' => '[:where(&)]:size-5',
|
||||
'micro' => '[:where(&)]:size-4',
|
||||
},
|
||||
);
|
||||
|
||||
$strokeWidth = match ($variant) {
|
||||
'outline' => 2,
|
||||
'mini' => 2.25,
|
||||
'micro' => 2.5,
|
||||
};
|
||||
@endphp
|
||||
|
||||
<svg {{ $attributes->class($classes) }} data-flux-icon xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="{{ $strokeWidth }}" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
@@ -0,0 +1,46 @@
|
||||
@props([
|
||||
'expandable' => false,
|
||||
'expanded' => true,
|
||||
'heading' => null,
|
||||
])
|
||||
|
||||
<?php if ($expandable && $heading): ?>
|
||||
|
||||
<ui-disclosure {{ $attributes->class('group/disclosure') }} @if ($expanded === true) open @endif
|
||||
data-flux-navlist-group>
|
||||
<button type="button"
|
||||
class="group/disclosure-button mb-[2px] flex h-10 w-full items-center rounded-lg text-zinc-500 hover:bg-zinc-800/5 hover:text-zinc-800 lg:h-8 dark:text-white/80 dark:hover:bg-white/[7%] dark:hover:text-white">
|
||||
<div class="ps-3 pe-4">
|
||||
<flux:icon.chevron-down class="hidden size-3! group-data-open/disclosure-button:block" />
|
||||
<flux:icon.chevron-right class="block size-3! group-data-open/disclosure-button:hidden" />
|
||||
</div>
|
||||
|
||||
<span class="text-sm font-medium leading-none">{{ $heading }}</span>
|
||||
</button>
|
||||
|
||||
<div class="relative hidden space-y-[2px] ps-7 data-open:block" @if ($expanded === true) data-open @endif>
|
||||
<div class="absolute inset-y-[3px] start-0 ms-4 w-px bg-zinc-200 dark:bg-white/30"></div>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</ui-disclosure>
|
||||
|
||||
<?php elseif ($heading): ?>
|
||||
|
||||
<div {{ $attributes->class('block space-y-[2px]') }}>
|
||||
<div class="px-1 py-2">
|
||||
<div class="text-xs leading-none text-zinc-400">{{ $heading }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div {{ $attributes->class('block space-y-[2px]') }}>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
@@ -0,0 +1,11 @@
|
||||
@php
|
||||
$helpdeskManifestPath = public_path('build/helpdesk/manifest.json');
|
||||
@endphp
|
||||
|
||||
@if (request()->routeIs('helpdesk.*') && file_exists($helpdeskManifestPath))
|
||||
@vite(['Resources/assets/js/app.js', 'Resources/assets/css/app.css'], 'build/helpdesk')
|
||||
@endif
|
||||
|
||||
<x-layouts::app.sidebar :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::app.sidebar>
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
|
||||
@include('partials.head')
|
||||
|
||||
<body class="min-h-screen bg-white dark:bg-zinc-800">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.toggle class="lg:hidden mr-2" icon="bars-2" inset="left" />
|
||||
|
||||
<x-app-logo href="{{ route('dashboard') }}" wire:navigate />
|
||||
|
||||
<flux:navbar class="-mb-px max-lg:hidden">
|
||||
<flux:navbar.item icon="layout-grid" :href="route('dashboard')" :current="request()->routeIs('dashboard')"
|
||||
wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:navbar.item>
|
||||
</flux:navbar>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:navbar class="me-1.5 space-x-0.5 rtl:space-x-reverse py-0!">
|
||||
<flux:tooltip :content="__('Search')" position="bottom">
|
||||
<flux:navbar.item class="!h-10 [&>div>svg]:size-5" icon="magnifying-glass" href="#"
|
||||
:label="__('Search')" />
|
||||
</flux:tooltip>
|
||||
<flux:tooltip :content="__('Repository')" position="bottom">
|
||||
<flux:navbar.item class="h-10 max-lg:hidden [&>div>svg]:size-5" icon="folder-git-2"
|
||||
href="https://github.com/laravel/livewire-starter-kit" target="_blank" :label="__('Repository')" />
|
||||
</flux:tooltip>
|
||||
<flux:tooltip :content="__('Documentation')" position="bottom">
|
||||
<flux:navbar.item class="h-10 max-lg:hidden [&>div>svg]:size-5" icon="book-open-text"
|
||||
href="https://laravel.com/docs/starter-kits#livewire" target="_blank"
|
||||
:label="__('Documentation')" />
|
||||
</flux:tooltip>
|
||||
</flux:navbar>
|
||||
|
||||
<x-desktop-user-menu />
|
||||
</flux:header>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<flux:sidebar collapsible="mobile" sticky
|
||||
class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
|
||||
<flux:sidebar.header>
|
||||
<x-app-logo :sidebar="true" href="{{ route('dashboard') }}" wire:navigate />
|
||||
<flux:sidebar.collapse
|
||||
class="in-data-flux-sidebar-on-desktop:not-in-data-flux-sidebar-collapsed-desktop:-mr-2" />
|
||||
</flux:sidebar.header>
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.group :heading="__('Platform')">
|
||||
<flux:sidebar.item icon="layout-grid" :href="route('dashboard')"
|
||||
:current="request()->routeIs('dashboard')" wire:navigate>
|
||||
{{ __('Dashboard') }}
|
||||
</flux:sidebar.item>
|
||||
</flux:sidebar.group>
|
||||
</flux:sidebar.nav>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
<flux:sidebar.nav>
|
||||
<flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit"
|
||||
target="_blank">
|
||||
{{ __('Repository') }}
|
||||
</flux:sidebar.item>
|
||||
<flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire"
|
||||
target="_blank">
|
||||
{{ __('Documentation') }}
|
||||
</flux:sidebar.item>
|
||||
</flux:sidebar.nav>
|
||||
</flux:sidebar>
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@persist('toast')
|
||||
<flux:toast.group>
|
||||
<flux:toast />
|
||||
</flux:toast.group>
|
||||
@endpersist
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,913 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
@include('partials.head')
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const SIDEBAR_STATE_KEY = 'wms-sidebar-state';
|
||||
const SIDEBAR_WIDTH_KEY = 'wms-sidebar-width';
|
||||
|
||||
const DEFAULT_WIDTH = 288;
|
||||
const MIN_WIDTH = 224;
|
||||
const MAX_WIDTH = 448;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
const savedWidth = Number.parseInt(localStorage.getItem(SIDEBAR_WIDTH_KEY) || DEFAULT_WIDTH, 10);
|
||||
const width = clamp(Number.isFinite(savedWidth) ? savedWidth : DEFAULT_WIDTH, MIN_WIDTH, MAX_WIDTH);
|
||||
const savedState = localStorage.getItem(SIDEBAR_STATE_KEY) || 'expanded';
|
||||
|
||||
document.documentElement.style.setProperty('--wms-sidebar-width', `${width}px`);
|
||||
document.documentElement.dataset.wmsSidebar = savedState === 'collapsed' ? 'collapsed' : 'expanded';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<body class="min-h-screen bg-white font-sans text-zinc-950 antialiased dark:bg-zinc-950 dark:text-zinc-100">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
|
||||
@vite(['resources/css/app.css'])
|
||||
|
||||
<div class="wms-app-shell" data-wms-shell data-wms-mobile-shell>
|
||||
<button type="button" class="wms-mobile-overlay" data-wms-mobile-close
|
||||
aria-label="{{ __('Close navigation menu') }}"></button>
|
||||
|
||||
<aside id="wms-main-sidebar" class="wms-sidebar" data-wms-sidebar aria-label="{{ __('Main navigation') }}">
|
||||
<div class="wms-sidebar-header">
|
||||
<a href="{{ auth()->user()?->isMpbaPlatformAdmin() && Route::has('platform.dashboard') ? route('platform.dashboard') : route('dashboard') }}"
|
||||
wire:navigate class="wms-brand" aria-label="{{ __('Warehouse Management System') }}">
|
||||
<span class="wms-brand-mark" data-wms-brand-toggle title="{{ __('Expand sidebar') }}">
|
||||
<span class="wms-brand-mark-inner">W</span>
|
||||
</span>
|
||||
|
||||
<span class="wms-brand-copy" data-wms-sidebar-label>
|
||||
<span class="wms-brand-title">{{ __('Warehouse Management System') }}</span>
|
||||
<span class="wms-brand-subtitle">MPBA</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<button type="button" class="wms-sidebar-toggle" data-wms-sidebar-toggle
|
||||
aria-label="{{ __('Collapse sidebar') }}" aria-expanded="true">
|
||||
<span class="wms-toggle-expanded">‹</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="wms-mobile-close" data-wms-mobile-close
|
||||
aria-label="{{ __('Close navigation') }}">×</button>
|
||||
</div>
|
||||
|
||||
<nav class="wms-sidebar-nav" aria-label="{{ __('Main navigation') }}">
|
||||
@php
|
||||
$platformRoute =
|
||||
auth()->user()?->isMpbaPlatformAdmin() && Route::has('platform.dashboard')
|
||||
? 'platform.dashboard'
|
||||
: (Route::has('dashboard')
|
||||
? 'dashboard'
|
||||
: null);
|
||||
@endphp
|
||||
|
||||
@if ($platformRoute)
|
||||
<div class="wms-sidebar-group" data-wms-menu-group="platform"
|
||||
@if (request()->routeIs('platform.dashboard') || request()->routeIs('dashboard')) data-wms-selected="true" data-wms-has-active="true" @endif>
|
||||
<a href="{{ route($platformRoute) }}" wire:navigate
|
||||
class="wms-sidebar-heading wms-sidebar-heading-button wms-sidebar-primary-link"
|
||||
title="{{ __('Platform') }}" data-wms-sidebar-label>
|
||||
<i class="fa-solid fa-screwdriver-wrench wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Platform') }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="wms-sidebar-group" data-wms-menu-group="operations">
|
||||
<button type="button" class="wms-sidebar-heading wms-sidebar-heading-button" data-wms-menu-toggle
|
||||
data-wms-sidebar-label aria-expanded="false">
|
||||
<span class="wms-sidebar-heading-content">
|
||||
<i class="fa-solid fa-chart-simple wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Operations') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="wms-sidebar-group-items" data-wms-menu-items>
|
||||
@if (Route::has('analytics.index'))
|
||||
<a href="{{ route('analytics.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('analytics.*')) is-active @endif"
|
||||
title="{{ __('Analytics') }}">
|
||||
<i class="fa-solid fa-chart-line wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Analytics') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('inventory-management.index'))
|
||||
<a href="{{ route('inventory-management.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('inventory-management.*')) is-active @endif"
|
||||
title="{{ __('Inventory') }}">
|
||||
<i class="fa-solid fa-boxes-stacked wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Inventory') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('replenishment.index'))
|
||||
<a href="{{ route('replenishment.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('replenishment.*')) is-active @endif"
|
||||
title="{{ __('Replenishment') }}">
|
||||
<i class="fa-solid fa-arrows-rotate wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Replenishment') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('shipping.index'))
|
||||
<a href="{{ route('shipping.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('shipping.*')) is-active @endif"
|
||||
title="{{ __('Shipping') }}">
|
||||
<i class="fa-solid fa-truck-fast wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Shipping') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('transportation.index'))
|
||||
<a href="{{ route('transportation.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('transportation.*')) is-active @endif"
|
||||
title="{{ __('Transportation') }}">
|
||||
<i class="fa-solid fa-truck-ramp-box wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Transportation') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('carrier-management.index'))
|
||||
<a href="{{ route('carrier-management.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('carrier-management.*')) is-active @endif"
|
||||
title="{{ __('Carriers') }}">
|
||||
<i class="fa-solid fa-route wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Carriers') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$helpdeskEnabled = Route::has('helpdesk.index') || Route::has('helpdesk.tickets.index');
|
||||
|
||||
$helpdeskSettingsRoute = Route::has('helpdesk.settings')
|
||||
? 'helpdesk.settings'
|
||||
: (Route::has('helpdesk.settings.index')
|
||||
? 'helpdesk.settings.index'
|
||||
: null);
|
||||
|
||||
$helpdeskGitRoute = Route::has('helpdesk.git.index') ? 'helpdesk.git.index' : null;
|
||||
|
||||
$helpdeskWorkflowRoute = Route::has('helpdesk.workflow-templates.index')
|
||||
? 'helpdesk.workflow-templates.index'
|
||||
: null;
|
||||
|
||||
$documentationEnabled =
|
||||
Route::has('documentation.index') || Route::has('documentation.documents.index');
|
||||
|
||||
$documentationRoute = Route::has('documentation.index')
|
||||
? 'documentation.index'
|
||||
: (Route::has('documentation.documents.index')
|
||||
? 'documentation.documents.index'
|
||||
: null);
|
||||
@endphp
|
||||
|
||||
@if ($helpdeskEnabled || $documentationEnabled)
|
||||
<div class="wms-sidebar-group" data-wms-menu-group="support">
|
||||
@if (Route::has('helpdesk.index'))
|
||||
<a href="{{ route('helpdesk.index') }}" wire:navigate
|
||||
class="wms-sidebar-heading wms-sidebar-heading-button @if (request()->routeIs('helpdesk.*')) is-active @endif"
|
||||
data-wms-sidebar-label title="{{ __('Support') }}">
|
||||
<span class="wms-sidebar-heading-content">
|
||||
<i class="fa-solid fa-headset wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Support') }}</span>
|
||||
</span>
|
||||
</a>
|
||||
@else
|
||||
<button type="button" class="wms-sidebar-heading wms-sidebar-heading-button"
|
||||
data-wms-menu-toggle data-wms-sidebar-label aria-expanded="false">
|
||||
<span class="wms-sidebar-heading-content">
|
||||
<i class="fa-solid fa-headset wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Support') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<div class="wms-sidebar-group-items mt-2 space-y-1" data-wms-menu-items>
|
||||
@if ($helpdeskGitRoute)
|
||||
<a href="{{ route($helpdeskGitRoute) }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('helpdesk.git.*')) is-active @endif"
|
||||
title="{{ __('Git Tracking') }}">
|
||||
<i class="fa-solid fa-code-commit wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Git Tracking') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($helpdeskWorkflowRoute)
|
||||
<a href="{{ route($helpdeskWorkflowRoute) }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('helpdesk.workflow-templates.*')) is-active @endif"
|
||||
title="{{ __('Workflow Emails') }}">
|
||||
<i class="fa-solid fa-envelope-circle-check wms-sidebar-icon"
|
||||
aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Workflow Emails') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($documentationRoute)
|
||||
<a href="{{ route($documentationRoute) }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('documentation.*')) is-active @endif"
|
||||
title="{{ __('Documentation') }}">
|
||||
<i class="fa-solid fa-book-open wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Documentation') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if ($helpdeskSettingsRoute)
|
||||
<a href="{{ route($helpdeskSettingsRoute) }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('helpdesk.settings') || request()->routeIs('helpdesk.settings.index')) is-active @endif"
|
||||
title="{{ __('Helpdesk Settings') }}">
|
||||
<i class="fa-solid fa-gears wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Settings') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="wms-sidebar-group" data-wms-menu-group="integration-reporting">
|
||||
<button type="button" class="wms-sidebar-heading wms-sidebar-heading-button" data-wms-menu-toggle
|
||||
data-wms-sidebar-label aria-expanded="false">
|
||||
<span class="wms-sidebar-heading-content">
|
||||
<i class="fa-solid fa-plug-circle-bolt wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Integration & Reporting') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="wms-sidebar-group-items" data-wms-menu-items>
|
||||
@if (Route::has('m3-integration.index'))
|
||||
<a href="{{ route('m3-integration.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('m3-integration.*')) is-active @endif"
|
||||
title="{{ __('M3 Integration') }}">
|
||||
<i class="fa-solid fa-plug-circle-bolt wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('M3 Integration') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('sentinel-integration.index'))
|
||||
<a href="{{ route('sentinel-integration.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('sentinel-integration.*')) is-active @endif"
|
||||
title="{{ __('Sentinel') }}">
|
||||
<i class="fa-solid fa-satellite-dish wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Sentinel') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('document-archive.index'))
|
||||
<a href="{{ route('document-archive.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('document-archive.*')) is-active @endif"
|
||||
title="{{ __('Documents') }}">
|
||||
<i class="fa-solid fa-box-archive wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Documents') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('reporting.index'))
|
||||
<a href="{{ route('reporting.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('reporting.*')) is-active @endif"
|
||||
title="{{ __('Reporting') }}">
|
||||
<i class="fa-solid fa-file-lines wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Reporting') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (auth()->user()?->isMpbaPlatformAdmin())
|
||||
<div class="wms-sidebar-group" data-wms-menu-group="platform-administration">
|
||||
<button type="button" class="wms-sidebar-heading wms-sidebar-heading-button"
|
||||
data-wms-menu-toggle data-wms-sidebar-label aria-expanded="false">
|
||||
<span class="wms-sidebar-heading-content">
|
||||
<i class="fa-solid fa-building-shield wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span>{{ __('Platform Administration') }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="wms-sidebar-group-items" data-wms-menu-items>
|
||||
@if (Route::has('platform.companies.index'))
|
||||
<a href="{{ route('platform.companies.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('platform.companies.*')) is-active @endif"
|
||||
title="{{ __('Companies') }}">
|
||||
<i class="fa-solid fa-building wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Companies') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('platform.licenses.index'))
|
||||
<a href="{{ route('platform.licenses.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('platform.licenses.*')) is-active @endif"
|
||||
title="{{ __('Licences') }}">
|
||||
<i class="fa-solid fa-key wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Licences') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('platform.payments.index'))
|
||||
<a href="{{ route('platform.payments.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('platform.payments.*')) is-active @endif"
|
||||
title="{{ __('Payments') }}">
|
||||
<i class="fa-solid fa-credit-card wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Payments') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('countries.index'))
|
||||
<a href="{{ route('countries.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('countries.*')) is-active @endif"
|
||||
title="{{ __('Countries') }}">
|
||||
<i class="fa-solid fa-globe wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Countries') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('warehouses.index'))
|
||||
<a href="{{ route('warehouses.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('warehouses.*')) is-active @endif"
|
||||
title="{{ __('Warehouses') }}">
|
||||
<i class="fa-solid fa-warehouse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Warehouses') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('warehouse-rules.index'))
|
||||
<a href="{{ route('warehouse-rules.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('warehouse-rules.*')) is-active @endif"
|
||||
title="{{ __('Validation Rules') }}">
|
||||
<i class="fa-solid fa-shield-halved wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Validation Rules') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (auth()->user()?->isMpbaPlatformAdmin() && Route::has('platform.prices.index'))
|
||||
<a href="{{ route('platform.prices.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('platform.prices.*')) is-active @endif"
|
||||
title="{{ __('Prices') }}">
|
||||
<i class="fa-solid fa-tags wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Prices') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('configuration.index'))
|
||||
<a href="{{ route('configuration.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('configuration.*')) is-active @endif"
|
||||
title="{{ __('Configuration') }}">
|
||||
<i class="fa-solid fa-sliders wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Configuration') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('security.index'))
|
||||
<a href="{{ route('security.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('security.*')) is-active @endif"
|
||||
title="{{ __('Security') }}">
|
||||
<i class="fa-solid fa-user-shield wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Security') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('audit.index'))
|
||||
<a href="{{ route('audit.index') }}" wire:navigate
|
||||
class="wms-sidebar-link @if (request()->routeIs('audit.*')) is-active @endif"
|
||||
title="{{ __('Audit') }}">
|
||||
<i class="fa-solid fa-clipboard-check wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Audit') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
@if (Route::has('modules.control.index'))
|
||||
|
||||
<a href="{{ route('modules.control.index') }}" wire:navigate
|
||||
|
||||
class="wms-sidebar-link @if (request()->routeIs('modules.control.*')) is-active @endif"
|
||||
|
||||
title="{{ __('Module Control') }}">
|
||||
|
||||
<i class="fa-solid fa-puzzle-piece wms-sidebar-icon" aria-hidden="true"></i>
|
||||
|
||||
<span data-wms-sidebar-label>{{ __('Module Control') }}</span>
|
||||
|
||||
</a>
|
||||
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</nav>
|
||||
<button type="button" class="wms-sidebar-resizer" data-wms-sidebar-resizer
|
||||
aria-label="{{ __('Resize sidebar') }}" title="{{ __('Drag to resize sidebar') }}"></button>
|
||||
</aside>
|
||||
|
||||
<div class="wms-main-panel">
|
||||
@include('layouts.partials.topbar')
|
||||
|
||||
<main class="wms-content">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wms-toast-stack"
|
||||
class="fixed right-4 top-4 z-[9999] flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-3 font-sans"
|
||||
aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<script>
|
||||
window.wmsToast = function(options) {
|
||||
const stack = document.getElementById('wms-toast-stack');
|
||||
|
||||
if (!stack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: options.type || 'info',
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
timeout: Number(options.timeout || 4500),
|
||||
};
|
||||
|
||||
const palette = {
|
||||
success: 'border-emerald-400/40 bg-emerald-950/95 text-emerald-50',
|
||||
error: 'border-red-400/40 bg-red-950/95 text-red-50',
|
||||
warning: 'border-amber-400/40 bg-amber-950/95 text-amber-50',
|
||||
info: 'border-blue-400/40 bg-blue-950/95 text-blue-50',
|
||||
};
|
||||
|
||||
const icon = {
|
||||
success: 'fa-circle-check',
|
||||
error: 'fa-circle-xmark',
|
||||
warning: 'fa-triangle-exclamation',
|
||||
info: 'fa-circle-info',
|
||||
};
|
||||
|
||||
const toast = document.createElement('div');
|
||||
|
||||
toast.className = [
|
||||
'wms-toast pointer-events-auto select-none rounded-2xl border px-4 py-3 font-sans shadow-2xl backdrop-blur',
|
||||
'transition duration-200 ease-out translate-y-0 opacity-100',
|
||||
palette[config.type] || palette.info,
|
||||
].join(' ');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fa-solid ${icon[config.type] || icon.info} mt-0.5 text-lg" aria-hidden="true"></i>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
${config.title ? `<div class="text-sm font-bold">${config.title}</div>` : ''}
|
||||
<div class="text-sm leading-5">${config.message}</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="ml-2 text-white/70 hover:text-white" aria-label="Close notification">
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const close = function() {
|
||||
toast.classList.add('translate-y-[-0.35rem]', 'opacity-0');
|
||||
|
||||
window.setTimeout(function() {
|
||||
toast.remove();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
toast.querySelector('button').addEventListener('click', close);
|
||||
|
||||
stack.appendChild(toast);
|
||||
|
||||
if (config.timeout > 0) {
|
||||
window.setTimeout(close, config.timeout);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('wms:toast', function(event) {
|
||||
window.wmsToast(event.detail || {});
|
||||
});
|
||||
|
||||
function showSessionToasts() {
|
||||
@if (session('success'))
|
||||
window.wmsToast({
|
||||
type: 'success',
|
||||
title: @json(__('Success')),
|
||||
message: @json(session('success')),
|
||||
});
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
window.wmsToast({
|
||||
type: 'error',
|
||||
title: @json(__('Error')),
|
||||
message: @json(session('error')),
|
||||
});
|
||||
@endif
|
||||
|
||||
@if (session('warning'))
|
||||
window.wmsToast({
|
||||
type: 'warning',
|
||||
title: @json(__('Warning')),
|
||||
message: @json(session('warning')),
|
||||
});
|
||||
@endif
|
||||
|
||||
@if (session('info'))
|
||||
window.wmsToast({
|
||||
type: 'info',
|
||||
title: @json(__('Information')),
|
||||
message: @json(session('info')),
|
||||
});
|
||||
@endif
|
||||
}
|
||||
|
||||
window.__wmsToastSessionKeys = window.__wmsToastSessionKeys || new Set();
|
||||
|
||||
window.showSessionToastsOnce = function () {
|
||||
const key = [
|
||||
@json(session('success')),
|
||||
@json(session('error')),
|
||||
@json(session('warning')),
|
||||
@json(session('info')),
|
||||
].filter(Boolean).join('|');
|
||||
|
||||
if (!key || window.__wmsToastSessionKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.__wmsToastSessionKeys.add(key);
|
||||
window.showSessionToasts();
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', showSessionToastsOnce);
|
||||
document.addEventListener('livewire:navigated', showSessionToastsOnce);
|
||||
</script>
|
||||
|
||||
@livewireScripts
|
||||
@fluxScripts
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const SIDEBAR_STATE_KEY = 'wms-sidebar-state';
|
||||
const SIDEBAR_WIDTH_KEY = 'wms-sidebar-width';
|
||||
const PLATFORM_FIRST_OPEN_KEY = 'wms-platform-first-opened';
|
||||
|
||||
const DEFAULT_WIDTH = 288;
|
||||
const MIN_WIDTH = 224;
|
||||
const MAX_WIDTH = 448;
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function getSavedSidebarWidth() {
|
||||
const savedWidth = Number.parseInt(localStorage.getItem(SIDEBAR_WIDTH_KEY) || DEFAULT_WIDTH, 10);
|
||||
|
||||
return clamp(Number.isFinite(savedWidth) ? savedWidth : DEFAULT_WIDTH, MIN_WIDTH, MAX_WIDTH);
|
||||
}
|
||||
|
||||
function applySidebarWidth() {
|
||||
const nextWidth = `${getSavedSidebarWidth()}px`;
|
||||
|
||||
document.documentElement.style.setProperty('--wms-sidebar-width', nextWidth);
|
||||
}
|
||||
|
||||
function applySidebarState() {
|
||||
const root = document.documentElement;
|
||||
const saved = localStorage.getItem(SIDEBAR_STATE_KEY) || 'expanded';
|
||||
|
||||
root.dataset.wmsSidebar = saved === 'collapsed' ? 'collapsed' : 'expanded';
|
||||
|
||||
document.querySelectorAll('[data-wms-sidebar-toggle]').forEach((button) => {
|
||||
const expanded = root.dataset.wmsSidebar !== 'collapsed';
|
||||
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
button.setAttribute('aria-label', expanded ? '{{ __('Collapse sidebar') }}' :
|
||||
'{{ __('Expand sidebar') }}');
|
||||
});
|
||||
}
|
||||
|
||||
function getMenuItems(group) {
|
||||
return Array.from(group.children).find((child) => child.matches('[data-wms-menu-items]')) || null;
|
||||
}
|
||||
|
||||
function getMenuToggle(group) {
|
||||
return Array.from(group.children).find((child) => child.matches('[data-wms-menu-toggle]')) || null;
|
||||
}
|
||||
|
||||
function groupHasActiveLink(group) {
|
||||
const items = getMenuItems(group);
|
||||
const activeHeading = Array.from(group.children).some((child) => {
|
||||
return child.matches('.wms-sidebar-heading-button.is-active');
|
||||
});
|
||||
|
||||
const routeSelectedGroup = group.dataset.wmsSelected === 'true' && group.dataset.wmsHasActive ===
|
||||
'true';
|
||||
|
||||
return routeSelectedGroup || activeHeading || (items ? items.querySelector(
|
||||
'.wms-sidebar-link.is-active') !== null : false);
|
||||
}
|
||||
|
||||
function setMenuGroupOpen(group, isOpen) {
|
||||
group.dataset.wmsMenuOpen = isOpen ? 'true' : 'false';
|
||||
|
||||
const toggle = getMenuToggle(group);
|
||||
|
||||
if (toggle) {
|
||||
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function setMenuGroupSelected(group, isSelected) {
|
||||
group.dataset.wmsSelected = isSelected ? 'true' : 'false';
|
||||
group.dataset.wmsHasActive = isSelected ? 'true' : 'false';
|
||||
}
|
||||
|
||||
function resetAllMenuGroups() {
|
||||
document.querySelectorAll('[data-wms-menu-group]').forEach((group) => {
|
||||
setMenuGroupOpen(group, false);
|
||||
setMenuGroupSelected(group, false);
|
||||
group.dataset.wmsManualActive = 'false';
|
||||
});
|
||||
}
|
||||
|
||||
function selectMenuGroup(group, openGroup = true) {
|
||||
resetAllMenuGroups();
|
||||
setMenuGroupSelected(group, true);
|
||||
setMenuGroupOpen(group, openGroup);
|
||||
}
|
||||
|
||||
function applyMenuState() {
|
||||
const groups = Array.from(document.querySelectorAll('[data-wms-menu-group]'));
|
||||
const selectedByRoute = groups.find((group) => groupHasActiveLink(group)) || null;
|
||||
const selectedByManualClick = groups.find((group) => group.dataset.wmsManualActive === 'true') || null;
|
||||
const selectedGroup = selectedByRoute || selectedByManualClick;
|
||||
const shouldOpenPlatformForFirstVisit = !selectedGroup && localStorage.getItem(
|
||||
PLATFORM_FIRST_OPEN_KEY) !== '1';
|
||||
|
||||
resetAllMenuGroups();
|
||||
|
||||
if (selectedGroup) {
|
||||
setMenuGroupSelected(selectedGroup, true);
|
||||
setMenuGroupOpen(selectedGroup, true);
|
||||
} else if (shouldOpenPlatformForFirstVisit) {
|
||||
const platformGroup = groups.find((group) => group.dataset.wmsMenuGroup === 'platform');
|
||||
|
||||
if (platformGroup) {
|
||||
setMenuGroupOpen(platformGroup, true);
|
||||
}
|
||||
|
||||
localStorage.setItem(PLATFORM_FIRST_OPEN_KEY, '1');
|
||||
}
|
||||
}
|
||||
|
||||
function bindMenuControls() {
|
||||
document.querySelectorAll('[data-wms-menu-toggle]').forEach((button) => {
|
||||
if (button.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.wmsBound = '1';
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
const group = button.closest('[data-wms-menu-group]');
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextOpen = group.dataset.wmsMenuOpen !== 'true';
|
||||
|
||||
resetAllMenuGroups();
|
||||
|
||||
if (nextOpen) {
|
||||
group.dataset.wmsManualActive = 'true';
|
||||
setMenuGroupSelected(group, true);
|
||||
setMenuGroupOpen(group, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindSidebarResize() {
|
||||
document.querySelectorAll('[data-wms-sidebar-resizer]').forEach((handle) => {
|
||||
if (handle.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.dataset.wmsBound = '1';
|
||||
|
||||
let startX = 0;
|
||||
let startWidth = DEFAULT_WIDTH;
|
||||
let isResizing = false;
|
||||
|
||||
function getClientX(event) {
|
||||
if (event.touches && event.touches.length > 0) {
|
||||
return event.touches[0].clientX;
|
||||
}
|
||||
|
||||
return event.clientX;
|
||||
}
|
||||
|
||||
function startResize(event) {
|
||||
if (document.documentElement.dataset.wmsSidebar === 'collapsed') {
|
||||
return;
|
||||
}
|
||||
|
||||
isResizing = true;
|
||||
startX = getClientX(event);
|
||||
|
||||
const currentWidth = Number.parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
'--wms-sidebar-width'),
|
||||
10
|
||||
);
|
||||
|
||||
startWidth = Number.isFinite(currentWidth) ? currentWidth : DEFAULT_WIDTH;
|
||||
|
||||
document.body.classList.add('wms-sidebar-is-resizing');
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function moveResize(event) {
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = getClientX(event) - startX;
|
||||
const nextWidth = clamp(startWidth + delta, MIN_WIDTH, MAX_WIDTH);
|
||||
|
||||
document.documentElement.style.setProperty('--wms-sidebar-width', `${nextWidth}px`);
|
||||
localStorage.setItem(SIDEBAR_WIDTH_KEY, String(nextWidth));
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function stopResize() {
|
||||
if (!isResizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isResizing = false;
|
||||
document.body.classList.remove('wms-sidebar-is-resizing');
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', startResize);
|
||||
handle.addEventListener('touchstart', startResize, {
|
||||
passive: false
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', moveResize);
|
||||
window.addEventListener('touchmove', moveResize, {
|
||||
passive: false
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', stopResize);
|
||||
window.addEventListener('touchend', stopResize);
|
||||
window.addEventListener('touchcancel', stopResize);
|
||||
});
|
||||
}
|
||||
|
||||
function setMobileToggleState(isOpen) {
|
||||
document.querySelectorAll('[data-wms-mobile-open]').forEach((button) => {
|
||||
button.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
|
||||
document.body.classList.toggle('wms-mobile-nav-open', isOpen);
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
document.documentElement.dataset.wmsMobileSidebar = 'closed';
|
||||
setMobileToggleState(false);
|
||||
}
|
||||
|
||||
function openMobileSidebar() {
|
||||
document.documentElement.dataset.wmsMobileSidebar = 'open';
|
||||
setMobileToggleState(true);
|
||||
}
|
||||
|
||||
function toggleMobileSidebar() {
|
||||
if (document.documentElement.dataset.wmsMobileSidebar === 'open') {
|
||||
closeMobileSidebar();
|
||||
return;
|
||||
}
|
||||
|
||||
openMobileSidebar();
|
||||
}
|
||||
|
||||
function bindSidebarControls() {
|
||||
applySidebarWidth();
|
||||
applySidebarState();
|
||||
applyMenuState();
|
||||
bindMenuControls();
|
||||
bindSidebarResize();
|
||||
|
||||
document.querySelectorAll('[data-wms-sidebar-toggle]').forEach((button) => {
|
||||
if (button.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.wmsBound = '1';
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
const root = document.documentElement;
|
||||
const next = root.dataset.wmsSidebar === 'collapsed' ? 'expanded' : 'collapsed';
|
||||
|
||||
root.dataset.wmsSidebar = next;
|
||||
localStorage.setItem(SIDEBAR_STATE_KEY, next);
|
||||
|
||||
applySidebarState();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-wms-brand-toggle]').forEach((button) => {
|
||||
if (button.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.wmsBound = '1';
|
||||
|
||||
button.addEventListener('click', function(event) {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (root.dataset.wmsSidebar !== 'collapsed') {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
root.dataset.wmsSidebar = 'expanded';
|
||||
localStorage.setItem(SIDEBAR_STATE_KEY, 'expanded');
|
||||
|
||||
applySidebarState();
|
||||
applyMenuState();
|
||||
});
|
||||
});
|
||||
|
||||
closeMobileSidebar();
|
||||
|
||||
document.querySelectorAll('[data-wms-mobile-open]').forEach((button) => {
|
||||
if (button.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.wmsBound = '1';
|
||||
|
||||
button.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
toggleMobileSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-wms-mobile-close]').forEach((button) => {
|
||||
if (button.dataset.wmsBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.wmsBound = '1';
|
||||
button.addEventListener('click', closeMobileSidebar);
|
||||
});
|
||||
|
||||
document.querySelectorAll(
|
||||
'.wms-sidebar-link[wire\\:navigate], .wms-sidebar-heading-button[wire\\:navigate]').forEach((
|
||||
link) => {
|
||||
if (link.dataset.wmsMobileBound === '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
link.dataset.wmsMobileBound = '1';
|
||||
|
||||
link.addEventListener('click', function() {
|
||||
document.querySelectorAll(
|
||||
'.wms-sidebar-link.is-active, .wms-sidebar-heading-button.is-active')
|
||||
.forEach((activeLink) => {
|
||||
activeLink.classList.remove('is-active');
|
||||
});
|
||||
|
||||
link.classList.add('is-active');
|
||||
|
||||
const selectedGroup = link.closest('[data-wms-menu-group]');
|
||||
|
||||
if (selectedGroup) {
|
||||
selectMenuGroup(selectedGroup, true);
|
||||
}
|
||||
|
||||
applySidebarWidth();
|
||||
closeMobileSidebar();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
applySidebarWidth();
|
||||
applySidebarState();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', bindSidebarControls);
|
||||
document.addEventListener('livewire:navigated', bindSidebarControls);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-layouts::auth.simple :title="$title ?? null">
|
||||
{{ $slot }}
|
||||
</x-layouts::auth.simple>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-neutral-100 antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-md flex-col gap-6">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
|
||||
<div class="px-10 py-8">{{ $slot }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@persist('toast')
|
||||
<flux:toast.group>
|
||||
<flux:toast />
|
||||
</flux:toast.group>
|
||||
@endpersist
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<div class="bg-background flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-sm flex-col gap-2">
|
||||
<a href="{{ route('home') }}" class="flex flex-col items-center gap-2 font-medium" wire:navigate>
|
||||
<span class="flex h-9 w-9 mb-1 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
<div class="flex flex-col gap-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@persist('toast')
|
||||
<flux:toast.group>
|
||||
<flux:toast />
|
||||
</flux:toast.group>
|
||||
@endpersist
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||
|
||||
<head>
|
||||
@include('partials.head')
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-white antialiased dark:bg-linear-to-b dark:from-neutral-950 dark:to-neutral-900">
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<div
|
||||
class="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div
|
||||
class="bg-muted relative hidden h-full flex-col p-10 text-white lg:flex dark:border-e dark:border-neutral-800">
|
||||
<div class="absolute inset-0 bg-neutral-900"></div>
|
||||
<a href="{{ route('home') }}" class="relative z-20 flex items-center text-lg font-medium" wire:navigate>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="me-2 h-7 fill-current text-white" />
|
||||
</span>
|
||||
{{ config('app.name', 'Laravel') }}
|
||||
</a>
|
||||
|
||||
@php
|
||||
[$message, $author] = str(Illuminate\Foundation\Inspiring::quotes()->random())->explode('-');
|
||||
@endphp
|
||||
|
||||
<div class="relative z-20 mt-auto">
|
||||
<blockquote class="space-y-2">
|
||||
<flux:heading size="lg">“{{ trim($message) }}”</flux:heading>
|
||||
<footer>
|
||||
<flux:heading>{{ trim($author) }}</flux:heading>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full lg:p-8">
|
||||
<div class="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<a href="{{ route('home') }}" class="z-20 flex flex-col items-center gap-2 font-medium lg:hidden"
|
||||
wire:navigate>
|
||||
<span class="flex h-9 w-9 items-center justify-center rounded-md">
|
||||
<x-app-logo-icon class="size-9 fill-current text-black dark:text-white" />
|
||||
</span>
|
||||
|
||||
<span class="sr-only">{{ config('app.name', 'Laravel') }}</span>
|
||||
</a>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@persist('toast')
|
||||
<flux:toast.group>
|
||||
<flux:toast />
|
||||
</flux:toast.group>
|
||||
@endpersist
|
||||
|
||||
@fluxScripts
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,105 @@
|
||||
{{-- Shared Application Top Bar --}}
|
||||
@php
|
||||
$user = auth()->user();
|
||||
$dashboardRoute =
|
||||
Route::has('platform.dashboard') && method_exists($user, 'isMpbaPlatformAdmin') && $user?->isMpbaPlatformAdmin()
|
||||
? 'platform.dashboard'
|
||||
: (Route::has('dashboard')
|
||||
? 'dashboard'
|
||||
: null);
|
||||
@endphp
|
||||
|
||||
<header class="sticky top-0 z-40 border-b border-white/10 bg-[#111118]/95 backdrop-blur" role="banner">
|
||||
<div class="flex h-16 w-full items-center justify-between gap-3 px-3 sm:px-4 lg:px-6">
|
||||
<div class="flex min-w-0 items-center gap-3 lg:hidden">
|
||||
<button type="button"
|
||||
class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-white/5 text-white transition hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-[#111118] lg:hidden"
|
||||
data-wms-mobile-open aria-label="{{ __('Open navigation menu') }}" aria-controls="wms-main-sidebar"
|
||||
aria-expanded="false" data-test="mobile-menu-button">
|
||||
<i class="fa-solid fa-bars" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<a href="{{ $dashboardRoute ? route($dashboardRoute) : '#' }}" wire:navigate
|
||||
class="flex min-w-0 items-center gap-3" aria-label="{{ __('Warehouse Management System') }}">
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-cyan-600 text-sm font-bold text-white shadow-lg shadow-cyan-950/40">
|
||||
W
|
||||
</span>
|
||||
|
||||
<span class="hidden min-w-0 sm:block">
|
||||
<span class="block truncate text-sm font-semibold text-white sm:text-base">
|
||||
{{ __('Warehouse Management System') }}
|
||||
</span>
|
||||
<span class="block text-xs font-semibold uppercase tracking-wide text-cyan-400">
|
||||
MPBA
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="userControls" class="relative ml-auto flex shrink-0 items-center gap-3" x-data="{ open: false }"
|
||||
x-on:keydown.escape.window="open = false">
|
||||
<button type="button"
|
||||
class="flex items-center gap-3 rounded-2xl border border-white/10 bg-white/5 px-2 py-2 text-left transition hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-[#111118] sm:px-3"
|
||||
x-on:click="open = ! open" x-bind:aria-expanded="open.toString()" aria-haspopup="menu"
|
||||
data-test="top-user-menu-button">
|
||||
<span class="hidden text-right md:block">
|
||||
<span
|
||||
class="block max-w-48 truncate text-sm font-semibold text-white">{{ $user?->name ?? __('User') }}</span>
|
||||
<span class="block max-w-48 truncate text-xs text-white/50">{{ $user?->email ?? '' }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-700 text-xs font-bold text-white shadow-lg shadow-blue-950/40">
|
||||
{{ $user?->initials() ?? 'U' }}
|
||||
</span>
|
||||
|
||||
<i class="fa-solid fa-chevron-down hidden text-white/50 sm:block" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div x-cloak x-show="open" x-transition.origin.top.right x-on:click.outside="open = false"
|
||||
class="absolute right-0 top-full mt-2 w-[calc(100vw-1.5rem)] max-w-72 overflow-hidden rounded-2xl border border-white/10 bg-[#181820] shadow-2xl shadow-black/40 sm:w-72"
|
||||
role="menu">
|
||||
<div class="border-b border-white/10 px-4 py-3">
|
||||
<div class="truncate text-sm font-semibold text-white">{{ $user?->name ?? __('User') }}</div>
|
||||
<div class="truncate text-xs text-white/50">{{ $user?->email ?? '' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2">
|
||||
@if ($dashboardRoute)
|
||||
<a href="{{ route($dashboardRoute) }}" wire:navigate
|
||||
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||
role="menuitem">
|
||||
{{ __('Dashboard') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('profile.edit'))
|
||||
<a href="{{ route('profile.edit') }}" wire:navigate
|
||||
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||
role="menuitem">
|
||||
{{ __('Profile') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if (Route::has('settings.index'))
|
||||
<a href="{{ route('settings.index') }}" wire:navigate
|
||||
class="block rounded-xl px-3 py-2 text-sm text-white/80 hover:bg-white/10 hover:text-white"
|
||||
role="menuitem">
|
||||
{{ __('Settings') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="mt-2 border-t border-white/10 pt-2">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="w-full rounded-xl px-3 py-2 text-left text-sm text-red-300 hover:bg-red-500/10 hover:text-red-200"
|
||||
role="menuitem" data-test="logout-button">
|
||||
{{ __('Sign out') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,44 @@
|
||||
<x-layouts::auth :title="__('Confirm password')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black text-white dark:bg-white dark:text-zinc-950">
|
||||
WMS</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Protected area') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Confirm password') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Confirm your identity before continuing to this secure WMS workflow.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<x-auth-session-status
|
||||
class="mb-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
:status="session('status')" />
|
||||
|
||||
<div class="mb-5">
|
||||
<x-passkey-verify options-route="passkey.confirm-options" submit-route="passkey.confirm"
|
||||
:label="__('Confirm with passkey')" :loading-label="__('Confirming...')" :separator="__('Or confirm with password')" />
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('password.confirm.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<flux:input name="password" :label="__('Password')" type="password" required
|
||||
autocomplete="current-password" :placeholder="__('Password')" viewable />
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full" data-test="confirm-password-button">
|
||||
{{ __('Confirm') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,47 @@
|
||||
<x-layouts::auth :title="__('Forgot password')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black text-white dark:bg-white dark:text-zinc-950">
|
||||
WMS</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Account recovery') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Warehouse Management System') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Request a secure reset link for your WMS account.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<x-auth-header :title="__('Forgot password')" :description="__('Enter your email address and we will send you a password reset link.')" />
|
||||
|
||||
<x-auth-session-status
|
||||
class="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
:status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.email') }}" class="mt-6 flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<flux:input name="email" :label="__('Email address')" type="email" required autofocus
|
||||
placeholder="email@example.com" />
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full"
|
||||
data-test="email-password-reset-link-button">
|
||||
{{ __('Email password reset link') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span>{{ __('Remember your password?') }}</span>
|
||||
<flux:link :href="route('login')" wire:navigate>{{ __('Log in') }}</flux:link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,95 @@
|
||||
<x-layouts::auth :title="__('Log in')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black tracking-tight text-white shadow-lg shadow-zinc-900/20 dark:bg-white dark:text-zinc-950">
|
||||
WMS
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">MPBA
|
||||
Limited</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Warehouse Management System') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Secure access for tenant workspaces, warehouse operations, support, licences, and dashboards.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<div class="mb-6 space-y-1 text-center">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Secure sign in') }}</p>
|
||||
<h2 class="text-xl font-bold text-zinc-950 dark:text-white">{{ __('Log in to your account') }}</h2>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Enter your email and password to continue.') }}</p>
|
||||
</div>
|
||||
|
||||
<x-auth-session-status
|
||||
class="mb-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
:status="session('status')" />
|
||||
|
||||
<div class="mb-5">
|
||||
<x-passkey-verify />
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('login.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="email"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Email address') }}</label>
|
||||
<input id="email" name="email" type="email" value="{{ old('email') }}" required
|
||||
autofocus autocomplete="email" placeholder="email@example.com"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
@error('email')
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<label for="password"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Password') }}</label>
|
||||
@if (Route::has('password.request'))
|
||||
<a class="text-sm font-semibold text-sky-700 underline underline-offset-4 hover:text-sky-900 dark:text-sky-300"
|
||||
href="{{ route('password.request') }}"
|
||||
wire:navigate>{{ __('Forgot password?') }}</a>
|
||||
@endif
|
||||
</div>
|
||||
<input id="password" name="password" type="password" required autocomplete="current-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
@error('password')
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<label
|
||||
class="flex items-center gap-3 rounded-xl border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-700 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-300">
|
||||
<input name="remember" type="checkbox" value="1" @checked(old('remember'))
|
||||
class="rounded border-zinc-300 text-sky-600 focus:ring-sky-500 dark:border-zinc-700 dark:bg-zinc-950" />
|
||||
<span>{{ __('Remember this device') }}</span>
|
||||
</label>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full rounded-xl bg-zinc-950 px-4 py-3 text-sm font-bold text-white shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-4 focus:ring-sky-200 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200 dark:focus:ring-sky-950"
|
||||
data-test="login-button">
|
||||
{{ __('Log in') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span>{{ __('Need access?') }}</span>
|
||||
<a href="{{ route('register') }}" wire:navigate
|
||||
class="font-semibold text-sky-700 underline underline-offset-4 hover:text-sky-900 dark:text-sky-300">{{ __('Create an account') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,87 @@
|
||||
<x-layouts::auth :title="__('Register')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black tracking-tight text-white shadow-lg shadow-zinc-900/20 dark:bg-white dark:text-zinc-950">
|
||||
WMS
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('MPBA Limited') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Create your WMS account') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Register for secure access to your company workspace and warehouse operations.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<x-auth-session-status
|
||||
class="mb-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
:status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('register.store') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="name"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Name') }}</label>
|
||||
<input id="name" name="name" type="text" value="{{ old('name') }}" required
|
||||
autofocus autocomplete="name" placeholder="{{ __('Full name') }}"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
@error('name')
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="email"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Email address') }}</label>
|
||||
<input id="email" name="email" type="email" value="{{ old('email') }}" required
|
||||
autocomplete="email" placeholder="email@example.com"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
@error('email')
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="password"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Password') }}</label>
|
||||
<input id="password" name="password" type="password" required autocomplete="new-password"
|
||||
placeholder="{{ __('Password') }}"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
@error('password')
|
||||
<p class="text-sm font-medium text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="password_confirmation"
|
||||
class="text-sm font-semibold text-zinc-800 dark:text-zinc-200">{{ __('Confirm password') }}</label>
|
||||
<input id="password_confirmation" name="password_confirmation" type="password" required
|
||||
autocomplete="new-password" placeholder="{{ __('Confirm password') }}"
|
||||
class="w-full rounded-xl border border-zinc-300 bg-white px-4 py-3 text-sm text-zinc-950 shadow-sm outline-none transition placeholder:text-zinc-400 focus:border-sky-500 focus:ring-4 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white dark:placeholder:text-zinc-500 dark:focus:border-sky-400 dark:focus:ring-sky-950" />
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full rounded-xl bg-zinc-950 px-4 py-3 text-sm font-bold text-white shadow-sm transition hover:bg-zinc-800 focus:outline-none focus:ring-4 focus:ring-sky-200 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200 dark:focus:ring-sky-950"
|
||||
data-test="register-user-button">
|
||||
{{ __('Create account') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span>{{ __('Already have access?') }}</span>
|
||||
<a href="{{ route('login') }}" wire:navigate
|
||||
class="font-semibold text-sky-700 underline underline-offset-4 hover:text-sky-900 dark:text-sky-300">{{ __('Log in') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,50 @@
|
||||
<x-layouts::auth :title="__('Reset password')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black text-white dark:bg-white dark:text-zinc-950">
|
||||
WMS</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Password reset') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Secure account recovery') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Create a new password for your WMS account.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<x-auth-session-status
|
||||
class="mb-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
:status="session('status')" />
|
||||
|
||||
<form method="POST" action="{{ route('password.update') }}" class="flex flex-col gap-5">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ request()->route('token') }}">
|
||||
|
||||
<flux:input name="email" value="{{ request('email') }}" :label="__('Email')" type="email"
|
||||
required autocomplete="email" />
|
||||
|
||||
<flux:input name="password" :label="__('Password')" type="password" required
|
||||
autocomplete="new-password" :placeholder="__('Password')"
|
||||
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
|
||||
viewable />
|
||||
|
||||
<flux:input name="password_confirmation" :label="__('Confirm password')" type="password" required
|
||||
autocomplete="new-password" :placeholder="__('Confirm password')"
|
||||
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
|
||||
viewable />
|
||||
|
||||
<flux:button type="submit" variant="primary" class="w-full" data-test="reset-password-button">
|
||||
{{ __('Reset password') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,101 @@
|
||||
<x-layouts::auth :title="__('Two-factor authentication')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black text-white dark:bg-white dark:text-zinc-950">
|
||||
WMS</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Multi-factor security') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Verify your sign in') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Complete the security challenge to continue into WMS.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<div class="relative w-full h-auto" x-cloak x-data="{
|
||||
showRecoveryInput: @js($errors->has('recovery_code')),
|
||||
code: '',
|
||||
recovery_code: '',
|
||||
focusOtp() {
|
||||
this.$nextTick(() => this.$refs.otp?.querySelector('input')?.focus());
|
||||
},
|
||||
init() {
|
||||
if (!this.showRecoveryInput) {
|
||||
this.focusOtp();
|
||||
}
|
||||
},
|
||||
toggleInput() {
|
||||
this.showRecoveryInput = !this.showRecoveryInput;
|
||||
|
||||
this.code = '';
|
||||
this.recovery_code = '';
|
||||
|
||||
$nextTick(() => {
|
||||
this.showRecoveryInput ?
|
||||
this.$refs.recovery_code?.focus() :
|
||||
this.focusOtp();
|
||||
});
|
||||
},
|
||||
}">
|
||||
<div x-show="!showRecoveryInput">
|
||||
<x-auth-header :title="__('Authentication code')" :description="__('Enter the authentication code provided by your authenticator application.')" />
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryInput">
|
||||
<x-auth-header :title="__('Recovery code')" :description="__(
|
||||
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||
)" />
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('two-factor.login.store') }}" class="mt-6">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-5 text-center">
|
||||
<div x-show="!showRecoveryInput">
|
||||
<div class="my-5 flex items-center justify-center" x-ref="otp">
|
||||
<flux:otp x-model="code" length="6" name="code" label="OTP Code"
|
||||
label:sr-only class="mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryInput">
|
||||
<div class="my-5">
|
||||
<flux:input type="text" name="recovery_code" x-ref="recovery_code"
|
||||
x-bind:required="showRecoveryInput" autocomplete="one-time-code"
|
||||
x-model="recovery_code" />
|
||||
</div>
|
||||
|
||||
@error('recovery_code')
|
||||
<flux:text color="red">
|
||||
{{ $message }}
|
||||
</flux:text>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<flux:button variant="primary" type="submit" class="w-full">
|
||||
{{ __('Continue') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-center text-sm leading-5 text-zinc-600 dark:text-zinc-400">
|
||||
<span>{{ __('or you can') }}</span>
|
||||
<button type="button"
|
||||
class="inline font-semibold text-sky-700 underline underline-offset-4 hover:text-sky-900 dark:text-sky-300"
|
||||
@click="toggleInput()">
|
||||
<span x-show="!showRecoveryInput">{{ __('login using a recovery code') }}</span>
|
||||
<span x-show="showRecoveryInput">{{ __('login using an authentication code') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,52 @@
|
||||
<x-layouts::auth :title="__('Email verification')">
|
||||
<section
|
||||
class="mx-auto w-full max-w-md overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-950">
|
||||
<div class="h-1.5 bg-gradient-to-r from-emerald-500 via-sky-500 to-blue-600"></div>
|
||||
|
||||
<div class="space-y-7 p-6 sm:p-8">
|
||||
<div class="space-y-5 text-center">
|
||||
<div
|
||||
class="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl bg-zinc-950 text-sm font-black text-white dark:bg-white dark:text-zinc-950">
|
||||
WMS</div>
|
||||
<div class="space-y-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.24em] text-sky-700 dark:text-sky-300">
|
||||
{{ __('Email verification') }}</p>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-zinc-950 dark:text-white">
|
||||
{{ __('Confirm your account') }}</h1>
|
||||
<p class="text-sm leading-6 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Verify your email address before accessing the WMS dashboard.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 dark:border-zinc-800 dark:bg-zinc-900/60">
|
||||
<flux:text class="text-center leading-6">
|
||||
{{ __('Please verify your email address by clicking on the link we just emailed to you.') }}
|
||||
</flux:text>
|
||||
|
||||
@if (session('status') == 'verification-link-sent')
|
||||
<div
|
||||
class="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-center text-sm font-semibold text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-200">
|
||||
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 flex flex-col gap-3">
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
<flux:button type="submit" variant="primary" class="w-full">
|
||||
{{ __('Resend verification email') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<flux:button variant="ghost" type="submit" class="w-full cursor-pointer text-sm"
|
||||
data-test="logout-button">
|
||||
{{ __('Log out') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</x-layouts::auth>
|
||||
@@ -0,0 +1,13 @@
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<flux:heading class="sr-only">{{ __('Appearance settings') }}</flux:heading>
|
||||
|
||||
<x-settings.layout :heading="__('Appearance')" :subheading="__('Update the appearance settings for your account')">
|
||||
<flux:radio.group x-data variant="segmented" x-model="$flux.appearance">
|
||||
<flux:radio value="light" icon="sun">{{ __('Light') }}</flux:radio>
|
||||
<flux:radio value="dark" icon="moon">{{ __('Dark') }}</flux:radio>
|
||||
<flux:radio value="system" icon="computer-desktop">{{ __('System') }}</flux:radio>
|
||||
</flux:radio.group>
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
@@ -0,0 +1,35 @@
|
||||
<section class="mt-10 space-y-6">
|
||||
<div class="relative mb-5">
|
||||
<flux:heading>{{ __('Delete account') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Delete your account and all of its resources') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:modal.trigger name="confirm-user-deletion">
|
||||
<flux:button variant="danger" x-data=""
|
||||
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')">
|
||||
{{ __('Delete account') }}
|
||||
</flux:button>
|
||||
</flux:modal.trigger>
|
||||
|
||||
<flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
|
||||
<form method="POST" wire:submit="deleteUser" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Are you sure you want to delete your account?') }}</flux:heading>
|
||||
|
||||
<flux:subheading>
|
||||
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="password" :label="__('Password')" type="password" viewable />
|
||||
|
||||
<div class="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<flux:modal.close>
|
||||
<flux:button variant="filled">{{ __('Cancel') }}</flux:button>
|
||||
</flux:modal.close>
|
||||
|
||||
<flux:button variant="danger" type="submit">{{ __('Delete account') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</section>
|
||||
@@ -0,0 +1,37 @@
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<flux:heading class="sr-only">{{ __('Profile settings') }}</flux:heading>
|
||||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
|
||||
<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
|
||||
@if ($this->hasUnverifiedEmail)
|
||||
<div>
|
||||
<flux:text class="mt-4">
|
||||
{{ __('Your email address is unverified.') }}
|
||||
|
||||
<flux:link class="text-sm cursor-pointer"
|
||||
wire:click.prevent="resendVerificationNotification">
|
||||
{{ __('Click here to re-send the verification email.') }}
|
||||
</flux:link>
|
||||
</flux:text>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($this->showDeleteUser)
|
||||
<livewire:settings.delete-user-form />
|
||||
@endif
|
||||
</x-settings.layout>
|
||||
</section>
|
||||
@@ -0,0 +1,266 @@
|
||||
<section class="w-full">
|
||||
@include('partials.settings-heading')
|
||||
|
||||
<flux:heading class="sr-only">{{ __('Security settings') }}</flux:heading>
|
||||
|
||||
<x-settings.layout :heading="__('Update password')" :subheading="__('Ensure your account is using a long, random password to stay secure')">
|
||||
<form method="POST" wire:submit="updatePassword" class="mt-6 space-y-6">
|
||||
<flux:input wire:model="current_password" :label="__('Current password')" type="password" required
|
||||
autocomplete="current-password" viewable />
|
||||
<flux:input wire:model="password" :label="__('New password')" type="password" required
|
||||
autocomplete="new-password"
|
||||
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
|
||||
viewable />
|
||||
<flux:input wire:model="password_confirmation" :label="__('Confirm password')" type="password" required
|
||||
autocomplete="new-password"
|
||||
passwordrules="{{ \Illuminate\Validation\Rules\Password::defaults()->toPasswordRulesString() }}"
|
||||
viewable />
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:button variant="primary" type="submit" data-test="update-password-button">{{ __('Save') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if ($canManageTwoFactor)
|
||||
<section class="mt-12">
|
||||
<flux:heading>{{ __('Two-factor authentication') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage your two-factor authentication settings') }}</flux:subheading>
|
||||
|
||||
<div class="flex flex-col w-full mx-auto space-y-6 text-sm" wire:cloak>
|
||||
@if ($twoFactorEnabled)
|
||||
<div class="space-y-4">
|
||||
<flux:text>
|
||||
{{ __('You will be prompted for a secure, random pin during login, which you can retrieve from the TOTP-supported application on your phone.') }}
|
||||
</flux:text>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<flux:button variant="danger" wire:click="disable">
|
||||
{{ __('Disable 2FA') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<livewire:settings.two-factor.recovery-codes :$requiresConfirmation />
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
<flux:text variant="subtle">
|
||||
{{ __('When you enable two-factor authentication, you will be prompted for a secure pin during login. This pin can be retrieved from a TOTP-supported application on your phone.') }}
|
||||
</flux:text>
|
||||
|
||||
<flux:button variant="primary" wire:click="enable">
|
||||
{{ __('Enable 2FA') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if ($canManageTwoFactor)
|
||||
<flux:modal name="two-factor-setup-modal" class="max-w-md md:min-w-md" @close="closeModal"
|
||||
wire:model="showModal">
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div
|
||||
class="p-0.5 w-auto rounded-full border border-stone-100 dark:border-stone-600 bg-white dark:bg-stone-800 shadow-sm">
|
||||
<div
|
||||
class="p-2.5 rounded-full border border-stone-200 dark:border-stone-600 overflow-hidden bg-stone-100 dark:bg-stone-200 relative">
|
||||
<div
|
||||
class="flex items-stretch absolute inset-0 w-full h-full divide-x [&>div]:flex-1 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
|
||||
@for ($i = 1; $i <= 5; $i++)
|
||||
<div></div>
|
||||
@endfor
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-stretch absolute w-full h-full divide-y [&>div]:flex-1 inset-0 divide-stone-200 dark:divide-stone-300 justify-around opacity-50">
|
||||
@for ($i = 1; $i <= 5; $i++)
|
||||
<div></div>
|
||||
@endfor
|
||||
</div>
|
||||
|
||||
<flux:icon.qr-code class="relative z-20 dark:text-accent-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-center">
|
||||
<flux:heading size="lg">{{ $this->modalConfig['title'] }}</flux:heading>
|
||||
<flux:text>{{ $this->modalConfig['description'] }}</flux:text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($showVerificationStep)
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col items-center space-y-3 justify-center" x-data
|
||||
x-init="$nextTick(() => $el.querySelector('input')?.focus())">
|
||||
<flux:otp name="code" wire:model="code" length="6" label="OTP Code" label:sr-only
|
||||
class="mx-auto" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:button variant="outline" class="flex-1" wire:click="resetVerification">
|
||||
{{ __('Back') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button variant="primary" class="flex-1" wire:click="confirmTwoFactor"
|
||||
x-bind:disabled="$wire.code.length < 6">
|
||||
{{ __('Confirm') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@error('setupData')
|
||||
<flux:callout variant="danger" icon="x-circle" heading="{{ $message }}" />
|
||||
@enderror
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="relative w-64 overflow-hidden border rounded-lg border-stone-200 dark:border-stone-700 aspect-square">
|
||||
@empty($qrCodeSvg)
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center bg-white dark:bg-stone-700 animate-pulse">
|
||||
<flux:icon.loading />
|
||||
</div>
|
||||
@else
|
||||
<div x-data class="flex items-center justify-center h-full p-4">
|
||||
<div class="bg-white p-3 rounded"
|
||||
:style="($flux.appearance === 'dark' || ($flux.appearance === 'system' && $flux
|
||||
.dark)) ? 'filter: invert(1) brightness(1.5)' : ''">
|
||||
{!! $qrCodeSvg !!}
|
||||
</div>
|
||||
</div>
|
||||
@endempty
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:button :disabled="$errors->has('setupData')" variant="primary" class="w-full"
|
||||
wire:click="showVerificationIfNecessary">
|
||||
{{ $this->modalConfig['buttonText'] }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="relative flex items-center justify-center w-full">
|
||||
<div class="absolute inset-0 w-full h-px top-1/2 bg-stone-200 dark:bg-stone-600"></div>
|
||||
<span
|
||||
class="relative px-2 text-sm bg-white dark:bg-stone-800 text-stone-600 dark:text-stone-400">
|
||||
{{ __('or, enter the code manually') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2" x-data="{
|
||||
copied: false,
|
||||
async copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText('{{ $manualSetupKey }}');
|
||||
this.copied = true;
|
||||
setTimeout(() => this.copied = false, 1500);
|
||||
} catch (e) {
|
||||
console.warn('Could not copy to clipboard');
|
||||
}
|
||||
}
|
||||
}">
|
||||
<div class="flex items-stretch w-full border rounded-xl dark:border-stone-700">
|
||||
@empty($manualSetupKey)
|
||||
<div
|
||||
class="flex items-center justify-center w-full p-3 bg-stone-100 dark:bg-stone-700">
|
||||
<flux:icon.loading variant="mini" />
|
||||
</div>
|
||||
@else
|
||||
<input type="text" readonly value="{{ $manualSetupKey }}"
|
||||
class="w-full p-3 bg-transparent outline-none text-stone-900 dark:text-stone-100" />
|
||||
|
||||
<button @click="copy()"
|
||||
class="px-3 transition-colors border-l cursor-pointer border-stone-200 dark:border-stone-600">
|
||||
<flux:icon.document-duplicate x-show="!copied" variant="outline"></flux:icon>
|
||||
<flux:icon.check x-show="copied" variant="solid" class="text-green-500">
|
||||
</flux:icon>
|
||||
</button>
|
||||
@endempty
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:modal>
|
||||
@endif
|
||||
|
||||
@if ($canManagePasskeys)
|
||||
<section class="mt-12">
|
||||
<flux:heading>{{ __('Passkeys') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage your passkeys for passwordless sign-in') }}</flux:subheading>
|
||||
|
||||
<div class="mt-6 flex flex-col w-full mx-auto space-y-6 text-sm" wire:cloak>
|
||||
<div class="border rounded-lg border-zinc-200 dark:border-zinc-700 overflow-hidden">
|
||||
@forelse ($passkeys as $passkey)
|
||||
<div
|
||||
class="flex items-center justify-between p-4 {{ !$loop->last ? 'border-b border-zinc-200 dark:border-zinc-700' : '' }}">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-zinc-100 dark:bg-zinc-800">
|
||||
<flux:icon.key class="size-5 text-zinc-500 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<p class="font-medium tracking-tight">{{ $passkey['name'] }}</p>
|
||||
@if ($passkey['authenticator'])
|
||||
<flux:badge size="sm">{{ $passkey['authenticator'] }}
|
||||
</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-zinc-500 dark:text-zinc-400 text-xs">
|
||||
{{ __('Added :time', ['time' => $passkey['created_at_diff']]) }}
|
||||
@if ($passkey['last_used_at_diff'])
|
||||
<span class="opacity-50 mx-1">/</span>
|
||||
{{ __('Last used :time', ['time' => $passkey['last_used_at_diff']]) }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:button variant="ghost" size="sm" icon="trash" icon:variant="outline"
|
||||
wire:click="confirmDelete({{ $passkey['id'] }})"
|
||||
class="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/50" />
|
||||
</div>
|
||||
@empty
|
||||
<div class="p-8 text-center">
|
||||
<div
|
||||
class="mx-auto mb-4 flex size-14 items-center justify-center rounded-2xl bg-zinc-100 dark:bg-zinc-800">
|
||||
<flux:icon.key class="size-7 text-zinc-400 dark:text-zinc-500" />
|
||||
</div>
|
||||
<p class="font-medium">{{ __('No passkeys yet') }}</p>
|
||||
<flux:text class="mt-1">{{ __('Add a passkey to sign in without a password') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<x-passkey-registration />
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</x-settings.layout>
|
||||
|
||||
<flux:modal name="delete-passkey-modal" class="max-w-md md:min-w-md" @close="closeDeleteModal"
|
||||
wire:model="showDeleteModal">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<flux:heading size="lg">{{ __('Remove passkey') }}</flux:heading>
|
||||
<flux:text>
|
||||
{{ __('Are you sure you want to remove the passkey ":name"? You will no longer be able to use it to sign in.', ['name' => $deletingPasskeyName]) }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<flux:button variant="outline" wire:click="closeDeleteModal">
|
||||
{{ __('Cancel') }}
|
||||
</flux:button>
|
||||
<flux:button variant="danger" wire:click="deletePasskey">
|
||||
{{ __('Remove passkey') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</section>
|
||||
@@ -0,0 +1,56 @@
|
||||
<div class="py-6 space-y-6 border shadow-sm rounded-xl border-zinc-200 dark:border-white/10" wire:cloak
|
||||
x-data="{ showRecoveryCodes: false }">
|
||||
<div class="px-6 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.lock-closed variant="outline" class="size-4" />
|
||||
<flux:heading size="lg" level="3">{{ __('2FA recovery codes') }}</flux:heading>
|
||||
</div>
|
||||
<flux:text variant="subtle">
|
||||
{{ __('Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.') }}
|
||||
</flux:text>
|
||||
</div>
|
||||
|
||||
<div class="px-6">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<flux:button x-show="!showRecoveryCodes" icon="eye" icon:variant="outline" variant="primary"
|
||||
@click="showRecoveryCodes = true;" aria-expanded="false" aria-controls="recovery-codes-section">
|
||||
{{ __('View recovery codes') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button x-show="showRecoveryCodes" icon="eye-slash" icon:variant="outline" variant="primary"
|
||||
@click="showRecoveryCodes = false" aria-expanded="true" aria-controls="recovery-codes-section">
|
||||
{{ __('Hide recovery codes') }}
|
||||
</flux:button>
|
||||
|
||||
@if (filled($recoveryCodes))
|
||||
<flux:button x-show="showRecoveryCodes" icon="arrow-path" variant="filled"
|
||||
wire:click="regenerateRecoveryCodes">
|
||||
{{ __('Regenerate codes') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div x-show="showRecoveryCodes" x-transition id="recovery-codes-section" class="relative overflow-hidden"
|
||||
x-bind:aria-hidden="!showRecoveryCodes">
|
||||
<div class="mt-3 space-y-3">
|
||||
@error('recoveryCodes')
|
||||
<flux:callout variant="danger" icon="x-circle" heading="{{ $message }}" />
|
||||
@enderror
|
||||
|
||||
@if (filled($recoveryCodes))
|
||||
<div class="grid gap-1 p-4 font-mono text-sm rounded-lg bg-zinc-100 dark:bg-white/5" role="list"
|
||||
aria-label="{{ __('Recovery codes') }}">
|
||||
@foreach ($recoveryCodes as $code)
|
||||
<div role="listitem" class="select-text" wire:loading.class="opacity-50 animate-pulse">
|
||||
{{ $code }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<flux:text variant="subtle" class="text-xs">
|
||||
{{ __('Each recovery code can be used once to access your account and will be removed after use. If you need more, click Regenerate codes above.') }}
|
||||
</flux:text>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<title>
|
||||
{{ filled($title ?? null) ? $title . ' - ' . config('app.name', 'Laravel') : config('app.name', 'Laravel') }}
|
||||
</title>
|
||||
|
||||
<link rel="icon" href="{{ asset('favicons/favicon.ico') }}" sizes="any">
|
||||
<link rel="icon" href="{{ asset('favicons/favicon.svg') }}" type="image/svg+xml">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicons/favicon-16x16.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicons/favicon-32x32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="{{ asset('favicons/favicon-48x48.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="{{ asset('favicons/favicon-64x64.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{ asset('favicons/favicon-96x96.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="{{ asset('favicons/favicon-128x128.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="{{ asset('favicons/favicon-192x192.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ asset('favicons/favicon-256x256.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="384x384" href="{{ asset('favicons/favicon-384x384.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="{{ asset('favicons/favicon-512x512.png') }}">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('favicons/apple-touch-icon.png') }}">
|
||||
<link rel="manifest" href="{{ asset('favicons/site.webmanifest') }}">
|
||||
|
||||
<meta name="application-name" content="{{ config('app.name') }}">
|
||||
<meta name="apple-mobile-web-app-title" content="{{ config('app.name') }}">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-config" content="{{ asset('favicons/browserconfig.xml') }}">
|
||||
<meta name="msapplication-TileImage" content="{{ asset('favicons/mstile-150x150.png') }}">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
@fonts
|
||||
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@fluxAppearance
|
||||
|
||||
</head>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="relative mb-6 w-full">
|
||||
<flux:heading size="xl" level="1">{{ __('Settings') }}</flux:heading>
|
||||
<flux:subheading size="lg" class="mb-6">{{ __('Manage your profile and account settings') }}</flux:subheading>
|
||||
<flux:separator variant="subtle" />
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
# Sidebar support menu replacement
|
||||
|
||||
Replace the existing `{{-- Support Portal --}} ... {{-- Support Portal end --}}` block in your app shell with this direct block:
|
||||
|
||||
```blade
|
||||
@include('partials.wms-support-navigation')
|
||||
```
|
||||
|
||||
Or paste the contents of `resources/views/partials/wms-support-navigation-direct-replacement.blade.php` directly into the sidebar.
|
||||
|
||||
This fixes Documentation not appearing because the Support group now renders when either Helpdesk routes or Documentation routes exist.
|
||||
@@ -0,0 +1,59 @@
|
||||
<div class="d-flex align-items-center" x-data>
|
||||
<form class="mr-2"
|
||||
x-on:submit.prevent="
|
||||
$refs.exportBtn.disabled = true;
|
||||
var oTable = LaravelDataTables['{{ $tableId }}'];
|
||||
var baseUrl = oTable.ajax.url() === '' ? window.location.toString() : oTable.ajax.url();
|
||||
|
||||
var url = new URL(baseUrl);
|
||||
var searchParams = new URLSearchParams(url.search);
|
||||
searchParams.set('action', 'exportQueue');
|
||||
searchParams.set('exportType', '{{ $fileType }}');
|
||||
searchParams.set('sheetName', '{{ $sheetName }}');
|
||||
searchParams.set('buttonName', '{{ $buttonName }}');
|
||||
searchParams.set('emailTo', '{{ urlencode($emailTo) }}');
|
||||
|
||||
var tableParams = $.param(oTable.ajax.params());
|
||||
if (tableParams) {
|
||||
var tableSearchParams = new URLSearchParams(tableParams);
|
||||
tableSearchParams.forEach((value, key) => {
|
||||
searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
url.search = searchParams.toString();
|
||||
|
||||
$.get(url.toString()).then(function(exportId) {
|
||||
$wire.export(exportId);
|
||||
}).catch(function(error) {
|
||||
$wire.exportFinished = true;
|
||||
$wire.exporting = false;
|
||||
$wire.exportFailed = true;
|
||||
});
|
||||
">
|
||||
<button type="submit" x-ref="exportBtn" :disabled="$wire.exporting"
|
||||
class="{{ $class }}">{{ $buttonName }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if ($exporting && $emailTo)
|
||||
<div class="d-inline">Export will be emailed to {{ $emailTo }}.</div>
|
||||
@endif
|
||||
|
||||
@if ($exporting && !$exportFinished)
|
||||
<div class="d-inline" wire:poll="updateExportProgress">Exporting...please wait.</div>
|
||||
@endif
|
||||
|
||||
@if ($exportFinished && !$exportFailed && !$autoDownload)
|
||||
<span>Done. Download file <a href="#" class="text-primary"
|
||||
wire:click.prevent="downloadExport">here</a></span>
|
||||
@endif
|
||||
|
||||
@if ($exportFinished && !$exportFailed && $autoDownload && $downloaded)
|
||||
<span>Done. File has been downloaded.</span>
|
||||
@endif
|
||||
|
||||
@if ($exportFailed)
|
||||
<span>Export failed, please try again later.</span>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
Attached you will find requested report.
|
||||
@@ -0,0 +1,18 @@
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} =
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
|
||||
$.ajaxSetup({headers: {'X-CSRF-TOKEN': '{{ csrf_token() }}'}});
|
||||
@foreach ($editors as $editor)
|
||||
var {{ $editor->instance }} =
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{ $editor->instance }}"] = new
|
||||
$.fn.dataTable.Editor({!! $editor->toJson() !!});
|
||||
{!! $editor->scripts !!}
|
||||
@foreach ((array) $editor->events as $event)
|
||||
{{ $editor->instance }}.on('{!! $event['event'] !!}', {!! $event['script'] !!});
|
||||
@endforeach
|
||||
@endforeach
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"] = $("#%1$s").DataTable(%2$s);
|
||||
});
|
||||
@foreach ($scripts as $script)
|
||||
@include($script)
|
||||
@endforeach
|
||||
@@ -0,0 +1,18 @@
|
||||
window.dtx = window.dtx || {};
|
||||
window.dtx["%1$s"] = function(opts) {
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} =
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
|
||||
@if (isset($editors))
|
||||
@foreach ($editors as $editor)
|
||||
var {{ $editor->instance }} =
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{ $editor->instance }}"] = new
|
||||
$.fn.dataTable.Editor({!! $editor->toJson() !!});
|
||||
{!! $editor->scripts !!}
|
||||
@foreach ((array) $editor->events as $event)
|
||||
{{ $editor->instance }}.on('{!! $event['event'] !!}', {!! $event['script'] !!});
|
||||
@endforeach
|
||||
@endforeach
|
||||
@endif
|
||||
return window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"] =
|
||||
$("#%1$s").DataTable($.extend(%2$s, opts));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
$(function(){
|
||||
@foreach ($editors as $editor)
|
||||
{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{ $editor->instance }}"].on('preSubmit',
|
||||
function(e, data, action) {
|
||||
if (action !== 'remove') return;
|
||||
|
||||
for (let row_id of Object.keys(data.data))
|
||||
{
|
||||
data.data[row_id] = {
|
||||
DT_RowId: data.data[row_id].DT_RowId
|
||||
};
|
||||
}
|
||||
});
|
||||
@endforeach
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} =
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.options = %2$s
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.editors = [];
|
||||
@foreach ($editors as $editor)
|
||||
window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.editors["{{ $editor->instance }}"] =
|
||||
{!! $editor->toJson() !!}
|
||||
@endforeach
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Print Table</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name=description content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 20px
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div data-dusk="page-ready" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);"
|
||||
aria-hidden="true"></div>
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
@foreach ($data as $row)
|
||||
@if ($loop->first)
|
||||
<tr>
|
||||
@foreach ($row as $key => $value)
|
||||
<th>{!! $key !!}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endif
|
||||
<tr>
|
||||
@foreach ($row as $key => $value)
|
||||
@if (is_string($value) || is_numeric($value))
|
||||
<td>{!! $value !!}</td>
|
||||
@else
|
||||
<td></td>
|
||||
@endif
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
$(function(){
|
||||
$('#%1$s').on('xhr.dt', function (e, settings, json, xhr) {
|
||||
if (json == null || !('disableOrdering' in json)) return;
|
||||
|
||||
let table = {{ config('datatables-html.namespace', 'LaravelDataTables') }}[$(this).attr('id')];
|
||||
if (json.disableOrdering) {
|
||||
table.settings()[0].aoColumns.forEach(function(column) {
|
||||
column.bSortable = false;
|
||||
$(column.nTh).removeClass('sorting_asc sorting_desc sorting').addClass('sorting_disabled');
|
||||
});
|
||||
} else {
|
||||
let changed = false;
|
||||
table.settings()[0].aoColumns.forEach(function(column) {
|
||||
if (column.bSortable) return;
|
||||
column.bSortable = true;
|
||||
changed = true;
|
||||
});
|
||||
if (changed) {
|
||||
table.draw();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
document.addEventListener("DOMContentLoaded",function(){window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}=window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}||{};window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"]=$("#%1$s").DataTable(%2$s);});
|
||||
@foreach ($scripts as $script)
|
||||
@include($script)
|
||||
@endforeach
|
||||
@@ -0,0 +1,55 @@
|
||||
@php
|
||||
use Filament\Actions\View\ActionsRenderHook;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
|
||||
$actionModalAlignment = $action->getModalAlignment();
|
||||
$actionIsModalAutofocused = $action->isModalAutofocused();
|
||||
$actionHasModalCloseButton = $action->hasModalCloseButton();
|
||||
$actionIsModalClosedByClickingAway = $action->isModalClosedByClickingAway();
|
||||
$actionIsModalClosedByEscaping = $action->isModalClosedByEscaping();
|
||||
$actionModalDescription = $action->getModalDescription();
|
||||
$actionExtraModalWindowAttributeBag = $action->getExtraModalWindowAttributeBag();
|
||||
$actionExtraModalOverlayAttributeBag = $action->getExtraModalOverlayAttributeBag();
|
||||
$actionModalFooterActions = $action->getVisibleModalFooterActions();
|
||||
$actionModalFooterActionsAlignment = $action->getModalFooterActionsAlignment();
|
||||
$actionModalHeading = $action->getModalHeading();
|
||||
$actionModalIcon = $action->getModalIcon();
|
||||
$actionModalIconColor = $action->getModalIconColor();
|
||||
$actionModalId = "fi-{$this->getId()}-action-{$action->getNestingIndex()}";
|
||||
$actionIsModalSlideOver = $action->isModalSlideOver();
|
||||
$actionModalSlideOverPosition = $action->getModalSlideOverPosition();
|
||||
$actionIsModalFooterSticky = $action->isModalFooterSticky();
|
||||
$actionIsModalHeaderSticky = $action->isModalHeaderSticky();
|
||||
$actionModalWidth = $action->getModalWidth();
|
||||
$actionLivewireCallMountedActionName = $action->hasFormWrapper()
|
||||
? $action->getLivewireCallMountedActionName()
|
||||
: null;
|
||||
$actionModalWireKey = "{$this->getId()}.actions.{$action->getName()}.modal";
|
||||
@endphp
|
||||
|
||||
<x-filament::modal :alignment="$actionModalAlignment" :autofocus="$actionIsModalAutofocused" :close-button="$actionHasModalCloseButton" :close-by-clicking-away="$actionIsModalClosedByClickingAway" :close-by-escaping="$actionIsModalClosedByEscaping"
|
||||
:description="$actionModalDescription" :extra-modal-window-attribute-bag="$actionExtraModalWindowAttributeBag" :extra-modal-overlay-attribute-bag="$actionExtraModalOverlayAttributeBag" :footer-actions="$actionModalFooterActions" :footer-actions-alignment="$actionModalFooterActionsAlignment" :heading="$actionModalHeading"
|
||||
:icon="$actionModalIcon" :icon-color="$actionModalIconColor" :id="$actionModalId" :slide-over="$actionIsModalSlideOver" :slide-over-position="$actionModalSlideOverPosition" :sticky-footer="$actionIsModalFooterSticky"
|
||||
:sticky-header="$actionIsModalHeaderSticky" :width="$actionModalWidth" :wire:key="$actionModalWireKey"
|
||||
:wire:submit.prevent="$actionLivewireCallMountedActionName"
|
||||
:x-on:modal-closed="'if ($event.detail.id === ' . \Illuminate\Support\Js::from($actionModalId) . ') $wire.unmountAction(false)'">
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_CUSTOM_CONTENT_BEFORE, scopes: static::class, data: ['action' => $action]) }}
|
||||
|
||||
{{ $action->getModalContent() }}
|
||||
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_CUSTOM_CONTENT_AFTER, scopes: static::class, data: ['action' => $action]) }}
|
||||
|
||||
@if ($this->mountedActionHasSchema(mountedAction: $action))
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_SCHEMA_BEFORE, scopes: static::class, data: ['action' => $action]) }}
|
||||
|
||||
{{ $this->getMountedActionSchema(mountedAction: $action) }}
|
||||
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_SCHEMA_AFTER, scopes: static::class, data: ['action' => $action]) }}
|
||||
@endif
|
||||
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_CUSTOM_CONTENT_FOOTER_BEFORE, scopes: static::class, data: ['action' => $action]) }}
|
||||
|
||||
{{ $action->getModalContentFooter() }}
|
||||
|
||||
{{ FilamentView::renderHook(ActionsRenderHook::MODAL_CUSTOM_CONTENT_FOOTER_AFTER, scopes: static::class, data: ['action' => $action]) }}
|
||||
</x-filament::modal>
|
||||
@@ -0,0 +1,62 @@
|
||||
@props([
|
||||
'actions' => [],
|
||||
'badge' => null,
|
||||
'badgeColor' => null,
|
||||
'button' => false,
|
||||
'buttonGroup' => null,
|
||||
'color' => null,
|
||||
'dropdownMaxHeight' => null,
|
||||
'dropdownOffset' => null,
|
||||
'dropdownPlacement' => null,
|
||||
'dropdownWidth' => null,
|
||||
'group' => null,
|
||||
'icon' => null,
|
||||
'iconSize' => null,
|
||||
'iconButton' => false,
|
||||
'label' => null,
|
||||
'link' => false,
|
||||
'size' => null,
|
||||
'tooltip' => null,
|
||||
'triggerView' => null,
|
||||
'view' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$group ??= \Filament\Actions\ActionGroup::make($actions)
|
||||
->badgeColor($badgeColor)
|
||||
->color($color)
|
||||
->dropdownMaxHeight($dropdownMaxHeight)
|
||||
->dropdownOffset($dropdownOffset)
|
||||
->dropdownPlacement($dropdownPlacement)
|
||||
->dropdownWidth($dropdownWidth)
|
||||
->icon($icon)
|
||||
->iconSize($iconSize)
|
||||
->label($label)
|
||||
->size($size)
|
||||
->tooltip($tooltip)
|
||||
->triggerView($triggerView)
|
||||
->view($view);
|
||||
|
||||
$badge === true ? $group->badge() : $group->badge($badge);
|
||||
|
||||
if ($button) {
|
||||
$group
|
||||
->button()
|
||||
->iconPosition($attributes->get('iconPosition') ?? $attributes->get('icon-position'))
|
||||
->outlined($attributes->get('outlined') ?? false);
|
||||
}
|
||||
|
||||
if ($buttonGroup) {
|
||||
$group->buttonGroup();
|
||||
}
|
||||
|
||||
if ($iconButton) {
|
||||
$group->iconButton();
|
||||
}
|
||||
|
||||
if ($link) {
|
||||
$group->link();
|
||||
}
|
||||
@endphp
|
||||
|
||||
{{ $group }}
|
||||
@@ -0,0 +1,15 @@
|
||||
@if ($this instanceof \Filament\Actions\Contracts\HasActions && !$this->hasActionsModalRendered)
|
||||
<div wire:partial="action-modals" x-data="filamentActionModals({
|
||||
livewireId: @js($this->getId()),
|
||||
})" style="height: 0">
|
||||
@foreach ($this->getMountedActions() as $action)
|
||||
@if (!$loop->last || $this->mountedActionShouldOpenModal())
|
||||
{{ $action->toModalHtmlable() }}
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@php
|
||||
$this->hasActionsModalRendered = true;
|
||||
@endphp
|
||||
@endif
|
||||
@@ -0,0 +1,275 @@
|
||||
@php
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$items = $getItems();
|
||||
$blockPickerBlocks = $getBlockPickerBlocks();
|
||||
$blockPickerColumns = $getBlockPickerColumns();
|
||||
$blockPickerWidth = $getBlockPickerWidth();
|
||||
$hasBlockPreviews = $hasBlockPreviews();
|
||||
$hasInteractiveBlockPreviews = $hasInteractiveBlockPreviews();
|
||||
|
||||
$addAction = $getAction($getAddActionName());
|
||||
$addActionAlignment = $getAddActionAlignment();
|
||||
$addBetweenAction = $getAction($getAddBetweenActionName());
|
||||
$cloneAction = $getAction($getCloneActionName());
|
||||
$collapseAllAction = $getAction($getCollapseAllActionName());
|
||||
$editAction = $getAction($getEditActionName());
|
||||
$expandAllAction = $getAction($getExpandAllActionName());
|
||||
$deleteAction = $getAction($getDeleteActionName());
|
||||
$moveDownAction = $getAction($getMoveDownActionName());
|
||||
$moveUpAction = $getAction($getMoveUpActionName());
|
||||
$reorderAction = $getAction($getReorderActionName());
|
||||
$extraItemActions = $getExtraItemActions();
|
||||
|
||||
$isAddable = $isAddable();
|
||||
$isCloneable = $isCloneable();
|
||||
$isCollapsible = $isCollapsible();
|
||||
$isDeletable = $isDeletable();
|
||||
$isReorderableWithButtons = $isReorderableWithButtons();
|
||||
$isReorderableWithDragAndDrop = $isReorderableWithDragAndDrop();
|
||||
|
||||
$collapseAllActionIsVisible = $isCollapsible && $collapseAllAction->isVisible();
|
||||
$expandAllActionIsVisible = $isCollapsible && $expandAllAction->isVisible();
|
||||
$persistCollapsed = $shouldPersistCollapsed();
|
||||
|
||||
$key = $getKey();
|
||||
$statePath = $getStatePath();
|
||||
|
||||
$blockLabelHeadingTag = $getHeadingTag();
|
||||
$isBlockLabelTruncated = $isBlockLabelTruncated();
|
||||
$labelBetweenItems = $getLabelBetweenItems();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div
|
||||
{{ $attributes->merge($getExtraAttributes(), escape: false)->class(['fi-fo-builder', 'fi-collapsible' => $isCollapsible]) }}>
|
||||
@if ($collapseAllActionIsVisible || $expandAllActionIsVisible)
|
||||
<div @class(['fi-fo-builder-actions', 'fi-hidden' => count($items) < 2])>
|
||||
@if ($collapseAllActionIsVisible)
|
||||
<span x-on:click="$dispatch('builder-collapse', '{{ $statePath }}')">
|
||||
{{ $collapseAllAction }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($expandAllActionIsVisible)
|
||||
<span x-on:click="$dispatch('builder-expand', '{{ $statePath }}')">
|
||||
{{ $expandAllAction }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (count($items))
|
||||
<ul x-sortable data-sortable-animation-duration="{{ $getReorderAnimationDuration() }}"
|
||||
x-on:end.stop="
|
||||
$wire.mountAction(
|
||||
'reorder',
|
||||
{ items: $event.target.sortable.toArray() },
|
||||
{ schemaComponent: '{{ $key }}' },
|
||||
)
|
||||
"
|
||||
class="fi-fo-builder-items">
|
||||
@php
|
||||
$hasBlockLabels = $hasBlockLabels();
|
||||
$hasBlockIcons = $hasBlockIcons();
|
||||
$hasBlockNumbers = $hasBlockNumbers();
|
||||
$hasBlockHeaders = $hasBlockHeaders();
|
||||
@endphp
|
||||
|
||||
@foreach ($items as $itemKey => $item)
|
||||
@php
|
||||
$visibleExtraItemActions = array_filter(
|
||||
$extraItemActions,
|
||||
fn(Action $action): bool => $action(['item' => $itemKey])->isVisible(),
|
||||
);
|
||||
$cloneAction = $cloneAction(['item' => $itemKey]);
|
||||
$cloneActionIsVisible = $isCloneable && $cloneAction->isVisible();
|
||||
$deleteAction = $deleteAction(['item' => $itemKey]);
|
||||
$deleteActionIsVisible = $isDeletable && $deleteAction->isVisible();
|
||||
$editAction = $editAction(['item' => $itemKey]);
|
||||
$editActionIsVisible = $hasBlockPreviews && $editAction->isVisible();
|
||||
$moveDownAction = $moveDownAction(['item' => $itemKey])->disabled($loop->last);
|
||||
$moveDownActionIsVisible = $isReorderableWithButtons && $moveDownAction->isVisible();
|
||||
$moveUpAction = $moveUpAction(['item' => $itemKey])->disabled($loop->first);
|
||||
$moveUpActionIsVisible = $isReorderableWithButtons && $moveUpAction->isVisible();
|
||||
$reorderActionIsVisible = $isReorderableWithDragAndDrop && $reorderAction->isVisible();
|
||||
$hasItemHeader =
|
||||
$hasBlockHeaders &&
|
||||
($reorderActionIsVisible ||
|
||||
$moveUpActionIsVisible ||
|
||||
$moveDownActionIsVisible ||
|
||||
$hasBlockIcons ||
|
||||
$hasBlockLabels ||
|
||||
$editActionIsVisible ||
|
||||
$cloneActionIsVisible ||
|
||||
$deleteActionIsVisible ||
|
||||
$isCollapsible ||
|
||||
$visibleExtraItemActions);
|
||||
@endphp
|
||||
|
||||
<li wire:ignore.self wire:key="{{ $item->getLivewireKey() }}.item" x-data="{
|
||||
isCollapsed: @if ($persistCollapsed) $persist(@js($isCollapsed($item))).as(`builder-${@js($key)}-${@js($itemKey)}-isCollapsed`) @else @js($isCollapsed($item)) @endif,
|
||||
}"
|
||||
x-on:builder-expand.window="$event.detail === '{{ $statePath }}' && (isCollapsed = false)"
|
||||
x-on:builder-collapse.window="$event.detail === '{{ $statePath }}' && (isCollapsed = true)"
|
||||
x-on:expand="isCollapsed = false" x-sortable-item="{{ $itemKey }}"
|
||||
{{ $item->getParentComponent()->getExtraAttributeBag()->class(['fi-fo-builder-item', 'fi-fo-builder-item-has-header' => $hasItemHeader]) }}
|
||||
x-bind:class="{ 'fi-collapsed': isCollapsed }">
|
||||
@if ($hasItemHeader)
|
||||
<div @if ($isCollapsible) x-on:click.stop="isCollapsed = !isCollapsed" @endif
|
||||
class="fi-fo-builder-item-header">
|
||||
@if ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible)
|
||||
<ul class="fi-fo-builder-item-header-start-actions">
|
||||
@if ($reorderActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $reorderAction->extraAttributes(['x-sortable-handle' => true], merge: true) }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($moveUpActionIsVisible || $moveDownActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $moveUpAction }}
|
||||
</li>
|
||||
|
||||
<li x-on:click.stop>
|
||||
{{ $moveDownAction }}
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$blockIcon = $item->getParentComponent()->getIcon($item->getRawState(), $itemKey);
|
||||
@endphp
|
||||
|
||||
@if ($hasBlockIcons && filled($blockIcon))
|
||||
{{ \Filament\Support\generate_icon_html($blockIcon, attributes: new \Illuminate\View\ComponentAttributeBag()->class(['fi-fo-builder-item-header-icon'])) }}
|
||||
@endif
|
||||
|
||||
@if ($hasBlockLabels)
|
||||
<{{ $blockLabelHeadingTag }} @class([
|
||||
'fi-fo-builder-item-header-label',
|
||||
'fi-truncated' => $isBlockLabelTruncated,
|
||||
])>
|
||||
{{ $item->getParentComponent()->getLabel($item->getRawState(), $itemKey, $loop->index) }}
|
||||
|
||||
@if ($hasBlockNumbers)
|
||||
{{ $loop->iteration }}
|
||||
@endif
|
||||
</{{ $blockLabelHeadingTag }}>
|
||||
@endif
|
||||
|
||||
@if (
|
||||
$editActionIsVisible ||
|
||||
$cloneActionIsVisible ||
|
||||
$deleteActionIsVisible ||
|
||||
$isCollapsible ||
|
||||
$visibleExtraItemActions)
|
||||
<ul class="fi-fo-builder-item-header-end-actions">
|
||||
@foreach ($visibleExtraItemActions as $extraItemAction)
|
||||
<li x-on:click.stop>
|
||||
{{ $extraItemAction(['item' => $itemKey]) }}
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
@if ($editActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $editAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($cloneActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $cloneAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($deleteActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $deleteAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($isCollapsible)
|
||||
<li class="fi-fo-builder-item-header-collapsible-actions"
|
||||
x-on:click.stop="isCollapsed = !isCollapsed">
|
||||
<div class="fi-fo-builder-item-header-collapse-action">
|
||||
{{ $getAction('collapse') }}
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-builder-item-header-expand-action">
|
||||
{{ $getAction('expand') }}
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="! isCollapsed" @class([
|
||||
'fi-fo-builder-item-content',
|
||||
'fi-fo-builder-item-content-has-preview' =>
|
||||
$hasBlockPreviews && $item->getParentComponent()->hasPreview(),
|
||||
])>
|
||||
@if ($hasBlockPreviews && $item->getParentComponent()->hasPreview())
|
||||
<div @class([
|
||||
'fi-fo-builder-item-preview',
|
||||
'fi-interactive' => $hasInteractiveBlockPreviews,
|
||||
])>
|
||||
{{ $item->getParentComponent()->renderPreview($item->getRawState()) }}
|
||||
</div>
|
||||
|
||||
@if ($editActionIsVisible && !$hasInteractiveBlockPreviews)
|
||||
<div class="fi-fo-builder-item-preview-edit-overlay" role="button"
|
||||
x-on:click.stop="{{ '$wire.mountAction(\'edit\', { item: \'' . $itemKey . '\' }, { schemaComponent: \'' . $key . '\' })' }}">
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
{{ $item }}
|
||||
@endif
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (!$loop->last)
|
||||
@if ($isAddable && $addBetweenAction(['afterItem' => $itemKey])->isVisible())
|
||||
<li class="fi-fo-builder-add-between-items-ctn">
|
||||
<div class="fi-fo-builder-add-between-items">
|
||||
<div class="fi-fo-builder-block-picker-ctn">
|
||||
<x-filament-forms::builder.block-picker :action="$addBetweenAction" :after-item="$itemKey"
|
||||
:columns="$blockPickerColumns" :blocks="$blockPickerBlocks" :key="$key" :width="$blockPickerWidth">
|
||||
<x-slot name="trigger">
|
||||
{{ $addBetweenAction(['afterItem' => $itemKey]) }}
|
||||
</x-slot>
|
||||
</x-filament-forms::builder.block-picker>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@elseif (filled($labelBetweenItems))
|
||||
<li class="fi-fo-builder-label-between-items-ctn">
|
||||
<div class="fi-fo-builder-label-between-items-divider-before"></div>
|
||||
|
||||
<span class="fi-fo-builder-label-between-items">
|
||||
{{ $labelBetweenItems }}
|
||||
</span>
|
||||
|
||||
<div class="fi-fo-builder-label-between-items-divider-after"></div>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if ($isAddable && $addAction->isVisible())
|
||||
<x-filament-forms::builder.block-picker :action="$addAction" :action-alignment="$addActionAlignment" :blocks="$blockPickerBlocks"
|
||||
:columns="$blockPickerColumns" :key="$key" :width="$blockPickerWidth">
|
||||
<x-slot name="trigger">
|
||||
{{ $addAction }}
|
||||
</x-slot>
|
||||
</x-filament-forms::builder.block-picker>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
@php
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Support\Enums\GridDirection;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
@endphp
|
||||
|
||||
@props([
|
||||
'action',
|
||||
'actionAlignment' => null,
|
||||
'afterItem' => null,
|
||||
'blocks',
|
||||
'columns' => null,
|
||||
'key',
|
||||
'trigger',
|
||||
'width' => null,
|
||||
])
|
||||
|
||||
<x-filament::dropdown :placement="match ($actionAlignment) {
|
||||
Alignment::Start, Alignment::Left => 'bottom-start',
|
||||
Alignment::End, Alignment::Right => 'bottom-end',
|
||||
default => null,
|
||||
}" shift :width="$width" :attributes="\Filament\Support\prepare_inherited_attributes(
|
||||
$attributes->class([
|
||||
'fi-fo-builder-block-picker',
|
||||
$actionAlignment instanceof Alignment ? 'fi-align-' . $actionAlignment->value : $actionAlignment,
|
||||
]),
|
||||
)">
|
||||
<x-slot name="trigger">
|
||||
{{ $trigger }}
|
||||
</x-slot>
|
||||
|
||||
<x-filament::dropdown.list>
|
||||
<div {{ new ComponentAttributeBag()->grid($columns, GridDirection::Column) }}>
|
||||
@foreach ($blocks as $block)
|
||||
@php
|
||||
$blockIcon = $block->getIcon();
|
||||
|
||||
$wireClickActionArguments = ['block' => $block->getName()];
|
||||
|
||||
if (filled($afterItem)) {
|
||||
$wireClickActionArguments['afterItem'] = $afterItem;
|
||||
}
|
||||
|
||||
$wireClickActionArguments = \Illuminate\Support\Js::from($wireClickActionArguments);
|
||||
|
||||
$wireClickAction = "mountAction('{$action->getName()}', {$wireClickActionArguments}, { schemaComponent: '{$key}' })";
|
||||
@endphp
|
||||
|
||||
<x-filament::dropdown.list.item :icon="$blockIcon" x-on:click="close" :wire:click="$wireClickAction">
|
||||
{{ $block->getLabel() }}
|
||||
</x-filament::dropdown.list.item>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::dropdown.list>
|
||||
</x-filament::dropdown>
|
||||
@@ -0,0 +1,110 @@
|
||||
@php
|
||||
use Filament\Support\Enums\GridDirection;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraInputAttributeBag = $getExtraInputAttributeBag();
|
||||
$isHtmlAllowed = $isHtmlAllowed();
|
||||
$gridDirection = $getGridDirection() ?? GridDirection::Column;
|
||||
$isBulkToggleable = $isBulkToggleable();
|
||||
$isDisabled = $isDisabled();
|
||||
$isSearchable = $isSearchable();
|
||||
$statePath = $getStatePath();
|
||||
$options = $getOptions();
|
||||
$livewireKey = $getLivewireKey();
|
||||
$wireModelAttribute = $applyStateBindingModifiers('wire:model');
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('checkbox-list', 'filament/forms') }}"
|
||||
x-data="checkboxListFormComponent({
|
||||
livewireId: @js($this->getId()),
|
||||
})" {{ $getExtraAlpineAttributeBag()->class(['fi-fo-checkbox-list']) }}>
|
||||
@if (!$isDisabled)
|
||||
@if ($isSearchable)
|
||||
<x-filament::input.wrapper inline-prefix :prefix-icon="\Filament\Support\Icons\Heroicon::MagnifyingGlass" :prefix-icon-alias="\Filament\Forms\View\FormsIconAlias::COMPONENTS_CHECKBOX_LIST_SEARCH_FIELD"
|
||||
class="fi-fo-checkbox-list-search-input-wrp">
|
||||
<input placeholder="{{ $getSearchPrompt() }}" type="search"
|
||||
x-model.debounce.{{ $getSearchDebounce() }}="search"
|
||||
class="fi-input fi-input-has-inline-prefix" />
|
||||
</x-filament::input.wrapper>
|
||||
@endif
|
||||
|
||||
@if ($isBulkToggleable && count($options))
|
||||
<div x-cloak class="fi-fo-checkbox-list-actions" wire:key="{{ $livewireKey }}.actions">
|
||||
<span x-show="! areAllCheckboxesChecked" x-on:click="toggleAllCheckboxes()"
|
||||
wire:key="{{ $livewireKey }}.actions.select-all">
|
||||
{{ $getAction('selectAll') }}
|
||||
</span>
|
||||
|
||||
<span x-show="areAllCheckboxesChecked" x-on:click="toggleAllCheckboxes()"
|
||||
wire:key="{{ $livewireKey }}.actions.deselect-all">
|
||||
{{ $getAction('deselectAll') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div
|
||||
{{ $getExtraAttributeBag()->grid($getColumns(), $gridDirection)->merge(
|
||||
[
|
||||
'x-show' => $isSearchable ? 'visibleCheckboxListOptions.length' : null,
|
||||
],
|
||||
escape: false,
|
||||
)->class(['fi-fo-checkbox-list-options']) }}>
|
||||
@forelse ($options as $value => $label)
|
||||
<div wire:key="{{ $livewireKey }}.options.{{ $value }}"
|
||||
@if ($isSearchable) x-show="
|
||||
$el
|
||||
.querySelector('.fi-fo-checkbox-list-option-label')
|
||||
?.innerText.toLowerCase()
|
||||
.includes(search.toLowerCase()) ||
|
||||
$el
|
||||
.querySelector('.fi-fo-checkbox-list-option-description')
|
||||
?.innerText.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
" @endif
|
||||
class="fi-fo-checkbox-list-option-ctn">
|
||||
<label class="fi-fo-checkbox-list-option">
|
||||
<input type="checkbox"
|
||||
{{ $extraInputAttributeBag->merge(
|
||||
[
|
||||
'disabled' => $isDisabled || $isOptionDisabled($value, $label),
|
||||
'value' => e($value),
|
||||
'wire:loading.attr' => 'disabled',
|
||||
$wireModelAttribute => $statePath,
|
||||
'x-on:change' => $isBulkToggleable ? 'checkIfAllCheckboxesAreChecked()' : null,
|
||||
],
|
||||
escape: false,
|
||||
)->class(['fi-checkbox-input', 'fi-valid' => !$errors->has($statePath), 'fi-invalid' => $errors->has($statePath)]) }} />
|
||||
|
||||
<div class="fi-fo-checkbox-list-option-text">
|
||||
<span class="fi-fo-checkbox-list-option-label">
|
||||
@if ($isHtmlAllowed)
|
||||
{!! $label !!}
|
||||
@else
|
||||
{{ $label }}
|
||||
@endif
|
||||
</span>
|
||||
|
||||
@if ($hasDescription($value))
|
||||
<p class="fi-fo-checkbox-list-option-description">
|
||||
{{ $getDescription($value) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@empty
|
||||
<div wire:key="{{ $livewireKey }}.empty"></div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@if ($isSearchable)
|
||||
<div x-cloak x-show="search && ! visibleCheckboxListOptions.length"
|
||||
class="fi-fo-checkbox-list-no-search-results-message">
|
||||
{{ $getNoSearchResultsMessage() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,33 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$statePath = $getStatePath();
|
||||
$attributes = $attributes
|
||||
->merge(
|
||||
[
|
||||
'autofocus' => $isAutofocused(),
|
||||
'disabled' => $isDisabled(),
|
||||
'id' => $getId(),
|
||||
'required' => $isRequired() && !$isConcealed(),
|
||||
'wire:loading.attr' => 'disabled',
|
||||
$applyStateBindingModifiers('wire:model') => $statePath,
|
||||
],
|
||||
escape: false,
|
||||
)
|
||||
->merge($getExtraAttributes(), escape: false)
|
||||
->merge($getExtraInputAttributes(), escape: false)
|
||||
->class([
|
||||
'fi-checkbox-input',
|
||||
'fi-valid' => !$errors->has($statePath),
|
||||
'fi-invalid' => $errors->has($statePath),
|
||||
]);
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field" :inline-label-vertical-alignment="\Filament\Support\Enums\VerticalAlignment::Center">
|
||||
@if ($isInline())
|
||||
<x-slot name="labelPrefix">
|
||||
<input type="checkbox" {{ $attributes }} />
|
||||
</x-slot>
|
||||
@else
|
||||
<input type="checkbox" {{ $attributes }} />
|
||||
@endif
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,34 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributeBag = $getExtraAttributeBag();
|
||||
$isDisabled = $isDisabled();
|
||||
$isLive = $isLive();
|
||||
$isLiveOnBlur = $isLiveOnBlur();
|
||||
$isLiveDebounced = $isLiveDebounced();
|
||||
$liveDebounce = $getLiveDebounce();
|
||||
$key = $getKey();
|
||||
$language = $getLanguage();
|
||||
$statePath = $getStatePath();
|
||||
$livewireKey = $getLivewireKey();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::input.wrapper :disabled="$isDisabled" :valid="!$errors->has($statePath)" :attributes="\Filament\Support\prepare_inherited_attributes($extraAttributeBag)->class(['fi-fo-code-editor'])">
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('code-editor', 'filament/forms') }}"
|
||||
x-data="codeEditorFormComponent({
|
||||
canWrap: @js($canWrap()),
|
||||
isDisabled: @js($isDisabled),
|
||||
isLive: @js($isLive),
|
||||
isLiveDebounced: @js($isLiveDebounced),
|
||||
isLiveOnBlur: @js($isLiveOnBlur),
|
||||
liveDebounce: @js($liveDebounce),
|
||||
language: @js($language?->value),
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')", isOptimisticallyLive: false) }},
|
||||
})" wire:ignore
|
||||
wire:key="{{ $livewireKey }}.{{ substr(md5(serialize([$isDisabled, $language?->value])), 0, 64) }}"
|
||||
{{ $getExtraAlpineAttributeBag() }}>
|
||||
<div x-ref="editor" x-cloak></div>
|
||||
</div>
|
||||
</x-filament::input.wrapper>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,86 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributeBag = $getExtraAttributeBag();
|
||||
$isDisabled = $isDisabled();
|
||||
$isLive = $isLive();
|
||||
$isLiveOnBlur = $isLiveOnBlur();
|
||||
$isLiveDebounced = $isLiveDebounced();
|
||||
$isPrefixInline = $isPrefixInline();
|
||||
$isSuffixInline = $isSuffixInline();
|
||||
$liveDebounce = $getLiveDebounce();
|
||||
$prefixActions = $getPrefixActions();
|
||||
$prefixIcon = $getPrefixIcon();
|
||||
$prefixIconColor = $getPrefixIconColor();
|
||||
$prefixLabel = $getPrefixLabel();
|
||||
$suffixActions = $getSuffixActions();
|
||||
$suffixIcon = $getSuffixIcon();
|
||||
$suffixIconColor = $getSuffixIconColor();
|
||||
$suffixLabel = $getSuffixLabel();
|
||||
$statePath = $getStatePath();
|
||||
$placeholder = $getPlaceholder();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field" :inline-label-vertical-alignment="\Filament\Support\Enums\VerticalAlignment::Center">
|
||||
<x-filament::input.wrapper :disabled="$isDisabled" :inline-prefix="$isPrefixInline" :inline-suffix="$isSuffixInline" :prefix="$prefixLabel" :prefix-actions="$prefixActions"
|
||||
:prefix-icon="$prefixIcon" :prefix-icon-color="$prefixIconColor" :suffix="$suffixLabel" :suffix-actions="$suffixActions" :suffix-icon="$suffixIcon" :suffix-icon-color="$suffixIconColor"
|
||||
:valid="!$errors->has($statePath)" x-on:focus-input.stop="$el.querySelector('input')?.focus()" :attributes="\Filament\Support\prepare_inherited_attributes($extraAttributeBag)->class('fi-fo-color-picker')">
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('color-picker', 'filament/forms') }}"
|
||||
x-data="colorPickerFormComponent({
|
||||
isAutofocused: @js($isAutofocused()),
|
||||
isDisabled: @js($isDisabled),
|
||||
isLive: @js($isLive),
|
||||
isLiveDebounced: @js($isLiveDebounced),
|
||||
isLiveOnBlur: @js($isLiveOnBlur),
|
||||
liveDebounce: @js($liveDebounce),
|
||||
state: $wire.$entangle('{{ $statePath }}'),
|
||||
})" x-on:keydown.esc="isOpen() && $event.stopPropagation()"
|
||||
x-on:focusout="if (isOpen() && ! $el.contains($event.relatedTarget)) $refs.panel.close()"
|
||||
{{ $getExtraAlpineAttributeBag()->class(['fi-input-wrp-content']) }}>
|
||||
<input x-on:focus="$refs.panel.open($refs.input)" x-on:keydown.enter.prevent.stop="togglePanelVisibility()"
|
||||
x-ref="input"
|
||||
{{ $getExtraInputAttributeBag()->merge(
|
||||
[
|
||||
'autocomplete' => 'off',
|
||||
'disabled' => $isDisabled,
|
||||
'id' => $getId(),
|
||||
'placeholder' => filled($placeholder) ? e($placeholder) : null,
|
||||
'required' => $isRequired() && !$isConcealed(),
|
||||
'type' => 'text',
|
||||
'x-model' . ($isLiveDebounced ? '.debounce.' . $liveDebounce : null) => 'state',
|
||||
'x-on:blur' => $isLiveOnBlur ? 'isOpen() ? null : commitState()' : null,
|
||||
],
|
||||
escape: false,
|
||||
)->class([
|
||||
'fi-input',
|
||||
'fi-input-has-inline-prefix' =>
|
||||
$isPrefixInline && (count($prefixActions) || $prefixIcon || filled($prefixLabel)),
|
||||
'fi-input-has-inline-suffix' =>
|
||||
$isSuffixInline && (count($suffixActions) || $suffixIcon || filled($suffixLabel)),
|
||||
]) }} />
|
||||
|
||||
<div class="fi-fo-color-picker-preview my-auto me-3 size-5 shrink-0 rounded-full select-none"
|
||||
x-on:click="togglePanelVisibility()"
|
||||
x-bind:class="{
|
||||
'fi-empty': !state,
|
||||
}"
|
||||
x-bind:style="{ 'background-color': state }"></div>
|
||||
|
||||
<div wire:ignore.self wire:key="{{ $getLivewireKey() }}.panel" x-cloak
|
||||
x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }" x-ref="panel"
|
||||
class="fi-fo-color-picker-panel">
|
||||
@php
|
||||
$tag =
|
||||
match ($getFormat()) {
|
||||
'hsl' => 'hsl-string',
|
||||
'rgb' => 'rgb-string',
|
||||
'rgba' => 'rgba-string',
|
||||
default => 'hex',
|
||||
} . '-color-picker';
|
||||
@endphp
|
||||
|
||||
<{{ $tag }} x-ref="picker" color="{{ $getState() }}" />
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::input.wrapper>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,192 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$datalistOptions = $getDatalistOptions();
|
||||
$disabledDates = $getDisabledDates();
|
||||
$extraAlpineAttributes = $getExtraAlpineAttributes();
|
||||
$extraAttributeBag = $getExtraAttributeBag();
|
||||
$extraInputAttributeBag = $getExtraInputAttributeBag();
|
||||
$hasDate = $hasDate();
|
||||
$hasTime = $hasTime();
|
||||
$hasSeconds = $hasSeconds();
|
||||
$id = $getId();
|
||||
$isDisabled = $isDisabled();
|
||||
$isAutofocused = $isAutofocused();
|
||||
$isPrefixInline = $isPrefixInline();
|
||||
$isSuffixInline = $isSuffixInline();
|
||||
$maxDate = $getMaxDate();
|
||||
$minDate = $getMinDate();
|
||||
$defaultFocusedDate = $getDefaultFocusedDate();
|
||||
$prefixActions = $getPrefixActions();
|
||||
$prefixIcon = $getPrefixIcon();
|
||||
$prefixIconColor = $getPrefixIconColor();
|
||||
$prefixLabel = $getPrefixLabel();
|
||||
$suffixActions = $getSuffixActions();
|
||||
$suffixIcon = $getSuffixIcon();
|
||||
$suffixIconColor = $getSuffixIconColor();
|
||||
$suffixLabel = $getSuffixLabel();
|
||||
$statePath = $getStatePath();
|
||||
$placeholder = $getPlaceholder();
|
||||
$isReadOnly = $isReadOnly();
|
||||
$isRequired = $isRequired();
|
||||
$isConcealed = $isConcealed();
|
||||
$step = $getStep();
|
||||
$type = $getType();
|
||||
$livewireKey = $getLivewireKey();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field" :inline-label-vertical-alignment="\Filament\Support\Enums\VerticalAlignment::Center">
|
||||
<x-filament::input.wrapper :disabled="$isDisabled" :inline-prefix="$isPrefixInline" :inline-suffix="$isSuffixInline" :prefix="$prefixLabel" :prefix-actions="$prefixActions"
|
||||
:prefix-icon="$prefixIcon" :prefix-icon-color="$prefixIconColor" :suffix="$suffixLabel" :suffix-actions="$suffixActions" :suffix-icon="$suffixIcon" :suffix-icon-color="$suffixIconColor"
|
||||
:valid="!$errors->has($statePath)" x-on:focus-input.stop="$el.querySelector('input:not([type=hidden])')?.focus()"
|
||||
:attributes="\Filament\Support\prepare_inherited_attributes($extraAttributeBag)->class([
|
||||
'fi-fo-date-time-picker',
|
||||
])">
|
||||
@if ($isNative())
|
||||
<input
|
||||
{{ $extraInputAttributeBag->merge($extraAlpineAttributes, escape: false)->merge(
|
||||
[
|
||||
'autofocus' => $isAutofocused,
|
||||
'disabled' => $isDisabled,
|
||||
'id' => $id,
|
||||
'list' => $datalistOptions ? $id . '-list' : null,
|
||||
'max' => $hasTime ? $maxDate : ($maxDate ? \Carbon\Carbon::parse($maxDate)->toDateString() : null),
|
||||
'min' => $hasTime ? $minDate : ($minDate ? \Carbon\Carbon::parse($minDate)->toDateString() : null),
|
||||
'placeholder' => filled($placeholder) ? e($placeholder) : null,
|
||||
'readonly' => $isReadOnly,
|
||||
'required' => $isRequired && !$isConcealed,
|
||||
'step' => $step,
|
||||
'type' => $type,
|
||||
$applyStateBindingModifiers('wire:model') => $statePath,
|
||||
'x-data' => count($extraAlpineAttributes) ? '{}' : null,
|
||||
],
|
||||
escape: false,
|
||||
)->class([
|
||||
'fi-input',
|
||||
'fi-input-has-inline-prefix' =>
|
||||
$isPrefixInline && (count($prefixActions) || $prefixIcon || filled($prefixLabel)),
|
||||
'fi-input-has-inline-suffix' =>
|
||||
$isSuffixInline && (count($suffixActions) || $suffixIcon || filled($suffixLabel)),
|
||||
]) }} />
|
||||
@else
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('date-time-picker', 'filament/forms') }}"
|
||||
x-data="dateTimePickerFormComponent({
|
||||
defaultFocusedDate: @js($defaultFocusedDate),
|
||||
displayFormat: '{{ convert_date_format($getDisplayFormat())->to('day.js') }}',
|
||||
firstDayOfWeek: {{ $getFirstDayOfWeek() }},
|
||||
isAutofocused: @js($isAutofocused),
|
||||
locale: @js($getLocale()),
|
||||
shouldCloseOnDateSelection: @js($shouldCloseOnDateSelection()),
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
|
||||
})" wire:ignore
|
||||
wire:key="{{ $livewireKey }}.{{ substr(
|
||||
md5(serialize([$disabledDates, $isDisabled, $isReadOnly, $maxDate, $minDate, $hasDate, $hasTime, $hasSeconds])),
|
||||
0,
|
||||
64,
|
||||
) }}"
|
||||
x-on:keydown.esc="isOpen() && $event.stopPropagation()" {{ $getExtraAlpineAttributeBag() }}>
|
||||
<input x-ref="maxDate" type="hidden" value="{{ $maxDate }}" />
|
||||
|
||||
<input x-ref="minDate" type="hidden" value="{{ $minDate }}" />
|
||||
|
||||
<input x-ref="disabledDates" type="hidden" value="{{ json_encode($disabledDates) }}" />
|
||||
|
||||
<button x-ref="button" x-on:click="togglePanelVisibility()"
|
||||
x-on:keydown.enter.prevent.stop="
|
||||
if (! $el.disabled) {
|
||||
isOpen() ? selectDate() : togglePanelVisibility()
|
||||
}
|
||||
"
|
||||
x-on:keydown.arrow-left.prevent.stop="if (! $el.disabled) focusPreviousDay()"
|
||||
x-on:keydown.arrow-right.prevent.stop="if (! $el.disabled) focusNextDay()"
|
||||
x-on:keydown.arrow-up.prevent.stop="if (! $el.disabled) focusPreviousWeek()"
|
||||
x-on:keydown.arrow-down.prevent.stop="if (! $el.disabled) focusNextWeek()"
|
||||
x-on:keydown.backspace.prevent.stop="if (! $el.disabled) clearState()"
|
||||
x-on:keydown.clear.prevent.stop="if (! $el.disabled) clearState()"
|
||||
x-on:keydown.delete.prevent.stop="if (! $el.disabled) clearState()"
|
||||
aria-label="{{ $placeholder }}" type="button" tabindex="-1" @disabled($isDisabled || $isReadOnly)
|
||||
{{ $getExtraTriggerAttributeBag()->class(['fi-fo-date-time-picker-trigger']) }}>
|
||||
<input @disabled($isDisabled) readonly placeholder="{{ $placeholder }}"
|
||||
wire:key="{{ $livewireKey }}.display-text" x-model="displayText"
|
||||
@if ($id = $getId()) id="{{ $id }}" @endif
|
||||
@class(['fi-fo-date-time-picker-display-text-input']) />
|
||||
</button>
|
||||
|
||||
<div x-ref="panel" x-cloak x-float.placement.bottom-start.offset.flip.shift="{ offset: 8 }" wire:ignore
|
||||
wire:key="{{ $livewireKey }}.panel" @class(['fi-fo-date-time-picker-panel'])>
|
||||
@if ($hasDate)
|
||||
<div class="fi-fo-date-time-picker-panel-header">
|
||||
<select x-model="focusedMonth" class="fi-fo-date-time-picker-month-select">
|
||||
<template x-for="(month, index) in months">
|
||||
<option x-bind:value="index" x-text="month"></option>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<input type="number" inputmode="numeric" x-model.debounce="focusedYear"
|
||||
class="fi-fo-date-time-picker-year-input" />
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-date-time-picker-calendar-header">
|
||||
<template x-for="(day, index) in dayLabels" x-bind:key="index">
|
||||
<div x-text="day" class="fi-fo-date-time-picker-calendar-header-day"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div role="grid" class="fi-fo-date-time-picker-calendar">
|
||||
<template x-for="day in emptyDaysInFocusedMonth" x-bind:key="day">
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<template x-for="day in daysInFocusedMonth" x-bind:key="day">
|
||||
<div x-text="day" x-on:click="dayIsDisabled(day) || selectDate(day)"
|
||||
x-on:mouseenter="setFocusedDay(day)" role="option"
|
||||
x-bind:aria-selected="focusedDate.date() === day"
|
||||
x-bind:class="{
|
||||
'fi-fo-date-time-picker-calendar-day-today': dayIsToday(day),
|
||||
'fi-focused': focusedDate.date() === day,
|
||||
'fi-selected': dayIsSelected(day),
|
||||
'fi-disabled': dayIsDisabled(day),
|
||||
}"
|
||||
class="fi-fo-date-time-picker-calendar-day"></div>
|
||||
</template>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($hasTime)
|
||||
<div class="fi-fo-date-time-picker-time-inputs">
|
||||
<input max="23" min="0" step="{{ $getHoursStep() }}" type="number"
|
||||
inputmode="numeric" x-on:blur="checkTimeInputValidity" x-on:invalid="timeInputInvalid"
|
||||
x-model.debounce="hour" />
|
||||
|
||||
<span class="fi-fo-date-time-picker-time-input-separator">
|
||||
:
|
||||
</span>
|
||||
|
||||
<input max="59" min="0" step="{{ $getMinutesStep() }}" type="number"
|
||||
inputmode="numeric" x-on:blur="checkTimeInputValidity" x-on:invalid="timeInputInvalid"
|
||||
x-model.debounce="minute" />
|
||||
|
||||
@if ($hasSeconds)
|
||||
<span class="fi-fo-date-time-picker-time-input-separator">
|
||||
:
|
||||
</span>
|
||||
|
||||
<input max="59" min="0" step="{{ $getSecondsStep() }}" type="number"
|
||||
inputmode="numeric" x-on:blur="checkTimeInputValidity"
|
||||
x-on:invalid="timeInputInvalid" x-model.debounce="second" />
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</x-filament::input.wrapper>
|
||||
|
||||
@if ($datalistOptions)
|
||||
<datalist id="{{ $id }}-list">
|
||||
@foreach ($datalistOptions as $option)
|
||||
<option value="{{ $option }}" />
|
||||
@endforeach
|
||||
</datalist>
|
||||
@endif
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,202 @@
|
||||
@php
|
||||
use Filament\Support\Enums\VerticalAlignment;
|
||||
@endphp
|
||||
|
||||
@props([
|
||||
'areHtmlErrorMessagesAllowed' => null,
|
||||
'errorMessage' => null,
|
||||
'errorMessages' => null,
|
||||
'field' => null,
|
||||
'hasErrors' => true,
|
||||
'hasInlineLabel' => null,
|
||||
'hasNestedRecursiveValidationRules' => null,
|
||||
'id' => null,
|
||||
'inlineLabelVerticalAlignment' => VerticalAlignment::Start,
|
||||
'isDisabled' => null,
|
||||
'label' => null,
|
||||
'labelPrefix' => null,
|
||||
'labelSrOnly' => null,
|
||||
'labelSuffix' => null,
|
||||
'labelTag' => 'label',
|
||||
'required' => null,
|
||||
'shouldShowAllValidationMessages' => null,
|
||||
'statePath' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
if ($field) {
|
||||
$hasInlineLabel ??= $field->hasInlineLabel();
|
||||
$hasNestedRecursiveValidationRules ??=
|
||||
$field instanceof \Filament\Forms\Components\Contracts\HasNestedRecursiveValidationRules;
|
||||
$id ??= $field->getId();
|
||||
$isDisabled ??= $field->isDisabled();
|
||||
$label ??= $field->getLabel();
|
||||
$labelSrOnly ??= $field->isLabelHidden();
|
||||
$required ??= $field->isMarkedAsRequired();
|
||||
$statePath ??= $field->getStatePath();
|
||||
$areHtmlErrorMessagesAllowed ??= $field->areHtmlValidationMessagesAllowed();
|
||||
$shouldShowAllValidationMessages ??= $field->shouldShowAllValidationMessages();
|
||||
}
|
||||
|
||||
$aboveLabelSchema = $field?->getChildSchema($field::ABOVE_LABEL_SCHEMA_KEY)?->toHtmlString();
|
||||
$belowLabelSchema = $field?->getChildSchema($field::BELOW_LABEL_SCHEMA_KEY)?->toHtmlString();
|
||||
$beforeLabelSchema = $field?->getChildSchema($field::BEFORE_LABEL_SCHEMA_KEY)?->toHtmlString();
|
||||
$afterLabelSchema = $field?->getChildSchema($field::AFTER_LABEL_SCHEMA_KEY)?->toHtmlString();
|
||||
$aboveContentSchema = $field?->getChildSchema($field::ABOVE_CONTENT_SCHEMA_KEY)?->toHtmlString();
|
||||
$belowContentSchema = $field?->getChildSchema($field::BELOW_CONTENT_SCHEMA_KEY)?->toHtmlString();
|
||||
$beforeContentSchema = $field?->getChildSchema($field::BEFORE_CONTENT_SCHEMA_KEY)?->toHtmlString();
|
||||
$afterContentSchema = $field?->getChildSchema($field::AFTER_CONTENT_SCHEMA_KEY)?->toHtmlString();
|
||||
$aboveErrorMessageSchema = $field?->getChildSchema($field::ABOVE_ERROR_MESSAGE_SCHEMA_KEY)?->toHtmlString();
|
||||
$belowErrorMessageSchema = $field?->getChildSchema($field::BELOW_ERROR_MESSAGE_SCHEMA_KEY)?->toHtmlString();
|
||||
|
||||
$hasError =
|
||||
$hasErrors &&
|
||||
(filled($errorMessage) ||
|
||||
filled($errorMessages) ||
|
||||
(filled($statePath) &&
|
||||
($errors->has($statePath) || ($hasNestedRecursiveValidationRules && $errors->has("{$statePath}.*")))));
|
||||
|
||||
if ($hasError && filled($statePath) && blank($errorMessage) && blank($errorMessages)) {
|
||||
if ($shouldShowAllValidationMessages) {
|
||||
$errorMessages = $errors->has($statePath)
|
||||
? $errors->get($statePath)
|
||||
: ($hasNestedRecursiveValidationRules
|
||||
? $errors->get("{$statePath}.*")
|
||||
: []);
|
||||
|
||||
if (count($errorMessages) === 1) {
|
||||
$errorMessage = Arr::first($errorMessages);
|
||||
$errorMessages = [];
|
||||
}
|
||||
} else {
|
||||
$errorMessage = $errors->has($statePath)
|
||||
? $errors->first($statePath)
|
||||
: ($hasNestedRecursiveValidationRules
|
||||
? $errors->first("{$statePath}.*")
|
||||
: null);
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div data-field-wrapper
|
||||
{{ $attributes->merge($field?->getExtraFieldWrapperAttributes() ?? [], escape: false)->class(['fi-fo-field', 'fi-fo-field-has-inline-label' => $hasInlineLabel]) }}>
|
||||
@if (filled($label) && $labelSrOnly)
|
||||
<{{ $labelTag }}
|
||||
@if ($labelTag === 'label') for="{{ $id }}"
|
||||
@else
|
||||
id="{{ $id }}-label" @endif
|
||||
class="fi-fo-field-label fi-sr-only">
|
||||
{{ $label }}
|
||||
</{{ $labelTag }}>
|
||||
@endif
|
||||
|
||||
@if (
|
||||
(filled($label) && !$labelSrOnly) ||
|
||||
$hasInlineLabel ||
|
||||
$aboveLabelSchema ||
|
||||
$belowLabelSchema ||
|
||||
$beforeLabelSchema ||
|
||||
$afterLabelSchema ||
|
||||
$labelPrefix ||
|
||||
$labelSuffix)
|
||||
<div @class([
|
||||
'fi-fo-field-label-col',
|
||||
"fi-vertical-align-{$inlineLabelVerticalAlignment->value}" => $hasInlineLabel,
|
||||
])>
|
||||
{{ $aboveLabelSchema }}
|
||||
|
||||
<div @class([
|
||||
'fi-fo-field-label-ctn',
|
||||
$label instanceof \Illuminate\View\ComponentSlot
|
||||
? $label->attributes->get('class')
|
||||
: null,
|
||||
])>
|
||||
{{ $beforeLabelSchema }}
|
||||
|
||||
@if ((filled($label) && !$labelSrOnly) || $labelPrefix || $labelSuffix)
|
||||
<{{ $labelTag }}
|
||||
@if ($labelTag === 'label') for="{{ $id }}"
|
||||
@else
|
||||
id="{{ $id }}-label" @endif
|
||||
class="fi-fo-field-label">
|
||||
{{ $labelPrefix }}
|
||||
|
||||
@if (filled($label) && !$labelSrOnly)
|
||||
<span class="fi-fo-field-label-content">
|
||||
{{ $label }}@if ($required && !$isDisabled)
|
||||
<sup class="fi-fo-field-label-required-mark">*</sup>
|
||||
@endif
|
||||
</span>
|
||||
@endif
|
||||
|
||||
{{ $labelSuffix }}
|
||||
</{{ $labelTag }}>
|
||||
@endif
|
||||
|
||||
{{ $afterLabelSchema }}
|
||||
</div>
|
||||
|
||||
{{ $belowLabelSchema }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (
|
||||
!\Filament\Support\is_slot_empty($slot) ||
|
||||
$hasError ||
|
||||
$aboveContentSchema ||
|
||||
$belowContentSchema ||
|
||||
$beforeContentSchema ||
|
||||
$afterContentSchema ||
|
||||
$aboveErrorMessageSchema ||
|
||||
$belowErrorMessageSchema)
|
||||
<div class="fi-fo-field-content-col">
|
||||
{{ $aboveContentSchema }}
|
||||
|
||||
@if ($beforeContentSchema || $afterContentSchema)
|
||||
<div class="fi-fo-field-content-ctn">
|
||||
{{ $beforeContentSchema }}
|
||||
|
||||
<div class="fi-fo-field-content">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
{{ $afterContentSchema }}
|
||||
</div>
|
||||
@else
|
||||
{{ $slot }}
|
||||
@endif
|
||||
|
||||
{{ $belowContentSchema }}
|
||||
|
||||
@if ($hasError)
|
||||
{{ $aboveErrorMessageSchema }}
|
||||
|
||||
@if (filled($errorMessages))
|
||||
<ul data-validation-error class="fi-fo-field-wrp-error-list">
|
||||
@foreach ($errorMessages as $errorMessage)
|
||||
<li class="fi-fo-field-wrp-error-message">
|
||||
@if ($areHtmlErrorMessagesAllowed)
|
||||
{!! $errorMessage !!}
|
||||
@else
|
||||
{{ $errorMessage }}
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@elseif ($areHtmlErrorMessagesAllowed)
|
||||
<div data-validation-error class="fi-fo-field-wrp-error-message">
|
||||
{!! $errorMessage !!}
|
||||
</div>
|
||||
@else
|
||||
<p data-validation-error class="fi-fo-field-wrp-error-message">
|
||||
{{ $errorMessage }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
{{ $belowErrorMessageSchema }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,292 @@
|
||||
@php
|
||||
use Filament\Support\Enums\Alignment;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$id = $getId();
|
||||
$automaticallyCropImagesAspectRatio = $getAutomaticallyCropImagesAspectRatio();
|
||||
$automaticallyResizeImagesHeight = $getAutomaticallyResizeImagesHeight();
|
||||
$automaticallyResizeImagesWidth = $getAutomaticallyResizeImagesWidth();
|
||||
$isAvatar = $isAvatar();
|
||||
$isMultiple = $isMultiple();
|
||||
$key = $getKey();
|
||||
$statePath = $getStatePath();
|
||||
$isDisabled = $isDisabled();
|
||||
$hasImageEditor = $hasImageEditor();
|
||||
$isImageEditorExplicitlyEnabled = $isImageEditorExplicitlyEnabled();
|
||||
$hasCircleCropper = $hasCircleCropper();
|
||||
$livewireKey = $getLivewireKey();
|
||||
|
||||
$alignment = $getAlignment() ?? Alignment::Start;
|
||||
|
||||
if (!$alignment instanceof Alignment) {
|
||||
$alignment = filled($alignment) ? Alignment::tryFrom($alignment) ?? $alignment : null;
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field" label-tag="div">
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('file-upload', 'filament/forms') }}"
|
||||
x-data="fileUploadFormComponent({
|
||||
acceptedFileTypes: @js($getAcceptedFileTypes()),
|
||||
automaticallyCropImagesAspectRatio: @js($automaticallyCropImagesAspectRatio),
|
||||
automaticallyOpenImageEditorForAspectRatio: @js($getAutomaticallyOpenImageEditorForAspectRatio()),
|
||||
automaticallyResizeImagesMode: @js($getAutomaticallyResizeImagesMode()),
|
||||
automaticallyResizeImagesHeight: @js($automaticallyResizeImagesHeight),
|
||||
automaticallyResizeImagesWidth: @js($automaticallyResizeImagesWidth),
|
||||
cancelUploadUsing: (fileKey) => {
|
||||
$wire.cancelUpload(`{{ $statePath }}.${fileKey}`)
|
||||
},
|
||||
canEditSvgs: @js($canEditSvgs()),
|
||||
confirmSvgEditingMessage: @js(__('filament-forms::components.file_upload.editor.svg.messages.confirmation')),
|
||||
deleteUploadedFileUsing: async (fileKey) => {
|
||||
return await $wire.callSchemaComponentMethod(
|
||||
@js($key),
|
||||
'deleteUploadedFile', { fileKey },
|
||||
)
|
||||
},
|
||||
disabledSvgEditingMessage: @js(__('filament-forms::components.file_upload.editor.svg.messages.disabled')),
|
||||
getUploadedFilesUsing: async () => {
|
||||
return await Livewire.fireAction(
|
||||
$wire.__instance,
|
||||
'callSchemaComponentMethod',
|
||||
[@js($key), 'getUploadedFiles'], { async: true },
|
||||
)
|
||||
},
|
||||
hasCircleCropper: @js($hasCircleCropper),
|
||||
hasImageEditor: @js($hasImageEditor),
|
||||
imageEditorEmptyFillColor: @js($getImageEditorEmptyFillColor()),
|
||||
imageEditorMode: @js($getImageEditorMode()),
|
||||
imageEditorViewportHeight: @js($getImageEditorViewportHeight()),
|
||||
imageEditorViewportWidth: @js($getImageEditorViewportWidth()),
|
||||
imagePreviewHeight: @js($getImagePreviewHeight()),
|
||||
isAvatar: @js($isAvatar),
|
||||
isDeletable: @js($isDeletable()),
|
||||
isDisabled: @js($isDisabled),
|
||||
isDownloadable: @js($isDownloadable()),
|
||||
isImageEditorExplicitlyEnabled: @js($isImageEditorExplicitlyEnabled),
|
||||
isMultiple: @js($isMultiple),
|
||||
isOpenable: @js($isOpenable()),
|
||||
isPasteable: @js($isPasteable()),
|
||||
isPreviewable: @js($isPreviewable()),
|
||||
isReorderable: @js($isReorderable()),
|
||||
isSvgEditingConfirmed: @js($isSvgEditingConfirmed()),
|
||||
itemPanelAspectRatio: @js($getItemPanelAspectRatio()),
|
||||
loadingIndicatorPosition: @js($getLoadingIndicatorPosition()),
|
||||
locale: @js(app()->getLocale()),
|
||||
maxFiles: @js($maxFiles = $getMaxFiles()),
|
||||
maxFilesValidationMessage: @js($maxFiles ? trans_choice('validation.max.array', $maxFiles, ['attribute' => $getValidationAttribute(), 'max' => $maxFiles]) : null),
|
||||
maxParallelUploads: @js($getMaxParallelUploads()),
|
||||
maxSize: @js(($size = $getMaxSize()) ? "{$size}KB" : null),
|
||||
mimeTypeMap: @js($getMimeTypeMap()),
|
||||
minSize: @js(($size = $getMinSize()) ? "{$size}KB" : null),
|
||||
panelAspectRatio: @js($getPanelAspectRatio()),
|
||||
panelLayout: @js($getPanelLayout()),
|
||||
placeholder: @js($getPlaceholder()),
|
||||
removeUploadedFileButtonPosition: @js($getRemoveUploadedFileButtonPosition()),
|
||||
removeUploadedFileUsing: async (fileKey) => {
|
||||
return await $wire.callSchemaComponentMethod(
|
||||
@js($key),
|
||||
'removeUploadedFile', { fileKey },
|
||||
)
|
||||
},
|
||||
reorderUploadedFilesUsing: async (fileKeys) => {
|
||||
return await $wire.callSchemaComponentMethod(
|
||||
@js($key),
|
||||
'reorderUploadedFiles', { fileKeys },
|
||||
)
|
||||
},
|
||||
shouldAppendFiles: @js($shouldAppendFiles()),
|
||||
shouldAutomaticallyUpscaleImagesWhenResizing: @js($shouldAutomaticallyUpscaleImagesWhenResizing()),
|
||||
shouldOrientImageFromExif: @js($shouldOrientImagesFromExif()),
|
||||
shouldTransformImage: @js($automaticallyCropImagesAspectRatio || $automaticallyResizeImagesHeight || $automaticallyResizeImagesWidth),
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
|
||||
uploadButtonPosition: @js($getUploadButtonPosition()),
|
||||
uploadingMessage: @js($getUploadingMessage()),
|
||||
uploadProgressIndicatorPosition: @js($getUploadProgressIndicatorPosition()),
|
||||
uploadUsing: (fileKey, file, success, error, progress) => {
|
||||
$wire.upload(
|
||||
`{{ $statePath }}.${fileKey}`,
|
||||
file,
|
||||
() => {
|
||||
success(fileKey)
|
||||
},
|
||||
error,
|
||||
(progressEvent) => {
|
||||
progress(true, progressEvent.detail.progress, 100)
|
||||
},
|
||||
)
|
||||
},
|
||||
})" wire:ignore
|
||||
wire:key="{{ $livewireKey }}.{{ substr(md5(serialize([$isDisabled])), 0, 64) }}"
|
||||
{{ $attributes->merge(
|
||||
[
|
||||
'aria-labelledby' => "{$id}-label",
|
||||
'id' => $id,
|
||||
'role' => 'group',
|
||||
],
|
||||
escape: false,
|
||||
)->merge($getExtraAttributes(), escape: false)->merge($getExtraAlpineAttributes(), escape: false)->class([
|
||||
'fi-fo-file-upload',
|
||||
'fi-fo-file-upload-avatar' => $isAvatar,
|
||||
$alignment instanceof Alignment ? "fi-align-{$alignment->value}" : $alignment,
|
||||
]) }}>
|
||||
<div class="fi-fo-file-upload-input-ctn">
|
||||
<input x-ref="input"
|
||||
{{ $getExtraInputAttributeBag()->merge(
|
||||
[
|
||||
'aria-labelledby' => "{$id}-label",
|
||||
'disabled' => $isDisabled,
|
||||
'multiple' => $isMultiple,
|
||||
'type' => 'file',
|
||||
],
|
||||
escape: false,
|
||||
) }} />
|
||||
</div>
|
||||
|
||||
<div x-show="error" x-text="error" x-cloak class="fi-fo-file-upload-error-message"></div>
|
||||
|
||||
@if ($hasImageEditor && !$isDisabled)
|
||||
<div x-show="isEditorOpen" x-cloak x-on:click.stop="" x-trap.noscroll="isEditorOpen"
|
||||
x-on:keydown.escape.prevent.stop="closeEditor" @class([
|
||||
'fi-fo-file-upload-editor',
|
||||
'fi-fo-file-upload-editor-circle-cropper' => $hasCircleCropper,
|
||||
'fi-fo-file-upload-editor-crop-only' => !$isImageEditorExplicitlyEnabled,
|
||||
])>
|
||||
<div aria-hidden="true" class="fi-fo-file-upload-editor-overlay"></div>
|
||||
|
||||
<div class="fi-fo-file-upload-editor-window">
|
||||
<div class="fi-fo-file-upload-editor-image-ctn">
|
||||
<img x-ref="editor" class="fi-fo-file-upload-editor-image" />
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-file-upload-editor-control-panel">
|
||||
@if ($isImageEditorExplicitlyEnabled)
|
||||
<div class="fi-fo-file-upload-editor-control-panel-main">
|
||||
<div class="fi-fo-file-upload-editor-control-panel-group">
|
||||
@foreach ([
|
||||
[
|
||||
'label' => __('filament-forms::components.file_upload.editor.fields.x_position.label'),
|
||||
'ref' => 'xPositionInput',
|
||||
'unit' => __('filament-forms::components.file_upload.editor.fields.x_position.unit'),
|
||||
'alpineSaveHandler' => 'editor.setData({...editor.getData(true), x: +$el.value})',
|
||||
],
|
||||
[
|
||||
'label' => __('filament-forms::components.file_upload.editor.fields.y_position.label'),
|
||||
'ref' => 'yPositionInput',
|
||||
'unit' => __('filament-forms::components.file_upload.editor.fields.y_position.unit'),
|
||||
'alpineSaveHandler' => 'editor.setData({...editor.getData(true), y: +$el.value})',
|
||||
],
|
||||
[
|
||||
'label' => __('filament-forms::components.file_upload.editor.fields.width.label'),
|
||||
'ref' => 'widthInput',
|
||||
'unit' => __('filament-forms::components.file_upload.editor.fields.width.unit'),
|
||||
'alpineSaveHandler' => 'editor.setData({...editor.getData(true), width: +$el.value})',
|
||||
],
|
||||
[
|
||||
'label' => __('filament-forms::components.file_upload.editor.fields.height.label'),
|
||||
'ref' => 'heightInput',
|
||||
'unit' => __('filament-forms::components.file_upload.editor.fields.height.unit'),
|
||||
'alpineSaveHandler' => 'editor.setData({...editor.getData(true), height: +$el.value})',
|
||||
],
|
||||
[
|
||||
'label' => __('filament-forms::components.file_upload.editor.fields.rotation.label'),
|
||||
'ref' => 'rotationInput',
|
||||
'unit' => __('filament-forms::components.file_upload.editor.fields.rotation.unit'),
|
||||
'alpineSaveHandler' => 'editor.rotateTo(+$el.value)',
|
||||
],
|
||||
] as $input)
|
||||
<label>
|
||||
<x-filament::input.wrapper>
|
||||
<x-slot name="prefix">
|
||||
{{ $input['label'] }}
|
||||
</x-slot>
|
||||
|
||||
<input x-on:keyup.enter.prevent.stop="editor && {!! $input['alpineSaveHandler'] !!}"
|
||||
x-on:blur="editor && {!! $input['alpineSaveHandler'] !!}"
|
||||
x-ref="{{ $input['ref'] }}" x-on:keydown.enter.prevent
|
||||
type="text" class="fi-input" />
|
||||
|
||||
<x-slot name="suffix">
|
||||
{{ $input['unit'] }}
|
||||
</x-slot>
|
||||
</x-filament::input.wrapper>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-file-upload-editor-control-panel-group">
|
||||
@foreach ($getImageEditorActions() as $groupedActions)
|
||||
<div class="fi-btn-group">
|
||||
@foreach ($groupedActions as $action)
|
||||
<button aria-label="{{ $action['label'] }}" type="button"
|
||||
x-on:click.prevent.stop="{{ $action['alpineClickHandler'] }}"
|
||||
x-tooltip="{ content: @js($action['label']), theme: $store.theme }"
|
||||
class="fi-btn">
|
||||
{{ $action['iconHtml'] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if (count($aspectRatios = $getImageEditorAspectRatioOptionsForJs()))
|
||||
<div class="fi-fo-file-upload-editor-control-panel-group">
|
||||
<div class="fi-fo-file-upload-editor-control-panel-group-title">
|
||||
{{ __('filament-forms::components.file_upload.editor.aspect_ratios.label') }}
|
||||
</div>
|
||||
|
||||
@foreach (collect($aspectRatios)->chunk(5) as $ratiosChunk)
|
||||
<div class="fi-btn-group">
|
||||
@foreach ($ratiosChunk as $label => $ratio)
|
||||
<button type="button"
|
||||
x-on:click.prevent.stop="
|
||||
currentRatio = @js($label) {!! ';' !!}
|
||||
editor.setAspectRatio(@js($ratio))
|
||||
"
|
||||
x-tooltip="{ content: @js(__('filament-forms::components.file_upload.editor.actions.set_aspect_ratio.label', ['ratio' => $label])), theme: $store.theme }"
|
||||
x-bind:class="{ 'fi-active': currentRatio === @js($label) }"
|
||||
class="fi-btn">
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="fi-fo-file-upload-editor-control-panel-footer">
|
||||
@if ($isImageEditorExplicitlyEnabled)
|
||||
<button type="button" x-on:click.prevent="pond.imageEditEditor.oncancel"
|
||||
class="fi-btn">
|
||||
{{ __('filament-forms::components.file_upload.editor.actions.cancel.label') }}
|
||||
</button>
|
||||
|
||||
<button type="button" x-on:click.prevent.stop="editor.reset()"
|
||||
{{ new \Illuminate\View\ComponentAttributeBag()->color(\Filament\Support\View\Components\ButtonComponent::class, 'danger')->class(['fi-btn fi-fo-file-upload-editor-control-panel-reset-action']) }}>
|
||||
{{ __('filament-forms::components.file_upload.editor.actions.reset.label') }}
|
||||
</button>
|
||||
|
||||
<button type="button" x-on:click.prevent="saveEditor"
|
||||
{{ new \Illuminate\View\ComponentAttributeBag()->color(\Filament\Support\View\Components\ButtonComponent::class, 'success')->class(['fi-btn']) }}>
|
||||
{{ __('filament-forms::components.file_upload.editor.actions.save.label') }}
|
||||
</button>
|
||||
@else
|
||||
<button type="button" x-on:click.prevent="saveEditor"
|
||||
{{ new \Illuminate\View\ComponentAttributeBag()->color(\Filament\Support\View\Components\ButtonComponent::class, 'success')->class(['fi-btn']) }}>
|
||||
{{ __('filament-forms::components.file_upload.editor.actions.save.label') }}
|
||||
</button>
|
||||
|
||||
<button type="button" x-on:click.prevent="pond.imageEditEditor.oncancel"
|
||||
class="fi-btn">
|
||||
{{ __('filament-forms::components.file_upload.editor.actions.cancel.label') }}
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<input
|
||||
{{ $attributes->merge(
|
||||
[
|
||||
'id' => $getId(),
|
||||
'type' => 'hidden',
|
||||
$applyStateBindingModifiers('wire:model') => $getStatePath(),
|
||||
],
|
||||
escape: false,
|
||||
)->merge($getExtraAttributes(), escape: false)->class(['fi-fo-hidden']) }} />
|
||||
@@ -0,0 +1,92 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributeBag = $getExtraAttributeBag();
|
||||
$canEditKeys = $canEditKeys();
|
||||
$canEditValues = $canEditValues();
|
||||
$keyPlaceholder = $getKeyPlaceholder();
|
||||
$valuePlaceholder = $getValuePlaceholder();
|
||||
$debounce = $getLiveDebounce();
|
||||
$isAddable = $isAddable();
|
||||
$isDeletable = $isDeletable();
|
||||
$isDisabled = $isDisabled();
|
||||
$isReorderable = $isReorderable();
|
||||
$statePath = $getStatePath();
|
||||
$livewireKey = $getLivewireKey();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field" class="fi-fo-key-value-wrp">
|
||||
<x-filament::input.wrapper :disabled="$isDisabled" :valid="!$errors->has($statePath)" :attributes="\Filament\Support\prepare_inherited_attributes($extraAttributeBag)->class(['fi-fo-key-value'])">
|
||||
<div x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('key-value', 'filament/forms') }}"
|
||||
x-data="keyValueFormComponent({
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
|
||||
})" wire:ignore
|
||||
wire:key="{{ $livewireKey }}.{{ substr(md5(serialize([$isDisabled])), 0, 64) }}"
|
||||
{{ $attributes->merge($getExtraAlpineAttributes(), escape: false)->class(['fi-fo-key-value-table-ctn']) }}>
|
||||
<table class="fi-fo-key-value-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@if ($isReorderable && !$isDisabled)
|
||||
<th scope="col" x-show="rows.length" class="fi-has-action"></th>
|
||||
@endif
|
||||
|
||||
<th scope="col">
|
||||
{{ $getKeyLabel() }}
|
||||
</th>
|
||||
|
||||
<th scope="col">
|
||||
{{ $getValueLabel() }}
|
||||
</th>
|
||||
|
||||
@if ($isDeletable && !$isDisabled)
|
||||
<th scope="col" x-show="rows.length" class="fi-has-action"></th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
@if ($isReorderable) x-on:end.stop="reorderRows($event)"
|
||||
x-sortable
|
||||
data-sortable-animation-duration="{{ $getReorderAnimationDuration() }}" @endif>
|
||||
<template x-bind:key="index" x-for="(row, index) in rows">
|
||||
<tr @if ($isReorderable) x-bind:x-sortable-item="row.key" @endif>
|
||||
@if ($isReorderable && !$isDisabled)
|
||||
<td class="fi-has-action">
|
||||
<div x-sortable-handle class="fi-fo-key-value-table-row-sortable-handle">
|
||||
{{ $getAction('reorder') }}
|
||||
</div>
|
||||
</td>
|
||||
@endif
|
||||
|
||||
<td>
|
||||
<input @disabled(!$canEditKeys || $isDisabled) placeholder="{{ $keyPlaceholder }}" type="text"
|
||||
x-model="row.key" x-on:input.debounce.{{ $debounce ?? '500ms' }}="updateState"
|
||||
class="fi-input" />
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<input @disabled(!$canEditValues || $isDisabled) placeholder="{{ $valuePlaceholder }}"
|
||||
type="text" x-model="row.value"
|
||||
x-on:input.debounce.{{ $debounce ?? '500ms' }}="updateState" class="fi-input" />
|
||||
</td>
|
||||
|
||||
@if ($isDeletable && !$isDisabled)
|
||||
<td class="fi-has-action">
|
||||
<div x-on:click="deleteRow(index)">
|
||||
{{ $getAction('delete') }}
|
||||
</div>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($isAddable && !$isDisabled)
|
||||
<div x-on:click="addRow" class="fi-fo-key-value-add-action-ctn">
|
||||
{{ $getAction('add') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::input.wrapper>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,29 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributes = $getExtraAttributes();
|
||||
$id = $getId();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
@if (filled($id) || filled($extraAttributes))
|
||||
{!! '<div' !!}
|
||||
{{-- Avoid formatting issues with unclosed elements --}}
|
||||
{{ $attributes->merge(
|
||||
[
|
||||
'id' => $id,
|
||||
],
|
||||
escape: false,
|
||||
)->merge($extraAttributes, escape: false) }}
|
||||
>
|
||||
@endif
|
||||
|
||||
@if (filled($key = $getLivewireKey()))
|
||||
@livewire($getComponent(), $getComponentProperties(), key($key))
|
||||
@else
|
||||
@livewire($getComponent(), $getComponentProperties())
|
||||
@endif
|
||||
@if (filled($id) || filled($extraAttributes))
|
||||
{!! '</div>' !!}
|
||||
{{-- Avoid formatting issues with unclosed elements --}}
|
||||
@endif
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,66 @@
|
||||
@php
|
||||
$id = $getId();
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributeBag = $getExtraAttributeBag();
|
||||
$key = $getKey();
|
||||
$label = $getLabel();
|
||||
$statePath = $getStatePath();
|
||||
$fileAttachmentsMaxSize = $getFileAttachmentsMaxSize();
|
||||
$fileAttachmentsAcceptedFileTypes = $getFileAttachmentsAcceptedFileTypes();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
@if ($isDisabled())
|
||||
<div id="{{ $id }}" class="fi-fo-markdown-editor fi-disabled fi-prose">
|
||||
{!! str($getState())->markdown($getCommonMarkOptions(), $getCommonMarkExtensions())->sanitizeHtml() !!}
|
||||
</div>
|
||||
@else
|
||||
<x-filament::input.wrapper :valid="!$errors->has($statePath)" :attributes="\Filament\Support\prepare_inherited_attributes($extraAttributeBag)->class(['fi-fo-markdown-editor'])">
|
||||
<div aria-labelledby="{{ $id }}-label" id="{{ $id }}" role="group" x-load
|
||||
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('markdown-editor', 'filament/forms') }}"
|
||||
x-data="markdownEditorFormComponent({
|
||||
canAttachFiles: @js($hasFileAttachments()),
|
||||
isLiveDebounced: @js($isLiveDebounced()),
|
||||
isLiveOnBlur: @js($isLiveOnBlur()),
|
||||
label: @js($label),
|
||||
liveDebounce: @js($getNormalizedLiveDebounce()),
|
||||
maxHeight: @js($getMaxHeight()),
|
||||
minHeight: @js($getMinHeight()),
|
||||
placeholder: @js($getPlaceholder()),
|
||||
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')", isOptimisticallyLive: false) }},
|
||||
toolbarButtons: @js($getToolbarButtons()),
|
||||
translations: @js(__('filament-forms::components.markdown_editor')),
|
||||
uploadFileAttachmentUsing: async (file, onSuccess, onError) => {
|
||||
const acceptedTypes = @js($fileAttachmentsAcceptedFileTypes)
|
||||
|
||||
if (acceptedTypes && !acceptedTypes.includes(file.type)) {
|
||||
return onError(@js($fileAttachmentsAcceptedFileTypes ? __('filament-forms::components.markdown_editor.file_attachments_accepted_file_types_message', ['values' => implode(', ', $fileAttachmentsAcceptedFileTypes)]) : null))
|
||||
}
|
||||
|
||||
const maxSize = @js($fileAttachmentsMaxSize)
|
||||
|
||||
if (maxSize && file.size > +maxSize * 1024) {
|
||||
return onError(@js($fileAttachmentsMaxSize ? trans_choice('filament-forms::components.markdown_editor.file_attachments_max_size_message', $fileAttachmentsMaxSize, ['max' => $fileAttachmentsMaxSize]) : null))
|
||||
}
|
||||
|
||||
$wire.upload(`componentFileAttachments.{{ $statePath }}`, file, () => {
|
||||
$wire
|
||||
.callSchemaComponentMethod(
|
||||
'{{ $key }}',
|
||||
'saveUploadedFileAttachmentAndGetUrl',
|
||||
)
|
||||
.then((url) => {
|
||||
if (!url) {
|
||||
return onError()
|
||||
}
|
||||
|
||||
onSuccess(url)
|
||||
})
|
||||
})
|
||||
},
|
||||
})" wire:ignore {{ $getExtraAlpineAttributeBag() }}>
|
||||
<textarea x-ref="editor" x-cloak></textarea>
|
||||
</div>
|
||||
</x-filament::input.wrapper>
|
||||
@endif
|
||||
</x-dynamic-component>
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
@php
|
||||
use Filament\Forms\Components\TableSelect\Livewire\TableSelectLivewireComponent;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraAttributes = $getExtraAttributes();
|
||||
$id = $getId();
|
||||
$isDisabled = $isDisabled();
|
||||
$isMultiple = $isMultiple();
|
||||
$hasBadges = $hasBadges();
|
||||
$badgeColor = $getBadgeColor();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div
|
||||
{{ $attributes->merge(
|
||||
[
|
||||
'id' => $id,
|
||||
],
|
||||
escape: false,
|
||||
)->merge($extraAttributes, escape: false)->class([
|
||||
'fi-fo-modal-table-select',
|
||||
'fi-fo-modal-table-select-disabled' => $isDisabled,
|
||||
'fi-fo-modal-table-select-multiple' => $isMultiple,
|
||||
]) }}>
|
||||
@if (
|
||||
(!$isMultiple && filled($optionLabel = $getOptionLabel())) ||
|
||||
($isMultiple && filled($optionLabels = $getOptionLabels())))
|
||||
@if ($isMultiple && $hasBadges)
|
||||
<div class="fi-fo-modal-table-select-badges-ctn">
|
||||
@foreach ($optionLabels as $optionLabel)
|
||||
@if ($hasBadges)
|
||||
<x-filament::badge :color="$badgeColor">
|
||||
{{ $optionLabel }}
|
||||
</x-filament::badge>
|
||||
@else
|
||||
{{ $optionLabel }}
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
@if ($hasBadges)
|
||||
<x-filament::badge :color="$badgeColor">
|
||||
{{ $optionLabel }}
|
||||
</x-filament::badge>
|
||||
@elseif ($isMultiple)
|
||||
@foreach ($optionLabels as $optionLabel)
|
||||
{{ $optionLabel . ($loop->last ? '' : ', ') }}
|
||||
@endforeach
|
||||
@else
|
||||
{{ $optionLabel }}
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@elseif (filled($placeholder = $getPlaceholder()))
|
||||
<div class="fi-fo-modal-table-select-placeholder">
|
||||
{{ $placeholder }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (!$isDisabled)
|
||||
<div>
|
||||
{{ $getAction('select') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$placeholder = $getPlaceholder();
|
||||
$extraAttributes = $getExtraAttributeBag()->merge($getExtraAlpineAttributes(), escape: false);
|
||||
$extraInputAttributes = $getExtraInputAttributeBag()->merge(
|
||||
[
|
||||
'autocomplete' => false,
|
||||
'autofocus' => $isAutofocused(),
|
||||
'disabled' => $isDisabled(),
|
||||
'id' => $getId(),
|
||||
'length' => $getLength(),
|
||||
'placeholder' => filled($placeholder) ? e($placeholder) : null,
|
||||
'readonly' => $isReadOnly(),
|
||||
'required' => $isRequired() && !$isConcealed(),
|
||||
$applyStateBindingModifiers('wire:model') => $getStatePath(),
|
||||
],
|
||||
escape: false,
|
||||
);
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::input.one-time-code :attributes="\Filament\Support\prepare_inherited_attributes($extraAttributes)">
|
||||
<x-slot name="input" :attributes="\Filament\Support\prepare_inherited_attributes($extraInputAttributes)"></x-slot>
|
||||
</x-filament::input.one-time-code>
|
||||
</x-dynamic-component>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
@props([
|
||||
'field' => null,
|
||||
'id' => null,
|
||||
'label' => null,
|
||||
'labelTag' => 'label',
|
||||
])
|
||||
|
||||
@php
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
if ($field) {
|
||||
$id ??= $field->getId();
|
||||
$label ??= $field->getLabel();
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div data-field-wrapper
|
||||
{{ new ComponentAttributeBag()->merge($field?->getExtraFieldWrapperAttributes() ?? [], escape: false)->class(['fi-fo-field']) }}>
|
||||
@if (filled($label))
|
||||
<{{ $labelTag }}
|
||||
@if ($labelTag === 'label') for="{{ $id }}"
|
||||
@else
|
||||
id="{{ $id }}-label" @endif
|
||||
class="fi-fo-field-label fi-sr-only">
|
||||
{{ $label }}
|
||||
</{{ $labelTag }}>
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
@php
|
||||
use Filament\Support\Enums\GridDirection;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$extraInputAttributeBag = $getExtraInputAttributeBag();
|
||||
$gridDirection = $getGridDirection() ?? GridDirection::Column;
|
||||
$id = $getId();
|
||||
$isDisabled = $isDisabled();
|
||||
$isInline = $isInline();
|
||||
$statePath = $getStatePath();
|
||||
$wireModelAttribute = $applyStateBindingModifiers('wire:model');
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div
|
||||
{{ $getExtraAttributeBag()->when(!$isInline, fn(ComponentAttributeBag $attributes) => $attributes->grid($getColumns(), $gridDirection))->class(['fi-fo-radio', 'fi-inline' => $isInline]) }}>
|
||||
@foreach ($getOptions() as $value => $label)
|
||||
@php
|
||||
$inputAttributes = $extraInputAttributeBag->merge(
|
||||
[
|
||||
'autofocus' => $loop->first && $isAutofocused(),
|
||||
'disabled' => $isDisabled || $isOptionDisabled($value, $label),
|
||||
'id' => e($id . '-' . $value),
|
||||
'name' => $id,
|
||||
'value' => e($value),
|
||||
$wireModelAttribute => $statePath,
|
||||
],
|
||||
escape: false,
|
||||
);
|
||||
@endphp
|
||||
|
||||
<label class="fi-fo-radio-label">
|
||||
<input type="radio"
|
||||
{{ $inputAttributes->class([
|
||||
'fi-radio-input',
|
||||
'fi-valid' => !$errors->has($statePath),
|
||||
'fi-invalid' => $errors->has($statePath),
|
||||
]) }} />
|
||||
|
||||
<div class="fi-fo-radio-label-text">
|
||||
<p>
|
||||
{{ $label }}
|
||||
</p>
|
||||
|
||||
@if ($hasDescription($value))
|
||||
<p class="fi-fo-radio-label-description">
|
||||
{{ $getDescription($value) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,224 @@
|
||||
@php
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
|
||||
$items = $getItems();
|
||||
|
||||
$addAction = $getAction($getAddActionName());
|
||||
$addActionAlignment = $getAddActionAlignment();
|
||||
$addBetweenAction = $getAction($getAddBetweenActionName());
|
||||
$cloneAction = $getAction($getCloneActionName());
|
||||
$collapseAllAction = $getAction($getCollapseAllActionName());
|
||||
$expandAllAction = $getAction($getExpandAllActionName());
|
||||
$deleteAction = $getAction($getDeleteActionName());
|
||||
$moveDownAction = $getAction($getMoveDownActionName());
|
||||
$moveUpAction = $getAction($getMoveUpActionName());
|
||||
$reorderAction = $getAction($getReorderActionName());
|
||||
$extraItemActions = $getExtraItemActions();
|
||||
|
||||
$hasItemNumbers = $hasItemNumbers();
|
||||
$hasItemHeaders = $hasItemHeaders();
|
||||
$isAddable = $isAddable();
|
||||
$isCloneable = $isCloneable();
|
||||
$isCollapsible = $isCollapsible();
|
||||
$isDeletable = $isDeletable();
|
||||
$isReorderableWithButtons = $isReorderableWithButtons();
|
||||
$isReorderableWithDragAndDrop = $isReorderableWithDragAndDrop();
|
||||
|
||||
$collapseAllActionIsVisible = $isCollapsible && $collapseAllAction->isVisible();
|
||||
$expandAllActionIsVisible = $isCollapsible && $expandAllAction->isVisible();
|
||||
$persistCollapsed = $shouldPersistCollapsed();
|
||||
|
||||
$key = $getKey();
|
||||
$statePath = $getStatePath();
|
||||
|
||||
$itemLabelHeadingTag = $getHeadingTag();
|
||||
$isItemLabelTruncated = $isItemLabelTruncated();
|
||||
$labelBetweenItems = $getLabelBetweenItems();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div
|
||||
{{ $attributes->merge($getExtraAttributes(), escape: false)->class(['fi-fo-repeater', 'fi-collapsible' => $isCollapsible]) }}>
|
||||
@if ($collapseAllActionIsVisible || $expandAllActionIsVisible)
|
||||
<div @class(['fi-fo-repeater-actions', 'fi-hidden' => count($items) < 2])>
|
||||
@if ($collapseAllActionIsVisible)
|
||||
<span x-on:click="$dispatch('repeater-collapse', '{{ $statePath }}')">
|
||||
{{ $collapseAllAction }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if ($expandAllActionIsVisible)
|
||||
<span x-on:click="$dispatch('repeater-expand', '{{ $statePath }}')">
|
||||
{{ $expandAllAction }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (count($items))
|
||||
<ul x-sortable
|
||||
{{ new ComponentAttributeBag()->grid($getGridColumns())->merge(
|
||||
[
|
||||
'data-sortable-animation-duration' => $getReorderAnimationDuration(),
|
||||
'x-on:end.stop' =>
|
||||
'$wire.mountAction(\'reorder\', { items: $event.target.sortable.toArray() }, { schemaComponent: \'' .
|
||||
$key .
|
||||
'\' })',
|
||||
],
|
||||
escape: false,
|
||||
)->class(['fi-fo-repeater-items']) }}>
|
||||
@foreach ($items as $itemKey => $item)
|
||||
@php
|
||||
$itemLabel = $getItemLabel($itemKey, $loop->index);
|
||||
$visibleExtraItemActions = array_filter(
|
||||
$extraItemActions,
|
||||
fn(Action $action): bool => $action(['item' => $itemKey])->isVisible(),
|
||||
);
|
||||
$cloneAction = $cloneAction(['item' => $itemKey]);
|
||||
$cloneActionIsVisible = $isCloneable && $cloneAction->isVisible();
|
||||
$deleteAction = $deleteAction(['item' => $itemKey]);
|
||||
$deleteActionIsVisible = $isDeletable && $deleteAction->isVisible();
|
||||
$moveDownAction = $moveDownAction(['item' => $itemKey])->disabled($loop->last);
|
||||
$moveDownActionIsVisible = $isReorderableWithButtons && $moveDownAction->isVisible();
|
||||
$moveUpAction = $moveUpAction(['item' => $itemKey])->disabled($loop->first);
|
||||
$moveUpActionIsVisible = $isReorderableWithButtons && $moveUpAction->isVisible();
|
||||
$reorderActionIsVisible = $isReorderableWithDragAndDrop && $reorderAction->isVisible();
|
||||
$hasItemHeader =
|
||||
$hasItemHeaders &&
|
||||
($reorderActionIsVisible ||
|
||||
$moveUpActionIsVisible ||
|
||||
$moveDownActionIsVisible ||
|
||||
filled($itemLabel) ||
|
||||
$cloneActionIsVisible ||
|
||||
$deleteActionIsVisible ||
|
||||
$isCollapsible ||
|
||||
$visibleExtraItemActions);
|
||||
@endphp
|
||||
|
||||
<li wire:ignore.self wire:key="{{ $item->getLivewireKey() }}.item" x-data="{
|
||||
isCollapsed: @if ($persistCollapsed) $persist(@js($isCollapsed($item))).as(`repeater-${@js($key)}-${@js($itemKey)}-isCollapsed`) @else @js($isCollapsed($item)) @endif,
|
||||
}"
|
||||
x-on:repeater-expand.window="$event.detail === '{{ $statePath }}' && (isCollapsed = false)"
|
||||
x-on:repeater-collapse.window="$event.detail === '{{ $statePath }}' && (isCollapsed = true)"
|
||||
x-on:expand="isCollapsed = false" x-sortable-item="{{ $itemKey }}"
|
||||
@class([
|
||||
'fi-fo-repeater-item',
|
||||
'fi-fo-repeater-item-has-header' => $hasItemHeader,
|
||||
]) x-bind:class="{ 'fi-collapsed': isCollapsed }">
|
||||
@if ($hasItemHeader)
|
||||
<div @if ($isCollapsible) x-on:click.stop="isCollapsed = !isCollapsed" @endif
|
||||
class="fi-fo-repeater-item-header">
|
||||
@if ($reorderActionIsVisible || $moveUpActionIsVisible || $moveDownActionIsVisible)
|
||||
<ul class="fi-fo-repeater-item-header-start-actions">
|
||||
@if ($reorderActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $reorderAction->extraAttributes(['x-sortable-handle' => true], merge: true) }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($moveUpActionIsVisible || $moveDownActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $moveUpAction }}
|
||||
</li>
|
||||
|
||||
<li x-on:click.stop>
|
||||
{{ $moveDownAction }}
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if (filled($itemLabel))
|
||||
<{{ $itemLabelHeadingTag }} @class([
|
||||
'fi-fo-repeater-item-header-label',
|
||||
'fi-truncated' => $isItemLabelTruncated,
|
||||
])>
|
||||
{{ $itemLabel }}
|
||||
|
||||
@if ($hasItemNumbers)
|
||||
{{ $loop->iteration }}
|
||||
@endif
|
||||
</{{ $itemLabelHeadingTag }}>
|
||||
@endif
|
||||
|
||||
@if ($cloneActionIsVisible || $deleteActionIsVisible || $isCollapsible || $visibleExtraItemActions)
|
||||
<ul class="fi-fo-repeater-item-header-end-actions">
|
||||
@foreach ($visibleExtraItemActions as $extraItemAction)
|
||||
<li x-on:click.stop>
|
||||
{{ $extraItemAction(['item' => $itemKey]) }}
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
@if ($cloneActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $cloneAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($deleteActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $deleteAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($isCollapsible)
|
||||
<li class="fi-fo-repeater-item-header-collapsible-actions"
|
||||
x-on:click.stop="isCollapsed = !isCollapsed">
|
||||
<div class="fi-fo-repeater-item-header-collapse-action">
|
||||
{{ $getAction('collapse') }}
|
||||
</div>
|
||||
|
||||
<div class="fi-fo-repeater-item-header-expand-action">
|
||||
{{ $getAction('expand') }}
|
||||
</div>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div x-show="! isCollapsed" class="fi-fo-repeater-item-content">
|
||||
{{ $item }}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (!$loop->last)
|
||||
@if ($isAddable && $addBetweenAction(['afterItem' => $itemKey])->isVisible())
|
||||
<li class="fi-fo-repeater-add-between-items-ctn">
|
||||
<div class="fi-fo-repeater-add-between-items">
|
||||
{{ $addBetweenAction(['afterItem' => $itemKey]) }}
|
||||
</div>
|
||||
</li>
|
||||
@elseif (filled($labelBetweenItems))
|
||||
<li class="fi-fo-repeater-label-between-items-ctn">
|
||||
<div class="fi-fo-repeater-label-between-items-divider-before"></div>
|
||||
|
||||
<span class="fi-fo-repeater-label-between-items">
|
||||
{{ $labelBetweenItems }}
|
||||
</span>
|
||||
|
||||
<div class="fi-fo-repeater-label-between-items-divider-after"></div>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if ($isAddable && $addAction->isVisible())
|
||||
<div @class([
|
||||
'fi-fo-repeater-add',
|
||||
$addActionAlignment instanceof Alignment
|
||||
? 'fi-align-' . $addActionAlignment->value
|
||||
: $addActionAlignment,
|
||||
])>
|
||||
{{ $addAction }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,125 @@
|
||||
@php
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Illuminate\View\ComponentAttributeBag;
|
||||
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
|
||||
$items = $getItems();
|
||||
|
||||
$addAction = $getAction($getAddActionName());
|
||||
$addActionAlignment = $getAddActionAlignment();
|
||||
$cloneAction = $getAction($getCloneActionName());
|
||||
$deleteAction = $getAction($getDeleteActionName());
|
||||
$moveDownAction = $getAction($getMoveDownActionName());
|
||||
$moveUpAction = $getAction($getMoveUpActionName());
|
||||
$reorderAction = $getAction($getReorderActionName());
|
||||
$extraItemActions = $getExtraItemActions();
|
||||
|
||||
$isAddable = $isAddable();
|
||||
$isCloneable = $isCloneable();
|
||||
$isDeletable = $isDeletable();
|
||||
$isReorderableWithButtons = $isReorderableWithButtons();
|
||||
$isReorderableWithDragAndDrop = $isReorderableWithDragAndDrop();
|
||||
|
||||
$key = $getKey();
|
||||
$statePath = $getStatePath();
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div {{ $attributes->merge($getExtraAttributes(), escape: false)->class(['fi-fo-simple-repeater']) }}>
|
||||
@if (count($items))
|
||||
<ul x-sortable
|
||||
{{ new ComponentAttributeBag()->grid($getGridColumns())->merge(
|
||||
[
|
||||
'data-sortable-animation-duration' => $getReorderAnimationDuration(),
|
||||
'x-on:end.stop' =>
|
||||
'$wire.mountAction(\'reorder\', { items: $event.target.sortable.toArray() }, { schemaComponent: \'' .
|
||||
$key .
|
||||
'\' })',
|
||||
],
|
||||
escape: false,
|
||||
)->class(['fi-fo-simple-repeater-items']) }}>
|
||||
@foreach ($items as $itemKey => $item)
|
||||
@php
|
||||
$visibleExtraItemActions = array_filter(
|
||||
$extraItemActions,
|
||||
fn(Action $action): bool => $action(['item' => $itemKey])->isVisible(),
|
||||
);
|
||||
$cloneAction = $cloneAction(['item' => $itemKey]);
|
||||
$cloneActionIsVisible = $isCloneable && $cloneAction->isVisible();
|
||||
$deleteAction = $deleteAction(['item' => $itemKey]);
|
||||
$deleteActionIsVisible = $isDeletable && $deleteAction->isVisible();
|
||||
$moveDownAction = $moveDownAction(['item' => $itemKey])->disabled($loop->last);
|
||||
$moveDownActionIsVisible = $isReorderableWithButtons && $moveDownAction->isVisible();
|
||||
$moveUpAction = $moveUpAction(['item' => $itemKey])->disabled($loop->first);
|
||||
$moveUpActionIsVisible = $isReorderableWithButtons && $moveUpAction->isVisible();
|
||||
$reorderActionIsVisible = $isReorderableWithDragAndDrop && $reorderAction->isVisible();
|
||||
@endphp
|
||||
|
||||
<li wire:key="{{ $item->getLivewireKey() }}.item" x-sortable-item="{{ $itemKey }}"
|
||||
class="fi-fo-simple-repeater-item">
|
||||
<div class="fi-fo-simple-repeater-item-content">
|
||||
{{ $item }}
|
||||
</div>
|
||||
|
||||
@if (
|
||||
$reorderActionIsVisible ||
|
||||
$moveUpActionIsVisible ||
|
||||
$moveDownActionIsVisible ||
|
||||
$cloneActionIsVisible ||
|
||||
$deleteActionIsVisible ||
|
||||
$visibleExtraItemActions)
|
||||
<ul class="fi-fo-simple-repeater-item-actions">
|
||||
@if ($reorderActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $reorderAction->extraAttributes(['x-sortable-handle' => true], merge: true) }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($moveUpActionIsVisible || $moveDownActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $moveUpAction }}
|
||||
</li>
|
||||
|
||||
<li x-on:click.stop>
|
||||
{{ $moveDownAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@foreach ($visibleExtraItemActions as $extraItemAction)
|
||||
<li x-on:click.stop>
|
||||
{{ $extraItemAction(['item' => $itemKey]) }}
|
||||
</li>
|
||||
@endforeach
|
||||
|
||||
@if ($cloneActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $cloneAction }}
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if ($deleteActionIsVisible)
|
||||
<li x-on:click.stop>
|
||||
{{ $deleteAction }}
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if ($isAddable && $addAction->isVisible())
|
||||
<div @class([
|
||||
'fi-fo-simple-repeater-add',
|
||||
$addActionAlignment instanceof Alignment
|
||||
? 'fi-align-' . $addActionAlignment->value
|
||||
: $addActionAlignment,
|
||||
])>
|
||||
{{ $addAction }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user