Merge branch 'main' into states

# Conflicts:
#	app/Domain/CalendarGenerator.php
#	app/Domain/VacationRequestStateManager.php
#	app/Eloquent/Models/VacationRequest.php
#	app/Eloquent/Observers/VacationRequestObserver.php
#	composer.json
#	composer.lock
#	tests/Feature/VacationRequestTest.php
This commit is contained in:
Adrian Hopek
2022-02-24 09:38:50 +01:00
52 changed files with 2572 additions and 155 deletions

View File

@@ -0,0 +1,12 @@
import {computed} from 'vue'
import {usePage} from '@inertiajs/inertia-vue3'
export default function useCurrentYearPeriodInfo() {
const minDate = computed(() => new Date(usePage().props.value.years.current, 0, 1))
const maxDate = computed(() => new Date(usePage().props.value.years.current, 11, 31))
return {
minDate,
maxDate,
}
}

View File

@@ -1,10 +1,20 @@
<template>
<InertiaHead title="Kalendarz urlopów" />
<div class="bg-white shadow-md">
<div class="p-4 sm:px-6">
<h2 class="text-lg leading-6 font-medium text-gray-900">
Kalendarz urlopów
</h2>
<div class="flex justify-between items-center p-4 sm:px-6">
<div>
<h2 class="text-lg leading-6 font-medium text-gray-900">
Kalendarz urlopów
</h2>
</div>
<div>
<a
:href="`/timesheet/${selectedMonth.value}`"
class="inline-flex items-center px-4 py-3 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blumilk-600 hover:bg-blumilk-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
>
Pobierz plik excel
</a>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-center table-fixed text-sm border border-gray-300">
@@ -19,7 +29,7 @@
<MenuButton
class="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500"
>
{{ selectedMonth.name }}
{{ selectedMonth.name }} {{ years.current }}
<ChevronDownIcon class="-mr-1 ml-2 h-5 w-5" />
</MenuButton>
</div>
@@ -42,8 +52,7 @@
v-slot="{ active }"
>
<InertiaLink
href="/vacation-calendar"
:data="{ month: month.value }"
:href="`/vacation-calendar/${month.value}`"
:class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'flex w-full font-normal px-4 py-2 text-sm']"
>
{{ month.name }}
@@ -151,6 +160,10 @@ export default {
type: String,
default: () => 'january',
},
years: {
type: Object,
default: () => null,
},
},
setup(props) {
const {getMonths, findMonth} = useMonthInfo()

View File

@@ -48,6 +48,7 @@
id="date"
v-model="form.date"
placeholder="Wybierz datę"
:config="{minDate, maxDate}"
class="block w-full max-w-lg shadow-sm rounded-md sm:text-sm"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.date, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.date }"
/>
@@ -81,8 +82,9 @@
</template>
<script>
import { useForm } from '@inertiajs/inertia-vue3'
import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import useCurrentYearPeriodInfo from '@/Composables/yearPeriodInfo'
export default {
name: 'HolidayCreate',
@@ -95,7 +97,13 @@ export default {
date: null,
})
return { form }
const {minDate, maxDate} = useCurrentYearPeriodInfo()
return {
form,
minDate,
maxDate,
}
},
methods: {
createHoliday() {

View File

@@ -28,6 +28,76 @@
</div>
</div>
</div>
<Listbox
v-model="form.user"
as="div"
class="sm:grid sm:grid-cols-3 py-4 items-center"
>
<ListboxLabel class="block text-sm font-medium text-gray-700">
Osoba składająca wniosek
</ListboxLabel>
<div class="mt-1 relative sm:mt-0 sm:col-span-2">
<ListboxButton
class="bg-white relative w-full max-w-lg border rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default sm:text-sm focus:ring-1"
:class="{ 'border-red-300 text-red-900 focus:outline-none focus:ring-red-500 focus:border-red-500': form.errors.type, 'focus:ring-blumilk-500 focus:border-blumilk-500 sm:text-sm border-gray-300': !form.errors.type }"
>
<span class="flex items-center">
<img
:src="form.user.avatar"
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span class="ml-3 block truncate">{{ form.user.name }}</span>
</span>
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon class="h-5 w-5 text-gray-400" />
</span>
</ListboxButton>
<transition
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute z-10 mt-1 w-full max-w-lg bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="user in users.data"
:key="user.id"
v-slot="{ active, selected }"
as="template"
:value="user"
>
<li :class="[active ? 'text-white bg-blumilk-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9']">
<div class="flex items-center">
<img
:src="user.avatar"
alt=""
class="flex-shrink-0 h-6 w-6 rounded-full"
>
<span :class="[selected ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']">
{{ user.name }}
</span>
</div>
<span
v-if="selected"
:class="[active ? 'text-white' : 'text-blumilk-600', 'absolute inset-y-0 right-0 flex items-center pr-4']"
>
<CheckIcon class="h-5 w-5" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
<p
v-if="form.errors.type"
class="mt-2 text-sm text-red-600"
>
{{ form.errors.type }}
</p>
</div>
</Listbox>
<Listbox
v-model="form.type"
as="div"
@@ -161,6 +231,25 @@
/>
</div>
</div>
<div class="sm:grid sm:grid-cols-3 py-4 items-center">
<label
for="flowSkipped"
class="block text-sm font-medium text-gray-700"
>
Natychmiastowo zatwierdź wniosek
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
<Switch
id="flowSkipped"
v-model="form.flowSkipped"
:class="[form.flowSkipped ? 'bg-blumilk-500' : 'bg-gray-200', 'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blumilk-500']"
>
<span
:class="[form.flowSkipped ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200']"
/>
</Switch>
</div>
</div>
<div class="flex justify-end py-3">
<div class="space-x-3">
<InertiaLink
@@ -183,16 +272,18 @@
</template>
<script>
import {useForm, usePage} from '@inertiajs/inertia-vue3'
import {useForm} from '@inertiajs/inertia-vue3'
import FlatPickr from 'vue-flatpickr-component'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions} from '@headlessui/vue'
import {Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions, Switch} from '@headlessui/vue'
import {CheckIcon, SelectorIcon, XCircleIcon} from '@heroicons/vue/solid'
import {reactive, ref, computed} from 'vue'
import {reactive, ref} from 'vue'
import axios from 'axios'
import useCurrentYearPeriodInfo from '@/Composables/yearPeriodInfo'
export default {
name: 'VacationRequestCreate',
components: {
Switch,
FlatPickr,
Listbox,
ListboxButton,
@@ -204,6 +295,14 @@ export default {
XCircleIcon,
},
props: {
auth: {
type: Object,
default: () => null,
},
users: {
type: Object,
default: () => null,
},
vacationTypes: {
type: Object,
default: () => null,
@@ -215,15 +314,16 @@ export default {
},
setup(props) {
const form = useForm({
user: props.users.data.find(user => user.id === props.auth.user.id),
from: null,
to: null,
type: props.vacationTypes[0],
comment: null,
flowSkipped: false,
})
const estimatedDays = ref([])
const minDate = computed(() => new Date(usePage().props.value.years.current, 0, 1))
const maxDate = computed(() => new Date(usePage().props.value.years.current, 11, 31))
const {minDate, maxDate} = useCurrentYearPeriodInfo()
const disableDates = [
date => (date.getDay() === 0 || date.getDay() === 6),
@@ -254,6 +354,7 @@ export default {
.transform(data => ({
...data,
type: data.type.value,
user: data.user.id,
}))
.post('/vacation-requests')
},

View File

@@ -390,4 +390,4 @@ export default {
}
},
}
</script>
</script>

View File

@@ -42,7 +42,35 @@
"Vacation limits have been updated.": "Limity urlopów zostały zaktualizowane.",
"Vacation request has been created.": "Wniosek urlopowy został utworzony.",
"Vacation request has been accepted.": "Wniosek urlopowy został zaakceptowany.",
"Vacation request has been approved.": "Wniosek urlopowy został zatwierdzony.",
"Vacation request has been rejected.": "Wniosek urlopowy został odrzucony.",
"Vacation request has been cancelled.": "Wniosek urlopowy został anulowany."
"Vacation request has been cancelled.": "Wniosek urlopowy został anulowany.",
"Sum:": "Suma:",
"Date": "Data",
"Day of week": "Dzień tygodnia",
"Start date": "Data rozpoczęcia",
"End date": "Data zakończenia",
"Worked hours": "Liczba godzin",
"Hi :user!": "Cześć :user!",
"The vacation request :title has changed state to :state.": "Wniosek urlopowy :title zmienił status na :state.",
"Vacation request :title": "Wniosek urlopowy :title",
"Regards": "Z poważaniem",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Jeżeli masz problemy z kliknięciem przycisku \":actionText\", skopiuj i wklej poniższy adres w pasek przeglądarki:",
"All rights reserved.": "Wszelkie prawa zastrzeżone",
"Show vacation request": "Pokaż wniosek",
"Vacation request :title has been created" : "Wniosek :title został utworzony",
"The vacation request :title has been created correctly in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title.",
"Vacation type: :type": "Rodzaj wniosku: :type",
"From :from to :to (number of days: :days)": "Od :from do :to (liczba dni: :days)",
"Click here for details": "Kliknij, aby zobaczyć szczegóły",
"Vacation request :title is waiting for your approval": "Wniosek urlopowy :title czeka na zaakceptowanie",
"The vacation request :title from user: :requester is waiting for your approval.": "Wniosek urlopowy :title od użytkownika :requester czeka na Twoją akceptację.",
"Vacation request :title has been approved": "Wniosek urlopowy :title został zatwierdzony",
"The vacation request :title for user :requester has been approved.": "Wniosek urlopowy :title od użytkownika :requester został zatwierdzony.",
"Vacation request :title has been cancelled": "Wniosek urlopowy :title został anulowany",
"The vacation request :title for user :requester has been cancelled.": "Wniosek urlopowy :title od użytkownika :requester został anulowany.",
"Vacation request :title has been rejected": "Wniosek urlopowy :title został odrzucony",
"The vacation request :title for user :requester has been rejected.": "Wniosek urlopowy :title od użytkownika :requester został odrzucony.",
"Vacation request :title has been created on your behalf": "Wniosek urlopowy :title został utworzony w Twoim imieniu",
"The vacation request :title has been created correctly by user :creator on your behalf in the :appName.": "W systemie :appName został poprawnie utworzony wniosek urlopowy :title w Twoim imieniu przez użytkownika :creator."
}

View File

@@ -0,0 +1,286 @@
/* Base */
body, body *:not(html):not(style):not(br):not(tr):not(code) {
font-family: Avenir, Helvetica, sans-serif;
box-sizing: border-box;
}
body {
background-color: #f5f8fa;
color: #74787e;
height: 100%;
hyphens: auto;
line-height: 1.4;
margin: 0;
-moz-hyphens: auto;
-ms-word-break: break-all;
width: 100% !important;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
word-break: break-all;
word-break: break-word;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3c5f97;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #2F3133;
font-size: 19px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
color: #2F3133;
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
color: #2F3133;
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
color: #74787e;
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
background-color: #f5f8fa;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #3c5f97;
font-size: 19px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px 0 #ffffff;
}
/* Body */
.body {
background-color: #ffffff;
border-bottom: 1px solid #edeff2;
border-top: 1px solid #edeff2;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.inner-body {
background-color: #ffffff;
margin: 0 auto;
padding: 0;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #edeff2;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 12px;
}
/* Footer */
.footer {
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
.footer p {
color: #aeaeae;
font-size: 12px;
text-align: center;
}
/* Tables */
.table table {
margin: 30px auto;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.table th {
color: #74787e;
border-bottom: 1px solid #edeff2;
padding-bottom: 8px;
}
.table td {
color: #74787e;
font-size: 15px;
line-height: 18px;
padding: 10px 0;
}
.content-cell {
padding: 35px;
}
/* Buttons */
.action {
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.button {
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
color: #ffffff;
display: inline-block;
text-decoration: none;
-webkit-text-size-adjust: none;
}
.button-blue, .button-primary {
background-color: #3c5f97;
border-top: 10px solid #3c5f97;
border-right: 18px solid #3c5f97;
border-bottom: 10px solid #3c5f97;
border-left: 18px solid #3c5f97;
}
.button-green, .button-success {
background-color: #22c55e;
border-top: 10px solid #22c55e;
border-right: 18px solid #22c55e;
border-bottom: 10px solid #22c55e;
border-left: 18px solid #22c55e;
}
.button-red, .button-error {
background-color: #ef4444;
border-top: 10px solid #ef4444;
border-right: 18px solid #ef4444;
border-bottom: 10px solid #ef4444;
border-left: 18px solid #ef4444;
}
/* Panels */
.panel {
margin: 0 0 21px;
}
.panel-content {
background-color: #edeff2;
padding: 16px;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Promotions */
.promotion {
background-color: #FFFFFF;
border: 2px dashed #9BA2AB;
margin: 0;
margin-bottom: 25px;
margin-top: 25px;
padding: 24px;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.promotion h1 {
text-align: center;
}
.promotion p {
font-size: 15px;
text-align: center;
}