Compare commits

..

No commits in common. "7877f57a227625d61d1e73edc646d47075e6497c" and "0b406e11f0642207fed1aa676acaf823991290d4" have entirely different histories.

73 changed files with 3176 additions and 4308 deletions

View File

@ -2,7 +2,6 @@
namespace App\Http;
use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Laravel\Passport\Http\Middleware\CheckClientCredentials;
@ -38,7 +37,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
HandleInertiaRequests::class,
\App\Http\Middleware\InertiaShareMiddleware::class,
\App\Http\Middleware\RedirectIfNotInitializedMiddleware::class,
],

View File

@ -4,40 +4,21 @@ namespace App\Http\Middleware;
use App\Http\Resources\UserResource;
use App\Setting\GeneralSettings;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Inertia\Middleware;
use Closure;
use Session;
class HandleInertiaRequests extends Middleware
class InertiaShareMiddleware
{
/**
* The root template that's loaded on the first page visit.
* Handle an incoming request.
*
* @see https://inertiajs.com/server-side-setup#root-template
* @param \Illuminate\Http\Request $request
*
* @var string
* @return mixed
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
public function handle($request, Closure $next)
{
return parent::version($request);
}
/**
* Defines the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*/
public function share(Request $request): array
{
return [
...parent::share($request),
\Inertia::share([
'auth' => ['user' => auth()->check() ? new UserResource(auth()->user()) : null],
'search' => $request->query('search', ''),
'flash' => session()->get('flash'),
@ -55,6 +36,10 @@ class HandleInertiaRequests extends Middleware
'settings' => [
'modules' => app(GeneralSettings::class)->modules,
],
];
]);
$response = $next($request);
return $response;
}
}

View File

@ -4,7 +4,6 @@ namespace App\Initialize;
use App\Member\Actions\InsertFullMemberAction;
use App\Member\Data\FullMember;
use App\Member\Member;
use App\Nami\Api\FullMemberAction;
use App\Setting\NamiSettings;
use Illuminate\Support\Facades\Bus;
@ -25,9 +24,6 @@ class InitializeMembers
$settings = app(NamiSettings::class);
Redis::delete('members');
$memberIds = $api->search($settings->search_params)->map(fn ($member) => $member->id)->toArray();
Member::remote()->whereNotIn('nami_id', $memberIds)->get()->each->delete();
$jobs = $api->search($settings->search_params)->map(function (NamiMemberEntry $member) use ($api) {
return FullMemberAction::makeJob($api, $member->groupId, $member->id, 'members');
})->toArray();

View File

@ -37,6 +37,6 @@ class SettingSaveAction
{
$this->handle($request->all());
return redirect()->back();
return redirect()->back()->success('Einstellungen gespeichert');
}
}

View File

@ -75,6 +75,6 @@ class SettingSaveAction
{
$this->handle($request->all());
return redirect()->back();
return redirect()->back()->success('Einstellungen gespeichert');
}
}

View File

@ -29,7 +29,7 @@
"cviebrock/eloquent-sluggable": "^9.0",
"doctrine/dbal": "^3.1",
"guzzlehttp/guzzle": "^7.0.1",
"inertiajs/inertia-laravel": "^0.6.9",
"inertiajs/inertia-laravel": "^0.2.5",
"laravel/framework": "^9.0",
"laravel/horizon": "^5.0",
"laravel/passport": "^11.8",

981
composer.lock generated

File diff suppressed because it is too large Load Diff

4780
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,30 +12,35 @@
"fix": "eslint \"resources/js/**/*.{js,vue}\" --fix"
},
"devDependencies": {
"autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"eslint": "^8.43.0",
"eslint-plugin-vue": "^8.7.1",
"postcss": "^8.4.24",
"tailwindcss": "^3.3.2",
"vue-axios": "^3.5.2"
"autoprefixer": "latest",
"axios": "^1.3.4",
"eslint": "^8.9.0",
"eslint-plugin-vue": "^8.4.1",
"postcss": "^8.4.6",
"tailwindcss": "^3.3",
"vue": "2.7",
"vue-axios": "^3.5.2",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"@inertiajs/vue3": "^1.0.8",
"@inertiajs/inertia": "^0.11.0",
"@inertiajs/inertia-vue": "^0.8.0",
"@tailwindcss/typography": "^0.5.9",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue2": "^2.2.0",
"change-case": "^4.1.2",
"floating-vue": "^2.0.0-beta.24",
"laravel-vite-plugin": "^0.7.8",
"laravel-vite-plugin": "^0.7.7",
"leaflet": "^1.9.3",
"lodash": "^4.17.21",
"merge": "^2.1.1",
"pinia": "^2.1.4",
"postcss-import": "^14.1.0",
"prettier": "^2.8.8",
"pinia": "^2.0.35",
"portal-vue": "^2.1.7",
"postcss-import": "^14.0.1",
"svg-sprite": "^2.0.2",
"vite": "^4.3.9",
"vue": "^3.3.4",
"vue-toastification": "^2.0.0-rc.5",
"v-tooltip": "^2.1.3",
"vite": "^4.3.8",
"vue-toasted": "^1.1.28",
"vue2-leaflet": "^2.7.1",
"wnumb": "^1.2.0"
}
}

View File

@ -10,3 +10,5 @@
@import 'sidebar';
@import 'bool';
@import 'form';
@import 'tooltip';
@import 'leaflet';

View File

@ -13,12 +13,6 @@
@apply bg-primary-500 text-primary-100;
}
}
&.btn-secondary {
@apply bg-primary-800 text-primary-400;
&:not(.disabled):hover {
@apply bg-primary-600 text-primary-200;
}
}
&.btn-primary-light {
@apply bg-primary-600 text-primary-200;
&:not(.disabled):hover {

666
resources/css/leaflet.css vendored Normal file
View File

@ -0,0 +1,666 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane {
z-index: 400;
}
.leaflet-tile-pane {
z-index: 200;
}
.leaflet-overlay-pane {
z-index: 400;
}
.leaflet-shadow-pane {
z-index: 500;
}
.leaflet-marker-pane {
z-index: 600;
}
.leaflet-tooltip-pane {
z-index: 650;
}
.leaflet-popup-pane {
z-index: 700;
}
.leaflet-map-pane canvas {
z-index: 100;
}
.leaflet-map-pane svg {
z-index: 200;
}
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1);
transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078a8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255, 255, 255, 0.5);
}
/* general typography */
.leaflet-container {
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in,
.leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(../img/leaflet/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(../img/leaflet/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
/* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(../img/leaflet/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0, 0, 0, 0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: 'progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)';
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: '';
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

View File

@ -1,6 +1,109 @@
.v-popper--theme-tooltip .v-popper__inner {
@apply bg-primary-400 text-primary-800 px-3 py-1 text-sm;
}
.v-popper--theme-tooltip .v-popper__arrow-outer {
@apply border-primary-400;
.tooltip {
display: block !important;
max-width: 50vw;
z-index: 10000;
.tooltip-inner {
@apply bg-primary-900;
color: white;
border-radius: 8px;
padding: 5px 10px 4px;
}
.tooltip-arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
border-color: black;
@apply border-primary-900;
z-index: 1;
}
&[x-placement^='top'] {
margin-bottom: 5px;
.tooltip-arrow {
border-width: 5px 5px 0 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^='bottom'] {
margin-top: 5px;
.tooltip-arrow {
border-width: 0 5px 5px 5px;
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
}
&[x-placement^='right'] {
margin-left: 5px;
.tooltip-arrow {
border-width: 5px 5px 5px 0;
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&[x-placement^='left'] {
margin-right: 5px;
.tooltip-arrow {
border-width: 5px 0 5px 5px;
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
}
&.popover {
.popover-inner {
background: #f9f9f9;
color: black;
padding: 24px;
border-radius: 5px;
box-shadow: 0 5px 30px rgba(black, 0.1);
}
.popover-arrow {
border-color: #f9f9f9;
}
}
&[aria-hidden='true'] {
visibility: hidden;
opacity: 0;
transition: opacity 0.15s, visibility 0.15s;
}
&[aria-hidden='false'] {
visibility: visible;
opacity: 1;
transition: opacity 0.15s;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

83
resources/js/app.js vendored
View File

@ -1,53 +1,64 @@
import {createApp, h, defineAsyncComponent} from 'vue';
import {createInertiaApp, Link as ILink} from '@inertiajs/vue3';
import Vue from 'vue';
import {App as InertiaApp, plugin, Link as ILink} from '@inertiajs/inertia-vue';
import {Inertia} from '@inertiajs/inertia';
import PortalVue from 'portal-vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import {Plugin as FloatingVue, options as FloatingVueOptions} from './lib/floatingVue.js';
import Toasted from 'vue-toasted';
import VTooltip from 'v-tooltip';
import {createPinia, PiniaVuePlugin} from 'pinia';
import requireModules from './lib/requireModules.js';
import AppLayout from './layouts/AppLayout.vue';
import hasModule from './mixins/hasModule.js';
import hasFlash from './mixins/hasFlash.js';
import {Toast, options as toastOptions, interceptor as toastInterceptor} from './lib/toast.js';
import '../css/app.css';
// ---------------------------------- Assets -----------------------------------
import '../css/app.css';
import 'vue-toastification/dist/index.css';
import.meta.glob(['../img/**']);
// ---------------------------------- Plugins ----------------------------------
Vue.use(plugin);
Vue.use(PortalVue);
Vue.use(VTooltip);
Vue.use(Toasted);
Vue.use(VueAxios, axios);
Vue.use(PiniaVuePlugin);
Vue.component('SvgSprite', () => import('./components/SvgSprite.js'));
Vue.component('ILink', ILink);
// -------------------------------- Components ---------------------------------
requireModules(import.meta.glob('./components/form/*.vue'), Vue, 'f');
requireModules(import.meta.glob('./components/ui/*.vue'), Vue, 'ui');
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), Vue, 'page');
// ---------------------------------- mixins -----------------------------------
Vue.mixin(hasModule);
Vue.mixin(hasFlash);
// ----------------------------------- init ------------------------------------
const el = document.getElementById('app');
const pinia = createPinia();
var views = import.meta.glob('./views/**/*.vue');
axios.interceptors.response.use(...toastInterceptor);
Inertia.on('start', (event) => window.dispatchEvent(new Event('inertiaStart')));
createInertiaApp({
title: (title) => `${title} | Adrema`,
resolve: async (name) => {
var page = (await views[`./views/${name}.vue`]()).default;
let views = import.meta.glob('./views/**/*.vue');
new Vue({
pinia,
render: (h) =>
h(InertiaApp, {
props: {
initialPage: JSON.parse(el.dataset.page),
resolveComponent: async (name) => {
var page = (await views[`./views/${name}.vue`]()).default;
if (page.layout === undefined) {
page.layout = AppLayout;
}
return page;
},
setup({el, App, props, plugin}) {
var app = createApp({pinia, render: () => h(App, props)})
.use(plugin)
.use(FloatingVue, FloatingVueOptions)
.use(Toast, toastOptions)
.use(VueAxios, axios)
.use(PiniaVuePlugin)
.component('ILink', ILink)
.mixin(hasModule)
.mixin(hasFlash);
requireModules(import.meta.glob('./components/form/*.vue'), app, 'f');
requireModules(import.meta.glob('./components/ui/*.vue'), app, 'ui');
requireModules(import.meta.glob('./components/page/*.vue', {eager: true}), app, 'page');
app.mount(el);
},
});
if (page.layout === undefined) {
page.layout = AppLayout;
}
return page;
},
},
}),
}).$mount(el);

25
resources/js/components/SvgSprite.js vendored Normal file
View File

@ -0,0 +1,25 @@
export default {
props: {
src: {required: true, type: String},
},
render: function (createElement) {
var attr = this.$attrs.class ? this.$attrs.class : '';
return createElement(
'svg',
{
class: attr + ' fill-current',
},
[
createElement(
'use',
{
attrs: {
'xlink:href': `/sprite.svg#${this.$props.src}`,
},
},
''
),
]
);
},
};

View File

@ -8,24 +8,24 @@
<div
@click="visible = !visible"
class="flex items-center border-gray-600 text-gray-300 leading-none border-solid bg-gray-700 w-full appearance-none outline-none rounded-lg size-sm text-xs px-1 border pr-6"
v-text="`${modelValue.length} Einträge ausgewählt`"
v-text="`${value.length} Einträge ausgewählt`"
></div>
<div v-show="visible" class="absolute w-[max-content] z-30 max-h-[31rem] overflow-auto shadow-lg bg-gray-600 border border-gray-500 rounded-lg p-2 top-7">
<div v-for="(option, index) in parsedOptions" class="flex items-center space-x-2" :key="index">
<f-switch :id="`${id}-${index}`" size="sm" :modelValue="modelValue.includes(option.id)" :value="option.id" @update:modelValue="trigger(option, $event)"></f-switch>
<f-switch :id="`${id}-${index}`" size="sm" :items="value.includes(option.id)" :value="option.id" @input="trigger(option, $event)"></f-switch>
<div class="text-sm text-gray-200" v-text="option.name"></div>
</div>
</div>
<div class="info-wrap">
<div v-if="hint" v-tooltip="hint">
<ui-sprite src="info-button" class="info-button"></ui-sprite>
<svg-sprite src="info-button" class="info-button"></svg-sprite>
</div>
<div class="px-1 relative" v-if="size != 'xs'">
<ui-sprite class="chevron w-3 h-3 fill-current" src="chevron-down"></ui-sprite>
<svg-sprite class="chevron w-3 h-3 fill-current" src="chevron-down"></svg-sprite>
</div>
<div class="px-1 relative" v-if="size == 'xs'">
<ui-sprite class="chevron w-2 h-2 fill-current" src="chevron-down"></ui-sprite>
<svg-sprite class="chevron w-2 h-2 fill-current" src="chevron-down"></svg-sprite>
</div>
</div>
</div>
@ -36,7 +36,6 @@
import map from 'lodash/map';
export default {
emits: ['update:modelValue'],
data: function () {
return {
visible: false,
@ -63,7 +62,7 @@ export default {
default: false,
type: Boolean,
},
modelValue: {
value: {
default: undefined,
},
label: {
@ -94,12 +93,12 @@ export default {
},
methods: {
trigger(option, v) {
var value = [...this.modelValue];
var value = [...this.value];
this.$emit('update:modelValue', value.includes(option.id) ? value.filter((cv) => cv !== option.id) : [...value, option.id]);
this.$emit('input', value.includes(option.id) ? value.filter((cv) => cv !== option.id) : [...value, option.id]);
},
clear() {
this.$emit('update:modelValue', null);
this.$emit('input', null);
},
},
};

View File

@ -1,7 +1,14 @@
<template>
<button :form="form" type="submit" class="flex items-center transition-all justify-center w-8 h-8 bg-primary-700 hover:bg-primary-600 rounded" v-tooltip="`speichern`">
<ui-sprite class="w-4 h-4 text-white" src="save"></ui-sprite>
</button>
<portal to="toolbar-right">
<button
:form="form"
type="submit"
class="flex items-center transition-all justify-center w-8 h-8 bg-primary-700 hover:bg-primary-600 rounded"
v-tooltip="`speichern`"
>
<svg-sprite class="w-4 h-4 text-white" src="save"></svg-sprite>
</button>
</portal>
</template>
<script>

View File

@ -5,20 +5,20 @@
<span v-show="required" class="text-red-800">&nbsp;*</span>
</span>
<div class="real-field-wrap" :class="`size-${size}`">
<select :disabled="disabled" :name="name" :value="modelValue" @change="trigger">
<option v-if="placeholder" value="">{{ placeholder }}</option>
<select :disabled="disabled" :name="name" :value="value" @change="trigger">
<option v-if="placeholder" v-html="placeholder" :value="null"></option>
<option v-for="option in parsedOptions" :key="option.id" :value="option.id">{{ option.name }}</option>
<option v-for="option in parsedOptions" :key="option.id" v-html="option.name" :value="option.id"></option>
</select>
<div class="info-wrap">
<div v-if="hint" v-tooltip="hint">
<ui-sprite src="info-button" class="info-button"></ui-sprite>
<svg-sprite src="info-button" class="info-button"></svg-sprite>
</div>
<div class="px-1 relative" v-if="size != 'xs'">
<ui-sprite class="chevron w-3 h-3 fill-current" src="chevron-down"></ui-sprite>
<svg-sprite class="chevron w-3 h-3 fill-current" src="chevron-down"></svg-sprite>
</div>
<div class="px-1 relative" v-if="size == 'xs'">
<ui-sprite class="chevron w-2 h-2 fill-current" src="chevron-down"></ui-sprite>
<svg-sprite class="chevron w-2 h-2 fill-current" src="chevron-down"></svg-sprite>
</div>
</div>
</div>
@ -29,7 +29,6 @@
import map from 'lodash/map';
export default {
emits: ['update:modelValue'],
props: {
disabled: {
type: Boolean,
@ -51,7 +50,7 @@ export default {
default: false,
type: Boolean,
},
modelValue: {
value: {
default: undefined,
},
label: {
@ -91,20 +90,20 @@ export default {
},
methods: {
trigger(v) {
this.$emit('update:modelValue', /^[0-9]+$/.test(v.target.value) ? parseInt(v.target.value) : v.target.value ? v.target.value : null);
this.$emit('input', /^[0-9]+$/.test(v.target.value) ? parseInt(v.target.value) : v.target.value ? v.target.value : null);
},
clear() {
this.$emit('update:modelValue', null);
this.$emit('input', null);
},
},
mounted() {
if (this.def !== -1 && typeof this.modelValue === 'undefined') {
this.$emit('update:modelValue', this.def);
if (this.def !== -1 && typeof this.value === 'undefined') {
this.$emit('input', this.def);
return;
}
if (this.placeholder && typeof this.modelValue === 'undefined') {
this.$emit('update:modelValue', null);
if (this.placeholder && typeof this.value === 'undefined') {
this.$emit('input', null);
}
},
};

View File

@ -21,14 +21,14 @@
<span>
<input :id="id" type="checkbox" :name="name" :value="value" v-model="v" :disabled="disabled" class="absolute peer" @keypress="$emit('keypress', $event)" />
<span class="relative cursor-pointer peer-focus:bg-red-500 flex grow display" :class="{'bg-switch': v === true, 'bg-gray-700': v === false}">
<span><ui-sprite class="relative text-gray-400 flex-none" :class="{'w-2 h-2': size === 'sm' || size == 'xs', 'w-4 h-4': size === 'base'}" src="check"></ui-sprite></span>
<span><ui-sprite class="relative text-gray-400 flex-none" :class="{'w-2 h-2': size === 'sm' || size == 'xs', 'w-4 h-4': size === 'base'}" src="close"></ui-sprite></span>
<span><svg-sprite class="relative text-gray-400 flex-none" :class="{'w-2 h-2': size === 'sm' || size == 'xs', 'w-4 h-4': size === 'base'}" src="check"></svg-sprite></span>
<span><svg-sprite class="relative text-gray-400 flex-none" :class="{'w-2 h-2': size === 'sm' || size == 'xs', 'w-4 h-4': size === 'base'}" src="close"></svg-sprite></span>
<var class="absolute overlay bg-gray-400 rounded top-0"></var>
</span>
</span>
<div v-if="hint" class="ml-2 info-wrap">
<div v-tooltip="hint">
<ui-sprite src="info-button" class="info-button w-4 h-4 text-primary-700"></ui-sprite>
<svg-sprite src="info-button" class="info-button w-4 h-4 text-primary-700"></svg-sprite>
</div>
</div>
</div>
@ -37,7 +37,6 @@
<script>
export default {
emits: ['update:modelValue'],
data: function () {
return {
sizes: {
@ -56,6 +55,10 @@ export default {
},
};
},
model: {
prop: 'items',
event: 'input',
},
props: {
hint: {
default: null,
@ -84,7 +87,7 @@ export default {
label: {
default: false,
},
modelValue: {
items: {
default: undefined,
},
},
@ -95,28 +98,28 @@ export default {
return;
}
if (typeof this.modelValue === 'boolean') {
this.$emit('update:modelValue', v);
if (typeof this.items === 'boolean') {
this.$emit('input', v);
return;
}
var a = this.modelValue.filter((i) => i !== this.value);
var a = this.items.filter((i) => i !== this.value);
if (v) {
a.push(this.value);
}
this.$emit('update:modelValue', a);
this.$emit('input', a);
},
get() {
if (typeof this.modelValue === 'boolean') {
return this.modelValue;
if (typeof this.items === 'boolean') {
return this.items;
}
if (typeof this.modelValue === 'undefined') {
return this.$emit('update:modelValue', false);
if (typeof this.items === 'undefined') {
return this.$emit('input', false);
}
return this.modelValue.indexOf(this.value) !== -1;
return this.items.indexOf(this.value) !== -1;
},
},
fieldSize() {

View File

@ -19,7 +19,7 @@
/>
<div v-if="hint" class="info-wrap">
<div v-tooltip="hint">
<ui-sprite src="info-button" class="info-button"></ui-sprite>
<svg-sprite src="info-button" class="info-button"></svg-sprite>
</div>
</div>
</div>
@ -293,7 +293,9 @@ export default {
hint: {
default: null,
},
modelValue: {},
value: {
default: undefined,
},
mask: {
default: undefined,
},
@ -333,10 +335,10 @@ export default {
computed: {
transformedValue: {
get() {
return transformers[this.mode][this.focus ? 'edit' : 'display'].to(this.modelValue);
return transformers[this.mode][this.focus ? 'edit' : 'display'].to(this.value);
},
set(v) {
this.$emit('update:modelValue', transformers[this.mode][this.focus ? 'edit' : 'display'].from(v));
this.$emit('input', transformers[this.mode][this.focus ? 'edit' : 'display'].from(v));
},
},
insetClass() {
@ -351,7 +353,7 @@ export default {
},
},
created() {
if (typeof this.modelValue === 'undefined') {
if (typeof this.value === 'undefined') {
this.$emit('input', this.default === undefined ? '' : this.default);
}
},

View File

@ -30,7 +30,7 @@
}"
></textarea>
<div v-if="hint" v-tooltip="hint" class="absolute right-0 top-0 mr-2 mt-2">
<ui-sprite src="info-button" class="w-5 h-5 text-indigo-200"></ui-sprite>
<svg-sprite src="info-button" class="w-5 h-5 text-indigo-200"></svg-sprite>
</div>
</label>
</template>

View File

@ -6,8 +6,8 @@
<slot name="toolbar"></slot>
</div>
<div class="flex items-center space-x-2 ml-2">
<a href="#" v-if="$attrs.onClose" @click.prevent="$emit('close')" class="btn label btn-primary-light icon">
<ui-sprite class="w-3 h-3" src="close"></ui-sprite>
<a href="#" v-if="$listeners.close" @click.prevent="$emit('close')" class="btn label btn-primary-light icon">
<svg-sprite class="w-3 h-3" src="close"></svg-sprite>
</a>
<slot name="right"></slot>
</div>

View File

@ -1,17 +1,16 @@
<template>
<div class="grow bg-gray-900 flex flex-col transition-all" :class="{'ml-56': menuStore.visible, 'ml-0': !menuStore.visible}">
<Head :title="$page.props.title"></Head>
<page-header :title="$page.props.title">
<template #before-title>
<a href="#" @click.prevent="menuStore.toggle()" class="mr-2 lg:hidden">
<ui-sprite src="menu" class="text-gray-100 w-5 h-5"></ui-sprite>
<svg-sprite src="menu" class="text-gray-100 w-5 h-5"></svg-sprite>
</a>
</template>
<template #toolbar>
<slot name="toolbar"></slot>
</template>
<template #right>
<slot name="right"></slot>
<portal-target name="toolbar-right"> </portal-target>
</template>
</page-header>
@ -23,7 +22,6 @@
<script>
import {menuStore} from '../../stores/menuStore.js';
import {Head} from '@inertiajs/vue3';
export default {
inheritAttrs: false,
@ -39,8 +37,5 @@ export default {
menuStore: menuStore(),
};
},
components: {
Head,
},
};
</script>

View File

@ -1,10 +1,6 @@
<template>
<button class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-if="$attrs.onClick">
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
<span class="hidden xl:inline"><slot></slot></span>
</button>
<i-link :href="href" class="btn label" v-bind="$attrs" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? slotContent : ''" v-else>
<ui-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></ui-sprite>
<i-link :href="href" v-on="$listeners" class="btn label" :class="colors[color]" v-tooltip="menuStore.tooltipsVisible ? $slots.default[0].text : ''">
<svg-sprite v-show="icon" class="w-3 h-3 xl:mr-2" :src="icon"></svg-sprite>
<span class="hidden xl:inline"><slot></slot></span>
</i-link>
</template>
@ -24,18 +20,9 @@ export default {
};
},
props: {
href: {
required: false,
default: () => '#',
},
href: {},
icon: {},
color: {},
},
computed: {
slotContent() {
return this.$slots.default()[0].children;
}
},
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="flex gap-1 justify-center items-center">
<ui-sprite class="flex-none" v-if="member.is_leader" :class="[ageColors.leiter, iconClass]" src="lilie"></ui-sprite>
<ui-sprite class="flex-none" v-if="member.age_group_icon" :class="[ageColors[member.age_group_icon], iconClass]" src="lilie"></ui-sprite>
<svg-sprite class="flex-none" v-if="member.is_leader" :class="[ageColors.leiter, iconClass]" src="lilie"></svg-sprite>
<svg-sprite class="flex-none" v-if="member.age_group_icon" :class="[ageColors[member.age_group_icon], iconClass]" src="lilie"></svg-sprite>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<div class="bool" v-tooltip="comment" :class="modelValue ? 'enabled' : 'disabled'">
<ui-sprite v-if="!$slots.default" :src="modelValue ? 'check' : 'close'"></ui-sprite>
<div class="bool" v-tooltip="comment" :class="value ? 'enabled' : 'disabled'">
<svg-sprite v-if="!$slots.default" :src="value ? 'check' : 'close'"></svg-sprite>
<slot></slot>
</div>
</template>
@ -8,20 +8,20 @@
<script>
export default {
props: {
modelValue: {},
value: {},
trueComment: {},
falseComment: {},
},
computed: {
t() {
return this.modelValue ? 'Ja' : 'Nein';
return this.value ? 'Ja' : 'Nein';
},
comment() {
if (this.modelValue && this.trueComment) {
if (this.value && this.trueComment) {
return this.trueComment;
}
if (!this.modelValue && this.falseComment) {
if (!this.value && this.falseComment) {
return this.falseComment;
}

View File

@ -1,7 +1,7 @@
<template>
<div v-tooltip="longLabel" class="flex space-x-2 items-center">
<div class="border-2 rounded-full w-5 h-5 flex items-center justify-center" :class="value ? 'border-green-700' : 'border-red-700'">
<ui-sprite :src="value ? 'check' : 'close'" :class="value ? 'text-green-800' : 'text-red-800'" class="w-3 h-3 flex-none"></ui-sprite>
<svg-sprite :src="value ? 'check' : 'close'" :class="value ? 'text-green-800' : 'text-red-800'" class="w-3 h-3 flex-none"></svg-sprite>
</div>
<div class="text-gray-400 text-xs" v-text="label"></div>
</div>

View File

@ -1,5 +1,5 @@
<template>
<button v-bind="$attrs" class="btn btn-primary relative group">
<button v-on="$listeners" class="btn btn-primary relative group">
<div :class="{hidden: !isLoading, flex: isLoading}" class="absolute items-center top-0 h-full left-0 ml-2">
<ui-spinner class="border-primary-400 w-6 h-6 group-hover:border-primary-200"></ui-spinner>
</div>

View File

@ -1,6 +1,6 @@
<template>
<button v-bind="$attrs" type="button" class="btn label btn-primary">
<ui-sprite class="w-3 h-3 mr-2" :src="icon"></ui-sprite>
<button v-on="$listeners" type="button" class="btn label btn-primary">
<svg-sprite class="w-3 h-3 mr-2" :src="icon"></svg-sprite>
<span><slot></slot></span>
</button>
</template>

View File

@ -4,7 +4,7 @@
<svg class="absolute top-0 left-0 w-full h-full flex-none rotate-[105deg]" viewBox="-16 -16 32 32" :class="types[type].iconContainer">
<circle cx="0" cy="0" r="15" stroke-dasharray="62.76896820 31.38448410" stroke-width="2"></circle>
</svg>
<ui-sprite :src="types[type].icon" class="relative w-4 h-4" :class="types[type].iconClass"></ui-sprite>
<svg-sprite :src="types[type].icon" class="relative w-4 h-4" :class="types[type].iconClass"></svg-sprite>
</div>
<div>
<h3 class="font-semibold" :class="types[type].heading" v-text="types[type].intro"></h3>

View File

@ -4,14 +4,14 @@
<div class="-mx-1 items-baseline" :class="{hidden: value.last_page == 1, flex: value.last_page > 1}">
<div class="pl-1 pr-3 text-gray-500 text-sm">Seite:</div>
<div class="px-1" v-for="(link, index) in links" :key="index">
<button
<i-link
href="#"
@click.prevent="goto(link)"
class="rounded text-sm w-8 h-8 text-primary-100 flex items-center justify-center leading-none shadow"
:key="index"
v-text="link.page"
:class="{'bg-primary-700': link.current, 'bg-primary-900': !link.current}"
></button>
></i-link>
</div>
</div>
</div>
@ -34,7 +34,7 @@ export default {
},
methods: {
goto(page) {
if (this.$attrs.onReload) {
if (this.$listeners.reload) {
this.$emit('reload', page.page);
return;
}

View File

@ -2,7 +2,7 @@
<div class="fixed z-40 top-0 left-0 w-full h-full flex items-center justify-center p-6">
<div class="relative rounded-lg p-8 bg-zinc-800 shadow-2xl shadow-black border border-zinc-700 border-solid w-full max-w-xl">
<a href="#" class="absolute top-0 right-0 mt-6 mr-6" @click.prevent="$emit('close')">
<ui-sprite src="close" class="text-zinc-400 w-6 h-6"></ui-sprite>
<svg-sprite src="close" class="text-zinc-400 w-6 h-6"></svg-sprite>
</a>
<h3 class="font-semibold text-primary-200 text-xl" v-html="heading"></h3>
<div class="text-primary-100">

View File

@ -1,7 +0,0 @@
<template>
<svg v-bind="$attrs" class="fill-current"><use :xlink:href="`/sprite.svg#${$attrs.src}`" /></svg>
</template>
<script>
export default {};
</script>

View File

@ -7,7 +7,7 @@
href="#"
@click.prevent="openMenu(index)"
class="rounded py-1 px-3 text-gray-400"
:class="index === modelValue ? `bg-gray-600` : ''"
:class="index === value ? `bg-gray-600` : ''"
v-text="item.title"
></a>
</div>
@ -18,12 +18,12 @@
<script>
export default {
props: {
modelValue: {},
value: {},
entries: {},
},
methods: {
openMenu(index) {
this.$emit('update:modelValue', index);
this.$emit('input', index);
},
},
};

View File

@ -1,38 +1,39 @@
<template>
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<div id="app" class="flex font-sans grow">
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<!-- ******************************** Sidebar ******************************** -->
<div
class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
:class="{
'-left-[14rem]': !menuStore.isShifted,
'left-0': menuStore.isShifted,
}"
>
<div class="grid gap-2">
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
<v-link href="/member" menu="member" icon="user">Mitglieder</v-link>
<v-link href="/subscription" v-show="hasModule('bill')" menu="subscription" icon="money">Beiträge</v-link>
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
<v-link href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</v-link>
<!-- ******************************** Sidebar ******************************** -->
<div
class="fixed z-40 bg-gray-800 p-6 w-56 top-0 h-screen border-r border-gray-600 border-solid flex flex-col justify-between transition-all"
:class="{
'-left-[14rem]': !menuStore.isShifted,
'left-0': menuStore.isShifted,
}"
>
<div class="grid gap-2">
<v-link href="/" menu="dashboard" icon="loss">Dashboard</v-link>
<v-link href="/member" menu="member" icon="user">Mitglieder</v-link>
<v-link href="/subscription" v-show="hasModule('bill')" menu="subscription" icon="money">Beiträge</v-link>
<v-link href="/contribution" menu="contribution" icon="contribution">Zuschüsse</v-link>
<v-link href="/activity" menu="activity" icon="activity">Tätigkeiten</v-link>
<v-link href="/maildispatcher" menu="maildispatcher" icon="at">Mail-Verteiler</v-link>
</div>
<div class="grid gap-2">
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
<v-link @click.prevent="$inertia.post('/logout')" icon="logout" href="/logout">Abmelden</v-link>
</div>
<a href="#" @click.prevent="menuStore.hide()" v-if="menuStore.hideable" class="absolute right-0 top-0 mr-2 mt-2">
<svg-sprite src="close" class="w-5 h-5 text-gray-300"></svg-sprite>
</a>
</div>
<div class="grid gap-2">
<v-link href="/setting" menu="setting" icon="setting">Einstellungen</v-link>
<v-link @click.prevent="$inertia.post('/logout')" icon="logout" href="/logout">Abmelden</v-link>
</div>
<a href="#" @click.prevent="menuStore.hide()" v-if="menuStore.hideable" class="absolute right-0 top-0 mr-2 mt-2">
<ui-sprite src="close" class="w-5 h-5 text-gray-300"></ui-sprite>
</a>
<slot></slot>
</div>
<slot></slot>
</template>
<script>
import VLink from './_VLink.vue';
import {menuStore} from '../stores/menuStore.js';
import VNotification from '../components/VNotification.vue';
export default {
data: function () {
@ -41,7 +42,7 @@ export default {
};
},
components: {
VNotification,
VNotification: () => import('../components/VNotification.vue'),
VLink,
},

View File

@ -1,6 +1,6 @@
<template>
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<div class="flex justify-center items-center grow min-h-full">
<div id="app" class="bg-gray-900 font-sans flex flex-col grow items-center justify-center">
<v-notification class="fixed z-40 right-0 bottom-0 mb-3 mr-3"></v-notification>
<div class="w-64 sm:w-72 md:w-96 bg-gray-800 rounded-xl overflow-hidden shadow-lg">
<slot></slot>
</div>
@ -8,11 +8,9 @@
</template>
<script>
import VNotification from '../components/VNotification.vue';
export default {
components: {
VNotification,
VNotification: () => import('../components/VNotification.vue'),
},
};
</script>

View File

@ -8,11 +8,9 @@
</template>
<script>
import {defineAsyncComponent} from 'vue';
export default {
components: {
VNotification: defineAsyncComponent(() => import('../components/VNotification.vue')),
VNotification: () => import('../components/VNotification.vue'),
},
};
</script>

View File

@ -1,6 +1,12 @@
<template>
<i-link class="flex text-white py-2 px-3 rounded-lg hover:bg-gray-600" :method="method" v-bind="$attrs" :href="href" :class="{'bg-gray-700': $page.props.menu == menu}">
<ui-sprite class="text-white w-6 h-6 mr-4" :src="icon"></ui-sprite>
<i-link
class="flex text-white py-2 px-3 rounded-lg hover:bg-gray-600"
:method="method"
v-on="$listeners"
:href="href"
:class="{'bg-gray-700': $page.props.menu == menu}"
>
<svg-sprite class="text-white w-6 h-6 mr-4" :src="icon"></svg-sprite>
<span class="font-semibold">
<slot></slot>
</span>

View File

@ -1,20 +0,0 @@
import Plugin from 'floating-vue';
import 'floating-vue/dist/style.css';
import '../../css/tooltip.css';
var options = {
distance: 7,
themes: {
tooltip: {
delay: {
show: 50,
hide: 0,
},
handleResize: true,
html: true,
},
},
};
export {Plugin, options};

View File

@ -1,10 +1,9 @@
import {paramCase} from 'change-case';
import {defineAsyncComponent} from 'vue';
export default function (context, app, prefix) {
export default function (context, Vue, prefix) {
for (const file in context) {
let componentName = paramCase(`${prefix}${file.replace(/^.*\/(.*?)\.vue$/g, '$1')}`);
app.component(componentName, typeof context[file] === 'function' ? defineAsyncComponent(context[file]) : context[file].default);
Vue.component(componentName, typeof context[file] === 'function' ? context[file] : context[file].default);
}
}

View File

@ -1,23 +0,0 @@
import Toast, {useToast} from 'vue-toastification';
const toast = useToast();
var interceptor = [
(config) => {
return config;
},
(err) => {
if (err.response.status === 422) {
var errors = err.response.data.errors;
for (const error in errors) {
errors[error].forEach((errorMessage) => toast.error(errorMessage));
}
}
return Promise.reject(err);
},
];
const options = {
position: 'bottom-right',
};
export {Toast, interceptor, options};

View File

@ -1,13 +1,18 @@
import { useToast } from 'vue-toastification'
const toast = useToast()
export default {
methods: {
['$success'](message) {
toast.success(message);
this.$toasted.show(message, {
position: 'bottom-right',
duration: 2000,
type: 'success',
});
},
['$error'](message) {
toast.error(message);
this.$toasted.show(message, {
position: 'bottom-right',
duration: 2000,
type: 'error',
});
},
errorsFromException(e) {
if (e.response?.status !== 422 || !e.response?.data?.errors) {
@ -18,7 +23,7 @@ export default {
Object.keys(errors).forEach((field) => {
errors[field].forEach((message) => {
toast.error(message);
this.$error(message);
});
});
},

View File

@ -33,7 +33,7 @@ export const menuStore = defineStore('menu', {
window.addEventListener('resize', this.menuListener);
this.menuListener();
window.addEventListener('inertia:start', () => {
window.addEventListener('inertiaStart', () => {
if (!window.matchMedia('(min-width: 1024px)').matches) {
_self.visible = false;
_self.overflowVisible = false;

View File

@ -27,7 +27,7 @@
id="gruppierung1Id"
size="sm"
:options="searchLayerOptions[0]"
@update:modelValue="loadSearchLayer(1, $event, search)"
@input="loadSearchLayer(1, $event, search)"
hint="Gruppierungs-Nummer einer Diözese, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deiner Diözese. Entspricht dem Feld '1. Ebene' in der NaMi Suche."
></f-select>
<f-select
@ -37,7 +37,7 @@
id="gruppierung2Id"
hint="Gruppierungs-Nummer eines Bezirks, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Bezirks. Entspricht dem Feld '2. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Bezirk zu begrenzen."
:disabled="!values.params.gruppierung1Id"
@update:modelValue="loadSearchLayer(2, $event, search)"
@input="loadSearchLayer(2, $event, search)"
size="sm"
:options="searchLayerOptions[1]"
></f-select>
@ -47,7 +47,7 @@
name="gruppierung3Id"
id="gruppierung3Id"
size="sm"
@update:modelValue="search"
@input="search"
hint="Gruppierungs-Nummer deines Stammes, auf die die Mitglieder passen sollen. I.d.R. ist das die Gruppierungsnummer deines Stammes. Entspricht dem Feld '3. Ebene' in der NaMi Suche. Fülle dieses Feld aus, um Mitglieder auf einen bestimmten Stamm zu beschränken."
:disabled="!values.params.gruppierung1Id || !values.params.gruppierung2Id"
:options="searchLayerOptions[2]"
@ -58,7 +58,7 @@
name="mglStatusId"
id="mglStatusId"
size="sm"
@update:modelValue="search"
@input="search"
:options="states"
hint="Wähle hier etwas aus, um nur aktive oder nur inaktive Mitglieder zu synchronisieren. Wir empfehlen dir, dies so zu belassen und Mitglieder ohne 'Datenweiterverwendung' gänzlich zu löschen, um Karteileichen zu entfernen."
></f-select>
@ -67,7 +67,7 @@
label="In Gruppierung suchen"
name="inGrp"
id="inGrp"
@update:modelValue="search"
@input="search"
hint="Mitglieder finden, die direktes Mitglied in der kleinsten befüllten Gruppierung sind."
size="sm"
></f-switch>
@ -76,12 +76,12 @@
label="Unterhalb Gruppierung suchen"
name="unterhalbGrp"
id="unterhalbGrp"
@update:modelValue="search"
@input="search"
hint="Mitglieder finden, die direktes Mitglied in einer Untergruppe der kleinsten befüllten Gruppierung sind."
size="sm"
></f-switch>
<div class="col-span-full flex justify-center">
<ui-button :is-loading="loading" class="!px-10" type="submit">Weiter</ui-button>
<ui-button :is-loading="loading" class="px-10" type="submit">Weiter</ui-button>
</div>
</form>
@ -117,10 +117,9 @@
<p>Dieser Gruppierung werden Mitglieder automatisch zugeordnet,<br />falls nichts anderes angegeben wurde.</p>
<p>I.d.R. ist das z.B. die Nummer deines Stammes, wenn du als StaVo mit Adrema Daten verwaltest.</p>
</div>
<form @submit.prevent="submit" class="grid grid-cols-2 gap-3 mt-5">
<f-text v-model="values.group_id" label="Gruppierungs-Nummer" name="groupId" id="groupId" type="tel" class="col-span-full" required></f-text>
<ui-button class="btn-secondary" @click.prevent="step--">Zurück</ui-button>
<ui-button type="submit">Weiter</ui-button>
<form @submit.prevent="submit" class="grid gap-3 mt-5">
<f-text v-model="values.group_id" label="Gruppierungs-Nummer" name="groupId" id="groupId" type="tel" required></f-text>
<button type="submit" class="btn w-full btn-primary mt-6 inline-block">Weiter</button>
</form>
</div>
<div v-if="step === 3">
@ -172,8 +171,12 @@ export default {
},
methods: {
async submit() {
await this.axios.post('/initialize', this.values);
this.step = 3;
try {
await this.axios.post('/initialize', this.values);
this.step = 3;
} catch (e) {
this.errorsFromException(e);
}
},
async storeSearch() {
this.values.group_id = this.values.params.gruppierung3Id ? this.values.params.gruppierung3Id : this.values.params.gruppierung2Id;
@ -186,10 +189,11 @@ export default {
this.loading = true;
try {
await this.axios.post('/nami/login-check', this.values);
this.$success('Login erfolgreich');
await this.loadSearchResult(1);
await this.loadSearchLayer(0, null, () => '');
this.step = 1;
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}
@ -218,6 +222,8 @@ export default {
});
after();
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}
@ -227,6 +233,8 @@ export default {
try {
var result = await this.axios.post('/nami/search', {...this.values, page: page});
this.preview = result.data;
} catch (e) {
this.errorsFromException(e);
} finally {
this.loading = false;
}

View File

@ -5,8 +5,8 @@
<img src="../../img/dpsg.gif" class="w-24" />
</div>
<div class="p-6 md:p-10 grid gap-5">
<f-text id="email" name="email" label="E-Mail-Adresse" v-model="values.email"></f-text>
<f-text id="password" name="password" type="password" label="Passwort" v-model="values.password"></f-text>
<f-text id="email" label="E-Mail-Adresse" v-model="values.email"></f-text>
<f-text id="password" type="password" label="Passwort" v-model="values.password"></f-text>
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>

View File

@ -24,12 +24,16 @@ export default {
methods: {
async store() {
if (this.model.id) {
var response = await this.axios.patch(this.model.links.update, this.model);
this.$emit('updated', response.data);
} else {
var response = await this.axios.post('/subactivity', this.model);
this.$emit('stored', response.data);
try {
if (this.model.id) {
var response = await this.axios.patch(this.model.links.update, this.model);
this.$emit('updated', response.data);
} else {
var response = await this.axios.post('/subactivity', this.model);
this.$emit('stored', response.data);
}
} catch (e) {
this.errorsFromException(e);
}
},
},

View File

@ -3,9 +3,6 @@
<template #toolbar>
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">zurück</page-toolbar-button>
</template>
<template #right>
<f-save-button form="actionform"></f-save-button>
</template>
<form id="actionform" class="grow p-3" @submit.prevent="submit">
<ui-popup heading="Neue Untertätigkeit" v-if="mode === 'edit' && currentSubactivity !== null" @close="currentSubactivity = null">
<subactivity-form class="mt-4" v-if="currentSubactivity" :value="currentSubactivity" @stored="reloadSubactivities" @updated="mergeSubactivity"></subactivity-form>
@ -21,7 +18,7 @@
<div class="grid gap-2 sm:grid-cols-2 md:grid-cols-4">
<div v-for="option in subactivities" class="flex items-center space-x-2">
<a href="#" @click.prevent="currentSubactivity = option" class="transition hover:bg-yellow-600 group w-5 h-5 rounded-full flex items-center justify-center flex-none">
<ui-sprite src="pencil" class="text-yellow-800 w-3 h-3 group-hover:text-yellow-200 transition"></ui-sprite>
<svg-sprite src="pencil" class="text-yellow-800 w-3 h-3 group-hover:text-yellow-200 transition"></svg-sprite>
</a>
<f-switch
inline
@ -35,20 +32,13 @@
></f-switch>
</div>
</div>
<f-save-button form="actionform"></f-save-button>
</form>
</page-layout>
</template>
<script>
import {defineAsyncComponent} from 'vue';
import {useToast} from 'vue-toastification';
export default {
setup() {
const toast = useToast();
return {toast};
},
data: function () {
return {
currentSubactivity: null,
@ -64,7 +54,7 @@ export default {
},
components: {
'subactivity-form': defineAsyncComponent(() => import('./SubactivityForm.vue')),
'subactivity-form': () => import('./SubactivityForm.vue'),
},
methods: {
@ -73,23 +63,27 @@ export default {
},
reloadSubactivities(model) {
var _self = this;
this.$inertia.reload({
onSuccess: (page) => {
this.subactivities = page.props.meta.subactivities;
this.inner.subactivities.push(model.id);
this.toast.success('Untertätigkeit gespeichert.');
this.currentSubactivity = null;
onSuccess(page) {
_self.subactivities = page.props.meta.subactivities;
_self.inner.subactivities.push(model.id);
_self.$success('Untertätigkeit gespeichert.');
_self.currentSubactivity = null;
},
});
},
mergeSubactivity(model) {
var _self = this;
this.$inertia.reload({
onSuccess: (page) => {
this.subactivities = page.props.meta.subactivities;
this.inner.subactivities = this.inner.subactivities.map((s) => (s.id === model.id ? model : s));
this.toast.success('Untertätigkeit aktualisiert.');
this.currentSubactivity = null;
onSuccess(page) {
_self.subactivities = page.props.meta.subactivities;
_self.inner.subactivities = _self.inner.subactivities.map((s) => (s.id === model.id ? model : s));
_self.$success('Untertätigkeit aktualisiert.');
_self.currentSubactivity = null;
},
});
},

View File

@ -22,8 +22,8 @@
<td v-text="activity.name"></td>
<td>
<div class="flex space-x-1">
<i-link :href="activity.links.edit" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"><ui-sprite src="pencil"></ui-sprite></i-link>
<i-link href="#" @click.prevent="deleting = activity" class="inline-flex btn btn-danger btn-sm" v-tooltip="`Entfernen`"><ui-sprite src="trash"></ui-sprite></i-link>
<i-link :href="activity.links.edit" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"><svg-sprite src="pencil"></svg-sprite></i-link>
<i-link href="#" @click.prevent="deleting = activity" class="inline-flex btn btn-danger btn-sm" v-tooltip="`Entfernen`"><svg-sprite src="trash"></svg-sprite></i-link>
</div>
</td>
</tr>

View File

@ -1,7 +1,11 @@
<template>
<div>
<div v-for="(group, index) in inner.groups" :key="index" class="flex mt-2 items-center leading-none text-gray-100">
<ui-sprite class="w-4 h-4 mr-2" src="lilie" :class="`text-${group.slug}`"></ui-sprite>
<div
v-for="(group, index) in inner.groups"
:key="index"
class="flex mt-2 items-center leading-none text-gray-100"
>
<svg-sprite class="w-4 h-4 mr-2" src="lilie" :class="`text-${group.slug}`"></svg-sprite>
<span v-text="group.name" class="grow"></span>
<span v-text="group.count"></span>
</div>

View File

@ -2,15 +2,13 @@
<page-layout>
<div class="gap-6 md:grid-cols-2 xl:grid-cols-4 grid p-6">
<v-block v-for="(block, index) in blocks" :key="index" :title="block.title">
<component :data="block.data" :is="block.component"></component>
<v-component :data="block.data" :is="block.component"></v-component>
</v-block>
</div>
</page-layout>
</template>
<script>
import {defineAsyncComponent} from 'vue';
export default {
props: {
data: {},
@ -18,12 +16,12 @@ export default {
},
components: {
'VBlock': defineAsyncComponent(() => import('./VBlock.vue')),
'age-group-count': defineAsyncComponent(() => import('./AgeGroupCount.vue')),
'efz-pending': defineAsyncComponent(() => import('./EfzPending.vue')),
'ps-pending': defineAsyncComponent(() => import('./PsPending.vue')),
'testers': defineAsyncComponent(() => import('./Testers.vue')),
'member-payment': defineAsyncComponent(() => import('./MemberPayment.vue')),
'VBlock': () => import('./VBlock.vue'),
'age-group-count': () => import('./AgeGroupCount.vue'),
'efz-pending': () => import('./EfzPending.vue'),
'ps-pending': () => import('./PsPending.vue'),
'testers': () => import('./Testers.vue'),
'member-payment': () => import('./MemberPayment.vue'),
},
};
</script>

View File

@ -3,10 +3,8 @@
<template #toolbar>
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">Zurück</page-toolbar-button>
</template>
<template #right>
<f-save-button form="form"></f-save-button>
</template>
<form id="form" class="p-3 grid gap-3" @submit.prevent="submit">
<f-save-button form="form"></f-save-button>
<ui-box heading="Metadatem">
<div class="grid gap-4 sm:grid-cols-2">
<f-text id="name" name="name" v-model="model.name" label="Name" size="sm" required></f-text>
@ -20,7 +18,7 @@
name="activity_ids"
:options="members.meta.filterActivities"
v-model="model.filter.activity_ids"
@update:modelValue="reload(1)"
@input="reload(1)"
label="Tätigkeit"
size="sm"
></f-multipleselect>
@ -29,7 +27,7 @@
name="subactivity_ids"
:options="members.meta.filterSubactivities"
v-model="model.filter.subactivity_ids"
@update:modelValue="reload(1)"
@input="reload(1)"
label="Unterttätigkeit"
size="sm"
></f-multipleselect>
@ -38,7 +36,7 @@
name="additional"
:options="members.meta.members"
v-model="model.filter.additional"
@update:modelValue="reload(1)"
@input="reload(1)"
label="Zusätzliche Mitglieder"
size="sm"
></f-multipleselect>
@ -47,7 +45,7 @@
name="groupIds"
:options="members.meta.groups"
v-model="model.filter.group_ids"
@update:modelValue="reload(1)"
@input="reload(1)"
label="Gruppierungen"
size="sm"
></f-multipleselect>
@ -107,8 +105,12 @@ export default {
},
async submit() {
this.model.id ? await this.axios.patch(this.model.links.update, this.model) : await this.axios.post('/maildispatcher', this.model);
this.$inertia.visit(this.meta.links.index);
try {
this.model.id ? await this.axios.patch(this.model.links.update, this.model) : await this.axios.post('/maildispatcher', this.model);
this.$inertia.visit(this.meta.links.index);
} catch (e) {
this.errorsFromException(e);
}
},
},

View File

@ -31,8 +31,8 @@
<div v-text="dispatcher.gateway.name"></div>
</td>
<td>
<i-link :href="dispatcher.links.edit" class="mr-1 inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<button @click.prevent="remove(dispatcher)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></button>
<i-link :href="dispatcher.links.edit" class="mr-1 inline-flex btn btn-warning btn-sm"><svg-sprite src="pencil"></svg-sprite></i-link>
<i-link @click.prevent="remove(dispatcher)" class="inline-flex btn btn-danger btn-sm"><svg-sprite src="trash"></svg-sprite></i-link>
</td>
</tr>
</table>

View File

@ -9,8 +9,8 @@
<f-text v-model="model.name" name="name" id="name" label="Bezeichnung" required></f-text>
<f-text v-model="model.domain" name="domain" id="domain" label="Domain" required></f-text>
<f-select
:modelValue="model.type.cls"
@update:modelValue="
:value="model.type.cls"
@input="
model.type = {
cls: $event,
params: {...getType($event).defaults},
@ -65,7 +65,7 @@
></ui-boolean-display>
</td>
<td>
<a href="#" v-tooltip="`Bearbeiten`" @click.prevent="model = {...gateway}" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></a>
<a href="#" v-tooltip="`Bearbeiten`" @click.prevent="model = {...gateway}" class="inline-flex btn btn-warning btn-sm"><svg-sprite src="pencil"></svg-sprite></a>
</td>
</tr>
</table>
@ -100,10 +100,14 @@ export default {
return this.data.meta.types.find((t) => t.id === type);
},
async submit() {
await this.axios[this.model.id ? 'patch' : 'post'](this.model.id ? this.model.links.update : this.data.meta.links.store, this.model);
try {
await this.axios[this.model.id ? 'patch' : 'post'](this.model.id ? this.model.links.update : this.data.meta.links.store, this.model);
this.reload();
this.model = null;
this.reload();
this.model = null;
} catch (e) {
this.errorsFromException(e);
}
},
},
components: {

View File

@ -38,9 +38,9 @@
mode = 'edit';
"
class="inline-flex btn btn-warning btn-sm"
><ui-sprite src="pencil"></ui-sprite
><svg-sprite src="pencil"></svg-sprite
></a>
<i-link href="#" @click.prevent="remove(course)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></i-link>
<i-link href="#" @click.prevent="remove(course)" class="inline-flex btn btn-danger btn-sm"><svg-sprite src="trash"></svg-sprite></i-link>
</td>
</tr>
</table>

View File

@ -8,8 +8,8 @@
</page-header>
<form v-if="single" class="p-6 grid gap-4 justify-start" @submit.prevent="submit">
<f-select id="group_id" name="group_id" :options="groups" v-model="single.group_id" label="Gruppierung" required></f-select>
<f-select id="activity_id" name="activity_id" :options="activities" v-model="single.activity_id" label="Tätigkeit" required></f-select>
<f-select id="group_id" name="group_id" :options="groups" v-model="single.group_id" label="Gruppierung" size="sm" required></f-select>
<f-select id="activity_id" name="activity_id" :options="activities" v-model="single.activity_id" label="Tätigkeit" size="sm" required></f-select>
<f-select
v-if="single.activity_id"
name="subactivity_id"
@ -19,7 +19,7 @@
label="Untertätigkeit"
size="sm"
></f-select>
<f-switch id="has_promise" :modelValue="single.promised_at !== null" @update:modelValue="single.promised_at = $event ? '2000-02-02' : null" label="Hat Versprechen"></f-switch>
<f-switch id="has_promise" :items="single.promised_at !== null" @input="single.promised_at = $event ? '2000-02-02' : null" size="sm" label="Hat Versprechen"></f-switch>
<f-text v-show="single.promised_at !== null" type="date" id="promised_at" v-model="single.promised_at" label="Versprechensdatum" size="sm"></f-text>
<button type="submit" class="btn btn-primary">Absenden</button>
</form>
@ -45,9 +45,9 @@
mode = 'edit';
"
class="inline-flex btn btn-warning btn-sm"
><ui-sprite src="pencil"></ui-sprite
><svg-sprite src="pencil"></svg-sprite
></a>
<i-link href="#" @click.prevent="remove(membership)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></i-link>
<i-link href="#" @click.prevent="remove(membership)" class="inline-flex btn btn-danger btn-sm"><svg-sprite src="trash"></svg-sprite></i-link>
</td>
</tr>
</table>

View File

@ -35,10 +35,10 @@
mode = 'edit';
"
class="inline-flex btn btn-warning btn-sm"
><ui-sprite src="pencil"></ui-sprite
><svg-sprite src="pencil"></svg-sprite
></a>
<i-link v-show="!payment.is_accepted" href="#" @click.prevent="accept(payment)" class="inline-flex btn btn-success btn-sm"><ui-sprite src="check"></ui-sprite></i-link>
<i-link href="#" @click.prevent="remove(payment)" class="inline-flex btn btn-danger btn-sm"><ui-sprite src="trash"></ui-sprite></i-link>
<i-link v-show="!payment.is_accepted" href="#" @click.prevent="accept(payment)" class="inline-flex btn btn-success btn-sm"><svg-sprite src="check"></svg-sprite></i-link>
<i-link href="#" @click.prevent="remove(payment)" class="inline-flex btn btn-danger btn-sm"><svg-sprite src="trash"></svg-sprite></i-link>
</td>
</tr>
</table>

View File

@ -49,15 +49,14 @@
</ui-box>
<ui-box heading="Karte" container-class="grow" class="area-map hidden xl:flex">
<div class="h-full flex items-center justify-center text-gray-400 text-center">Keine Karte vorhanden</div>
<vmap v-if="inner.lat && inner.lon" :value="[inner.lat, inner.lon]"></vmap>
<div class="h-full flex items-center justify-center text-gray-400 text-center" v-else>Keine Karte vorhanden</div>
</ui-box>
</div>
</page-layout>
</template>
<script>
import {defineAsyncComponent} from 'vue';
export default {
data: function () {
return {
@ -96,14 +95,15 @@ export default {
},
components: {
stamm: defineAsyncComponent(() => import('./boxes/Stamm.vue')),
kontakt: defineAsyncComponent(() => import('./boxes/Kontakt.vue')),
prae: defineAsyncComponent(() => import('./boxes/Prae.vue')),
courses: defineAsyncComponent(() => import('./boxes/Courses.vue')),
system: defineAsyncComponent(() => import('./boxes/System.vue')),
payments: defineAsyncComponent(() => import('./boxes/Payments.vue')),
memberships: defineAsyncComponent(() => import('./boxes/Memberships.vue')),
tabs: defineAsyncComponent(() => import('./Tabs.vue')),
stamm: () => import(/* webpackChunkName: "member" */ './boxes/Stamm.vue'),
kontakt: () => import(/* webpackChunkName: "member" */ './boxes/Kontakt.vue'),
prae: () => import(/* webpackChunkName: "member" */ './boxes/Prae.vue'),
courses: () => import(/* webpackChunkName: "member" */ './boxes/Courses.vue'),
system: () => import(/* webpackChunkName: "member" */ './boxes/System.vue'),
payments: () => import(/* webpackChunkName: "member" */ './boxes/Payments.vue'),
memberships: () => import(/* webpackChunkName: "member" */ './boxes/Memberships.vue'),
vmap: () => import(/* webpackChunkName: "member" */ './boxes/Vmap.vue'),
tabs: () => import(/* webpackChunkName: "member" */ './Tabs.vue'),
},
created() {

View File

@ -29,18 +29,18 @@ export default {
},
props: {
modelValue: {},
value: {},
},
methods: {
navigate(v) {
this.inner.active = v;
this.$emit('update:modelValue', this.inner);
this.$emit('input', this.inner);
},
},
created() {
this.inner = this.modelValue;
this.inner = this.value;
},
};
</script>

View File

@ -4,10 +4,8 @@
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">zurück</page-toolbar-button>
<page-toolbar-button v-if="mode === 'edit'" :href="data.links.show" color="primary" icon="eye">anschauen</page-toolbar-button>
</template>
<template #right>
<f-save-button form="memberedit"></f-save-button>
</template>
<form class="flex grow relative" id="memberedit" @submit.prevent="submit">
<f-save-button form="memberedit"></f-save-button>
<ui-popup heading="Ein Konflikt ist aufgetreten" v-if="conflict === true">
<div>
<p class="mt-4">Dieses Mitglied wurde vorher bereits aktualisiert. Daher könnte ein Update zu Datenverlust führen.</p>

View File

@ -20,13 +20,13 @@
</div>
</ui-popup>
<div class="px-6 py-2 flex border-b border-gray-600 items-center space-x-3">
<f-text :modelValue="getFilter('search')" @update:modelValue="setFilter('search', $event)" id="search" name="search" label="Suchen …" size="sm"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" @update:modelValue="setFilter('ausstand', $event)" :modelValue="getFilter('ausstand')" label="Nur Ausstände" size="sm"></f-switch>
<f-text :value="getFilter('search')" @input="setFilter('search', $event)" id="search" name="search" label="Suchen …" size="sm"></f-text>
<f-switch v-show="hasModule('bill')" id="ausstand" @input="setFilter('ausstand', $event)" :items="getFilter('ausstand')" label="Nur Ausstände" size="sm"></f-switch>
<f-multipleselect
id="group_ids"
@update:modelValue="setFilter('group_ids', $event)"
@input="setFilter('group_ids', $event)"
:options="data.meta.groups"
:modelValue="getFilter('group_ids')"
:value="getFilter('group_ids')"
label="Gruppierungen"
size="sm"
name="group_ids"
@ -35,32 +35,32 @@
v-show="hasModule('bill')"
name="billKinds"
id="billKinds"
@update:modelValue="setFilter('bill_kind', $event)"
@input="setFilter('bill_kind', $event)"
:options="data.meta.billKinds"
:modelValue="getFilter('bill_kind')"
:value="getFilter('bill_kind')"
label="Rechnung"
size="sm"
></f-select>
<f-multipleselect
id="activity_ids"
@update:modelValue="setFilter('activity_ids', $event)"
@input="setFilter('activity_ids', $event)"
:options="data.meta.filterActivities"
:modelValue="getFilter('activity_ids')"
:value="getFilter('activity_ids')"
label="Tätigkeiten"
size="sm"
name="activity_ids"
></f-multipleselect>
<f-multipleselect
id="subactivity_ids"
@update:modelValue="setFilter('subactivity_ids', $event)"
@input="setFilter('subactivity_ids', $event)"
:options="data.meta.filterSubactivities"
:modelValue="getFilter('subactivity_ids')"
:value="getFilter('subactivity_ids')"
label="Untertätigkeiten"
size="sm"
name="subactivity_ids"
></f-multipleselect>
<button class="btn btn-primary label mr-2" @click.prevent="exportMembers">
<ui-sprite class="w-3 h-3 xl:mr-2" src="save"></ui-sprite>
<svg-sprite class="w-3 h-3 xl:mr-2" src="save"></svg-sprite>
<span class="hidden xl:inline">Exportieren</span>
</button>
</div>
@ -109,7 +109,7 @@
</div>
<actions class="mt-2" :member="member" @sidebar="openSidebar(index, $event)" @remove="remove(member)"></actions>
<div class="absolute right-0 top-0 h-full flex items-center mr-2">
<i-link :href="member.links.show" v-tooltip="`Details`"><ui-sprite src="chevron-down" class="w-6 h-6 text-teal-100 -rotate-90"></ui-sprite></i-link>
<i-link :href="member.links.show" v-tooltip="`Details`"><svg-sprite src="chevron-down" class="w-6 h-6 text-teal-100 -rotate-90"></svg-sprite></i-link>
</div>
</ui-box>
</div>
@ -118,22 +118,24 @@
<ui-pagination class="mt-4" :value="data.meta" :only="['data']"></ui-pagination>
</div>
<member-payments
v-if="single !== null && sidebar === 'payment.index'"
@close="closeSidebar"
:subscriptions="data.meta.subscriptions"
:statuses="data.meta.statuses"
:value="data.data[single]"
></member-payments>
<member-memberships
v-if="single !== null && sidebar === 'membership.index'"
@close="closeSidebar"
:groups="data.meta.groups"
:activities="data.meta.formActivities"
:subactivities="data.meta.formSubactivities"
:value="data.data[single]"
></member-memberships>
<member-courses v-if="single !== null && sidebar === 'courses.index'" @close="closeSidebar" :courses="data.meta.courses" :value="data.data[single]"></member-courses>
<transition name="sidebar">
<member-payments
v-if="single !== null && sidebar === 'payment.index'"
@close="closeSidebar"
:subscriptions="data.meta.subscriptions"
:statuses="data.meta.statuses"
:value="data.data[single]"
></member-payments>
<member-memberships
v-if="single !== null && sidebar === 'membership.index'"
@close="closeSidebar"
:groups="data.meta.groups"
:activities="data.meta.formActivities"
:subactivities="data.meta.formSubactivities"
:value="data.data[single]"
></member-memberships>
<member-courses v-if="single !== null && sidebar === 'courses.index'" @close="closeSidebar" :courses="data.meta.courses" :value="data.data[single]"></member-courses>
</transition>
</page-layout>
</template>
@ -143,8 +145,6 @@ import MemberMemberships from './MemberMemberships.vue';
import MemberCourses from './MemberCourses.vue';
import indexHelpers from '../../mixins/indexHelpers.js';
import hasModule from '../../mixins/hasModule.js';
import Tags from './Tags.vue';
import Actions from './index/Actions.vue';
export default {
data: function () {
@ -161,8 +161,8 @@ export default {
MemberMemberships,
MemberPayments,
MemberCourses,
Tags,
Actions,
tags: () => import('./Tags.vue'),
actions: () => import('./index/Actions.vue'),
},
methods: {

View File

@ -0,0 +1,43 @@
<template>
<l-map style="height: 100%" :zoom="zoom" :center="center">
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="markerLatLng"></l-marker>
</l-map>
</template>
<script>
import {LMap, LTileLayer, LMarker} from 'vue2-leaflet';
import {Icon} from 'leaflet';
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
export default {
props: {
value: {
required: true,
validator(f) {
return f.length === 2;
},
},
},
components: {
LMap,
LTileLayer,
LMarker,
},
data() {
return {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
zoom: 15,
center: [...this.value],
markerLatLng: [...this.value],
};
},
};
</script>

View File

@ -1,16 +1,48 @@
<template>
<div class="flex space-x-1">
<i-link :href="member.links.show" class="inline-flex btn btn-primary btn-sm" v-tooltip="`Details`"><ui-sprite src="eye"></ui-sprite></i-link>
<i-link :href="`/member/${member.id}/edit`" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"><ui-sprite src="pencil"></ui-sprite></i-link>
<a href="#" v-tooltip="`Zahlungen`" v-show="hasModule('bill')" @click.prevent="$emit('sidebar', 'payment.index')" class="inline-flex btn btn-info btn-sm"
><ui-sprite src="money"></ui-sprite
<i-link :href="member.links.show" class="inline-flex btn btn-primary btn-sm" v-tooltip="`Details`"
><svg-sprite src="eye"></svg-sprite
></i-link>
<i-link :href="`/member/${member.id}/edit`" class="inline-flex btn btn-warning btn-sm" v-tooltip="`bearbeiten`"
><svg-sprite src="pencil"></svg-sprite
></i-link>
<a
href="#"
v-tooltip="`Zahlungen`"
v-show="hasModule('bill')"
@click.prevent="$emit('sidebar', 'payment.index')"
class="inline-flex btn btn-info btn-sm"
><svg-sprite src="money"></svg-sprite
></a>
<a href="#" v-tooltip="`Ausbildungen`" v-show="hasModule('courses')" @click.prevent="$emit('sidebar', 'courses.index')" class="inline-flex btn btn-info btn-sm"
><ui-sprite src="course"></ui-sprite
<a
href="#"
v-tooltip="`Ausbildungen`"
v-show="hasModule('courses')"
@click.prevent="$emit('sidebar', 'courses.index')"
class="inline-flex btn btn-info btn-sm"
><svg-sprite src="course"></svg-sprite
></a>
<a href="#" v-tooltip="`Mitgliedschaften`" @click.prevent="$emit('sidebar', 'membership.index')" class="inline-flex btn btn-info btn-sm"><ui-sprite src="user"></ui-sprite></a>
<a :href="member.efz_link" v-show="member.efz_link" class="inline-flex btn btn-info btn-sm" v-tooltip="`EFZ Formular`"><ui-sprite src="report"></ui-sprite></a>
<i-link href="#" @click.prevent="$emit('remove')" class="inline-flex btn btn-danger btn-sm" v-tooltip="`Entfernen`"><ui-sprite src="trash"></ui-sprite></i-link>
<a
href="#"
v-tooltip="`Mitgliedschaften`"
@click.prevent="$emit('sidebar', 'membership.index')"
class="inline-flex btn btn-info btn-sm"
><svg-sprite src="user"></svg-sprite
></a>
<a
:href="member.efz_link"
v-show="member.efz_link"
class="inline-flex btn btn-info btn-sm"
v-tooltip="`EFZ Formular`"
><svg-sprite src="report"></svg-sprite
></a>
<i-link
href="#"
@click.prevent="$emit('remove')"
class="inline-flex btn btn-danger btn-sm"
v-tooltip="`Entfernen`"
><svg-sprite src="trash"></svg-sprite
></i-link>
</div>
</template>

View File

@ -1,10 +1,8 @@
<template>
<page-layout>
<template #right>
<f-save-button form="billsettingform"></f-save-button>
</template>
<setting-layout>
<form id="billsettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
<f-save-button form="billsettingform"></f-save-button>
<f-text label="Absender" hint="Absender-Name in Kurzform, i.d.R. der kurze Stammesname" name="from" id="from" v-model="inner.from"></f-text>
<f-text label="Absender (lang)" v-model="inner.from_long" name="from_long" id="from_long" hint="Absender-Name in Langform, i.d.R. der Stammesname"></f-text>
<h2 class="text-lg font-semibold text-gray-300 col-span-2 mt-5">Kontaktdaten</h2>
@ -36,9 +34,7 @@ export default {
},
methods: {
submit() {
this.$inertia.post('/setting/bill', this.inner, {
onSuccess: () => this.$success('Einstellungen gespeichert.')
});
this.$inertia.post('/setting/bill', this.inner);
},
},
components: {

View File

@ -1,10 +1,8 @@
<template>
<page-layout>
<template #right>
<f-save-button form="mailmansettingform"></f-save-button>
</template>
<setting-layout>
<form id="mailmansettingform" class="grow p-6 grid grid-cols-2 gap-3 items-start content-start" @submit.prevent="submit">
<f-save-button form="mailmansettingform"></f-save-button>
<div class="col-span-full text-gray-100 mb-3">
<p class="text-sm">
Scoutrobot kann automatisch Mailinglisten erstellen, wenn es mit einem existierenden
@ -16,7 +14,7 @@
<f-switch id="is_active" v-model="inner.is_active" label="Mailman-Synchronisation aktiv"></f-switch>
</div>
<div class="flex h-full items-center">
<ui-sprite :src="stateDisplay.icon" :class="stateDisplay.text" class="w-5 h-5"></ui-sprite>
<svg-sprite :src="stateDisplay.icon" :class="stateDisplay.text" class="w-5 h-5"></svg-sprite>
<span class="ml-3" :class="stateDisplay.text" v-text="stateDisplay.label"></span>
</div>
<f-text label="URL" hint="URL der Mailman Api" name="base_url" id="base_url" v-model="inner.base_url"></f-text>
@ -72,10 +70,11 @@ export default {
methods: {
submit() {
var _self = this;
this.$inertia.post('/setting/mailman', this.inner, {
onSuccess: (page) => {
this.$success('Einstellungen gespeichert.')
this.inner = page.props.data;
onSuccess(page) {
_self.inner = page.props.data;
},
});
},

View File

@ -3,10 +3,8 @@
<template #toolbar>
<page-toolbar-button :href="meta.links.index" color="primary" icon="undo">zurück</page-toolbar-button>
</template>
<template #right>
<f-save-button form="subedit"></f-save-button>
</template>
<form id="subedit" class="p-3 grid gap-3" @submit.prevent="submit">
<f-save-button form="subedit"></f-save-button>
<ui-box heading="Beitrag">
<div class="grid gap-4 sm:grid-cols-2">
<f-text id="name" v-model="inner.name" label="Name" size="sm" required></f-text>
@ -21,11 +19,11 @@
<f-text :id="`name-${index}`" v-model="pos.name" label="Name" size="sm" required></f-text>
<f-text :id="`amount-${index}`" v-model="pos.amount" label="Beitrag" size="sm" mode="area" required></f-text>
<a href="#" @click.prevent="inner.children.splice(index, 1)" class="btn btn-sm btn-danger icon flex-none">
<ui-sprite src="trash" class="w-5 h-5"></ui-sprite>
<svg-sprite src="trash" class="w-5 h-5"></svg-sprite>
</a>
</div>
<a href="#" @click.prevent="inner.children.push({name: '', amount: 0})" class="btn btn-sm flex btn-primary flex self-start mt-4">
<ui-sprite src="plus" class="w-5 h-5"></ui-sprite>
<svg-sprite src="plus" class="w-5 h-5"></svg-sprite>
Position hinzufügen
</a>
</div>

View File

@ -22,7 +22,7 @@
<div v-text="subscription.fee_name"></div>
</td>
<td>
<i-link :href="`/subscription/${subscription.id}/edit`" class="inline-flex btn btn-warning btn-sm"><ui-sprite src="pencil"></ui-sprite></i-link>
<i-link :href="`/subscription/${subscription.id}/edit`" class="inline-flex btn btn-warning btn-sm"><svg-sprite src="pencil"></svg-sprite></i-link>
</td>
</tr>
</table>

View File

@ -7,6 +7,6 @@
@vite('resources/js/app.js')
</head>
<body class="min-h-full flex flex-col">
@inertia('app" class="bg-gray-900 font-sans flex flex-col grow"')
@inertia
</body>
</html>

2
vite.config.js vendored
View File

@ -1,6 +1,6 @@
import {defineConfig} from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import vue from '@vitejs/plugin-vue2';
export default defineConfig({
plugins: [