23-01-2026
This commit is contained in:
parent
07959c0ba2
commit
854ce02bf6
166 changed files with 32909 additions and 1262 deletions
|
|
@ -63,13 +63,53 @@ select:focus[data-flux-control] {
|
|||
@apply outline-hidden ring-2 ring-accent ring-offset-2 ring-offset-accent-foreground;
|
||||
}
|
||||
|
||||
/* Verbesserte Kontraste für Input-Werte */
|
||||
input[data-flux-control],
|
||||
textarea[data-flux-control],
|
||||
select[data-flux-control] {
|
||||
@apply text-zinc-900 dark:text-zinc-50;
|
||||
}
|
||||
|
||||
/* Placeholder sollte deutlich heller sein */
|
||||
input[data-flux-control]::placeholder,
|
||||
textarea[data-flux-control]::placeholder {
|
||||
@apply text-zinc-400 dark:text-zinc-700;
|
||||
}
|
||||
|
||||
.shadow-elegant {
|
||||
box-shadow: 0 4px 12px -8px rgba(0, 136, 204, 0.4);
|
||||
}
|
||||
.bg-background {
|
||||
background-color: hsl(var(--background));
|
||||
|
||||
& button,
|
||||
& .btn,
|
||||
& .btn-secondary,
|
||||
& a[class*="btn"],
|
||||
& [role="button"] {
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
filter 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* \[:where(&)\]:size-4 {
|
||||
@apply size-4;
|
||||
} */
|
||||
& button:hover,
|
||||
& .btn:hover,
|
||||
& .btn-secondary:hover,
|
||||
& a[class*="btn"]:hover,
|
||||
& [role="button"]:hover {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.06),
|
||||
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||
0 4px 8px rgba(0, 0, 0, 0.1),
|
||||
0 8px 16px rgba(0, 0, 0, 0.08),
|
||||
0 0 10px hsla(199, 74%, 49%, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Scroll-Optimierung für sanftere Animationen */
|
||||
& {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Theme-specific button styles for Backend Portal */
|
||||
|
||||
|
|
|
|||
|
|
@ -83,16 +83,16 @@ h1, h2, h3, h4, h5, h6 {
|
|||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: hsl(var(--secondary));
|
||||
background-color: hsl(var(--primary-dark));
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
hsl(var(--primary-light)) 0%,
|
||||
hsl(var(--primary)) 100%
|
||||
) !important;
|
||||
color: hsl(var(--secondary-lighter));
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +112,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary-accent:hover {
|
||||
|
|
@ -121,7 +122,6 @@ h1, h2, h3, h4, h5, h6 {
|
|||
hsl(var(--primary-light)) 0%,
|
||||
hsl(var(--primary)) 100%
|
||||
) !important;
|
||||
color: hsl(var(--secondary-lighter));
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
|
|
@ -141,12 +141,18 @@ h1, h2, h3, h4, h5, h6 {
|
|||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-lighter));
|
||||
}
|
||||
background-color: hsl(var(--secondary-dark));
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
hsl(var(--secondary-dark)) 0%,
|
||||
hsl(var(--secondary)) 100%
|
||||
) !important;
|
||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary-accent {
|
||||
background-color: hsl(var(--secondary));
|
||||
|
|
@ -163,6 +169,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary-accent:hover {
|
||||
|
|
@ -172,9 +179,14 @@ h1, h2, h3, h4, h5, h6 {
|
|||
hsl(var(--secondary-dark)) 0%,
|
||||
hsl(var(--secondary)) 100%
|
||||
) !important;
|
||||
color: hsl(var(--primary-lighter));
|
||||
}
|
||||
|
||||
.btn-secondary-accent.small {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
|
||||
.btn-accent {
|
||||
background-color: hsl(var(--accent));
|
||||
background: linear-gradient(
|
||||
|
|
@ -190,6 +202,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-accent:hover {
|
||||
|
|
@ -648,3 +661,191 @@ h1, h2, h3, h4, h5, h6 {
|
|||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shadow Effects - Updated for new primary color */
|
||||
.shadow-elegant {
|
||||
box-shadow: 0 10px 30px -10px rgba(0, 136, 204, 0.2);
|
||||
}
|
||||
|
||||
.shadow-elegant-white {
|
||||
box-shadow: 0 10px 30px -10px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.shadow-white {
|
||||
box-shadow: 0 10px 30px -10px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.elegant-shadow-card {
|
||||
box-shadow:
|
||||
0 2px 8px -2px rgba(0, 0, 0, 0.08),
|
||||
0 4px 10px 0px rgba(0, 136, 204,1);
|
||||
}
|
||||
|
||||
.shadow-premium {
|
||||
box-shadow: 0 20px 60px -15px rgba(0, 136, 204, 0.3);
|
||||
}
|
||||
|
||||
/* Glow Effects - Inspired by Möbius band's luminous edge */
|
||||
.glow-soft {
|
||||
box-shadow:
|
||||
0 0 10px rgba(0, 155, 221, 0.15),
|
||||
0 0 20px rgba(0, 155, 221, 0.1),
|
||||
0 4px 10px -4px rgba(0, 136, 204, 0.2);
|
||||
}
|
||||
|
||||
.glow-medium {
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 155, 221, 0.25),
|
||||
0 0 30px rgba(0, 155, 221, 0.15),
|
||||
0 5px 10px -5px rgba(0, 136, 204, 0.3);
|
||||
}
|
||||
|
||||
.glow-strong {
|
||||
box-shadow:
|
||||
0 0 30px rgba(0, 155, 221, 0.35),
|
||||
0 0 60px rgba(0, 155, 221, 0.2),
|
||||
0 0 90px rgba(0, 113, 168, 0.1),
|
||||
0 10px 10px -10px rgba(0, 136, 204, 0.4);
|
||||
}
|
||||
.icon-secondary-linear {
|
||||
background-color: hsl(var(--secondary) / 0.1);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--secondary)) 0%,
|
||||
hsl(var(--secondary-dark)) 100%
|
||||
);
|
||||
color: hsl(var(--secondary-foreground)) !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-section-title {
|
||||
line-height: 0.95em;
|
||||
}
|
||||
.variante-glass-flow {
|
||||
|
||||
& * {
|
||||
will-change: auto;
|
||||
} section,
|
||||
& .section-container {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--background)) 0%,
|
||||
hsl(var(--background)/90%) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& section.bg-accent {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--accent) /0.5) 0%,
|
||||
hsl(var(--accent-dark) / 0.5) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& section.bg-secondary {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--secondary)) 0%,
|
||||
hsl(var(--secondary-dark)) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& div.bg-hero-container {
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--hero-container)) 0%,
|
||||
hsl(var(--hero-container-dark)) 100%
|
||||
);
|
||||
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
/* Cards - Multi-Layer-Schatten für Tiefe (Optimiert) */
|
||||
& .card,
|
||||
& [class*="card"],
|
||||
& .bg-card {
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
hsl(0 0% 100%) 0%,
|
||||
hsl(0 0% 99.5%) 40%,
|
||||
hsl(0 0% 99%) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.03),
|
||||
0 1px 1px rgba(0, 0, 0, 0.02),
|
||||
0 2px 3px rgba(0, 0, 0, 0.025),
|
||||
0 4px 6px rgba(0, 0, 0, 0.03),
|
||||
0 8px 12px rgba(0, 0, 0, 0.04),
|
||||
0 12px 20px rgba(0, 0, 0, 0.045),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.8),
|
||||
inset 0 -1px 1px rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
filter 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
||||
& .card:hover,
|
||||
& [class*="card"]:hover {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04),
|
||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||
0 4px 8px rgba(0, 0, 0, 0.04),
|
||||
0 8px 16px rgba(0, 0, 0, 0.05),
|
||||
0 16px 24px rgba(0, 0, 0, 0.07),
|
||||
0 24px 40px rgba(0, 0, 0, 0.08),
|
||||
0 32px 64px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 3px rgba(255, 255, 255, 0.9),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||
transform: translateY(-2px) scale(1.005);
|
||||
filter: brightness(1.01);
|
||||
}
|
||||
|
||||
& button,
|
||||
& .btn,
|
||||
& .btn-secondary,
|
||||
& a[class*="btn"],
|
||||
& [role="button"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.05),
|
||||
0 2px 4px rgba(0, 0, 0, 0.06),
|
||||
0 4px 8px rgba(0, 0, 0, 0.07),
|
||||
0 8px 16px rgba(0, 0, 0, 0.05);
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
filter 0.2s ease;
|
||||
}
|
||||
|
||||
& button:hover,
|
||||
& .btn:hover,
|
||||
& .btn-secondary:hover,
|
||||
& a[class*="btn"]:hover,
|
||||
& [role="button"]:hover {
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.06),
|
||||
0 4px 8px rgba(0, 0, 0, 0.08),
|
||||
0 8px 16px rgba(0, 0, 0, 0.1),
|
||||
0 16px 32px rgba(0, 0, 0, 0.08),
|
||||
0 0 40px hsla(199, 74%, 49%, 0.3);
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
/* Scroll-Optimierung für sanftere Animationen */
|
||||
& {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,14 @@
|
|||
|
||||
/* B2A Theme Colors */
|
||||
--primary: 207 70% 26%; /* #123f6d - Azur Blue */
|
||||
--primary-light: 207 70% 36%; /* #123f6d - Azur Blue */
|
||||
--primary-lighter: 207 70% 86%; /* #123f6d - Azur Blue */
|
||||
--primary-dark: 207 70% 20%; /* #123f6d - Azur Blue */
|
||||
--primary-foreground: 30 25% 98%; /* #faf9f7 - Off White */
|
||||
--secondary: 352 76% 48%; /* #ce1d2e - Liberty Red */
|
||||
--secondary-light: 352 76% 56%; /* #ce1d2e - Liberty Red */
|
||||
--secondary-lighter: 352 76% 75%; /* #ce1d2e - Liberty Red */
|
||||
--secondary-dark: 352 76% 42%; /* #ce1d2e - Liberty Red */
|
||||
--secondary-foreground: 0 25% 96%; /* #hsl(0 25% 96%) - Off White */
|
||||
|
||||
/* Neutral colors */
|
||||
|
|
@ -39,13 +45,18 @@
|
|||
|
||||
/* Hero container background */
|
||||
--hero-container: 0 0% 91%; /* #e8e8e8 - Light Gray */
|
||||
|
||||
--hero-container-dark: 0 0% 83%; /* #e8e8e8 - Light Gray */
|
||||
--hero-container-light: 0 0% 93%; /* #e8e8e8 - Light Gray */
|
||||
/* Shadows */
|
||||
--shadow-warm: 0 10px 30px -15px hsl(var(--foreground) / 0.1);
|
||||
--shadow-card: 0 4px 20px -8px hsl(var(--foreground) / 0.08);
|
||||
--shadow-elevated: 0 20px 40px -20px hsl(var(--foreground) / 0.15);
|
||||
--shadow-accent-glow: 0 0 30px hsl(var(--secondary) / 0.3);
|
||||
|
||||
/* Transitions */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
/* Font families */
|
||||
--font-primary: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-secondary: 'Merriweather', Georgia, serif;
|
||||
|
|
|
|||
|
|
@ -72,192 +72,5 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Shadow Effects - Updated for new primary color */
|
||||
.shadow-elegant {
|
||||
box-shadow: 0 10px 30px -10px rgba(0, 136, 204, 0.2);
|
||||
}
|
||||
|
||||
.shadow-elegant-white {
|
||||
box-shadow: 0 10px 30px -10px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.shadow-white {
|
||||
box-shadow: 0 10px 30px -10px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.elegant-shadow-card {
|
||||
box-shadow:
|
||||
0 2px 8px -2px rgba(0, 0, 0, 0.08),
|
||||
0 4px 10px 0px rgba(0, 136, 204,1);
|
||||
}
|
||||
|
||||
.shadow-premium {
|
||||
box-shadow: 0 20px 60px -15px rgba(0, 136, 204, 0.3);
|
||||
}
|
||||
|
||||
/* Glow Effects - Inspired by Möbius band's luminous edge */
|
||||
.glow-soft {
|
||||
box-shadow:
|
||||
0 0 10px rgba(0, 155, 221, 0.15),
|
||||
0 0 20px rgba(0, 155, 221, 0.1),
|
||||
0 4px 10px -4px rgba(0, 136, 204, 0.2);
|
||||
}
|
||||
|
||||
.glow-medium {
|
||||
box-shadow:
|
||||
0 0 15px rgba(0, 155, 221, 0.25),
|
||||
0 0 30px rgba(0, 155, 221, 0.15),
|
||||
0 5px 10px -5px rgba(0, 136, 204, 0.3);
|
||||
}
|
||||
|
||||
.glow-strong {
|
||||
box-shadow:
|
||||
0 0 30px rgba(0, 155, 221, 0.35),
|
||||
0 0 60px rgba(0, 155, 221, 0.2),
|
||||
0 0 90px rgba(0, 113, 168, 0.1),
|
||||
0 10px 10px -10px rgba(0, 136, 204, 0.4);
|
||||
}
|
||||
.icon-secondary-linear {
|
||||
background-color: hsl(var(--secondary) / 0.1);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--secondary)) 0%,
|
||||
hsl(var(--secondary-dark)) 100%
|
||||
);
|
||||
color: hsl(var(--secondary-foreground)) !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-section-title {
|
||||
line-height: 0.95em;
|
||||
}
|
||||
.variante-glass-flow {
|
||||
|
||||
& * {
|
||||
will-change: auto;
|
||||
} section,
|
||||
& .section-container {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--background)) 0%,
|
||||
hsl(var(--background)/90%) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& section.bg-accent {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--accent) /0.5) 0%,
|
||||
hsl(var(--accent-dark) / 0.5) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& section.bg-secondary {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--secondary)) 0%,
|
||||
hsl(var(--secondary-dark)) 100%
|
||||
);
|
||||
position: relative;
|
||||
}
|
||||
& div.bg-hero-container {
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
hsl(var(--hero-container)) 0%,
|
||||
hsl(var(--hero-container-dark)) 100%
|
||||
);
|
||||
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
/* Cards - Multi-Layer-Schatten für Tiefe (Optimiert) */
|
||||
& .card,
|
||||
& [class*="card"],
|
||||
& .bg-card {
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
hsl(0 0% 100%) 0%,
|
||||
hsl(0 0% 99.5%) 40%,
|
||||
hsl(0 0% 99%) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.03),
|
||||
0 1px 1px rgba(0, 0, 0, 0.02),
|
||||
0 2px 3px rgba(0, 0, 0, 0.025),
|
||||
0 4px 6px rgba(0, 0, 0, 0.03),
|
||||
0 8px 12px rgba(0, 0, 0, 0.04),
|
||||
0 12px 20px rgba(0, 0, 0, 0.045),
|
||||
inset 0 1px 2px rgba(255, 255, 255, 0.8),
|
||||
inset 0 -1px 1px rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
transition:
|
||||
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
box-shadow 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||
filter 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
||||
& .card:hover,
|
||||
& [class*="card"]:hover {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04),
|
||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||
0 4px 8px rgba(0, 0, 0, 0.04),
|
||||
0 8px 16px rgba(0, 0, 0, 0.05),
|
||||
0 16px 24px rgba(0, 0, 0, 0.07),
|
||||
0 24px 40px rgba(0, 0, 0, 0.08),
|
||||
0 32px 64px rgba(0, 0, 0, 0.06),
|
||||
inset 0 1px 3px rgba(255, 255, 255, 0.9),
|
||||
inset 0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||
transform: translateY(-2px) scale(1.005);
|
||||
filter: brightness(1.01);
|
||||
}
|
||||
|
||||
& button,
|
||||
& .btn,
|
||||
& .btn-secondary,
|
||||
& a[class*="btn"],
|
||||
& [role="button"] {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.05),
|
||||
0 2px 4px rgba(0, 0, 0, 0.06),
|
||||
0 4px 8px rgba(0, 0, 0, 0.07),
|
||||
0 8px 16px rgba(0, 0, 0, 0.05);
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
filter 0.2s ease;
|
||||
}
|
||||
|
||||
& button:hover,
|
||||
& .btn:hover,
|
||||
& .btn-secondary:hover,
|
||||
& a[class*="btn"]:hover,
|
||||
& [role="button"]:hover {
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.06),
|
||||
0 4px 8px rgba(0, 0, 0, 0.08),
|
||||
0 8px 16px rgba(0, 0, 0, 0.1),
|
||||
0 16px 32px rgba(0, 0, 0, 0.08),
|
||||
0 0 40px hsla(199, 74%, 49%, 0.3);
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
/* Scroll-Optimierung für sanftere Animationen */
|
||||
& {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
@import "./shared-styles.css";
|
||||
|
|
|
|||
|
|
@ -14,8 +14,14 @@
|
|||
|
||||
/* Stileigentum Theme Colors */
|
||||
--primary: 209 65% 20%; /* #123453 - Imperial Blue */
|
||||
--primary-light: 209 65% 30%; /* #123453 - Imperial Blue */
|
||||
--primary-lighter: 209 65% 80%; /* #123453 - Imperial Blue */
|
||||
--primary-dark: 209 65% 15%; /* #123453 - Imperial Blue */
|
||||
--primary-foreground: 0 25% 96%; /* #hsl(0 25% 96%) - Off White */
|
||||
--secondary: 38 40% 66%; /* #c9ac84 - Sand Gold */
|
||||
--secondary-light: 38 40% 76%; /* #c9ac84 - Sand Gold */
|
||||
--secondary-lighter: 38 40% 85%; /* #c9ac84 - Sand Gold */
|
||||
--secondary-dark: 38 40% 55%; /* #c9ac84 - Sand Gold */
|
||||
--secondary-foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */
|
||||
|
||||
/* Neutral colors */
|
||||
|
|
@ -38,13 +44,18 @@
|
|||
|
||||
/* Hero container background */
|
||||
--hero-container: 0 0% 91%; /* #e8e8e8 - Light Gray */
|
||||
|
||||
--hero-container-dark: 0 0% 83%; /* #e8e8e8 - Light Gray */
|
||||
--hero-container-light: 0 0% 93%; /* #e8e8e8 - Light Gray */
|
||||
/* Shadows */
|
||||
--shadow-warm: 0 10px 30px -15px hsl(var(--foreground) / 0.1);
|
||||
--shadow-card: 0 4px 20px -8px hsl(var(--foreground) / 0.08);
|
||||
--shadow-elevated: 0 20px 40px -20px hsl(var(--foreground) / 0.15);
|
||||
--shadow-accent-glow: 0 0 30px hsl(var(--secondary) / 0.3);
|
||||
|
||||
/* Transitions */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
/* Font families */
|
||||
--font-primary: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
--font-secondary: 'EB Garamond', Georgia, serif;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,14 @@
|
|||
|
||||
/* Style2own Theme Colors */
|
||||
--primary: 195 100% 34%; /* #007aab - Style Blue */
|
||||
--primary-light: 195 100% 44%; /* #007aab - Style Blue */
|
||||
--primary-lighter: 195 100% 84%; /* #007aab - Style Blue */
|
||||
--primary-dark: 195 100% 24%; /* #007aab - Style Blue */
|
||||
--primary-foreground: 30 25% 98%; /* #faf9f7 - Off White */
|
||||
--secondary: 46 95% 56%; /* #fbaf22 - Style Sun */
|
||||
--secondary-light: 46 95% 66%; /* #fbaf22 - Style Sun */
|
||||
--secondary-lighter: 46 95% 86%; /* #fbaf22 - Style Sun */
|
||||
--secondary-dark: 46 95% 46%; /* #fbaf22 - Style Sun */
|
||||
--secondary-foreground: 20 14% 16%; /* #2a2a2a - Dark Gray */
|
||||
|
||||
/* Neutral colors */
|
||||
|
|
@ -39,6 +45,12 @@
|
|||
|
||||
/* Hero container background */
|
||||
--hero-container: 0 0% 91%; /* #e8e8e8 - Light Gray */
|
||||
--hero-container-dark: 0 0% 83%; /* #e8e8e8 - Light Gray */
|
||||
--hero-container-light: 0 0% 93%; /* #e8e8e8 - Light Gray */
|
||||
|
||||
/* Transitions */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-warm: 0 10px 30px -15px hsl(var(--foreground) / 0.1);
|
||||
|
|
|
|||
|
|
@ -3,11 +3,41 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Globaler Observer, der wiederverwendet wird
|
||||
let globalObserver = null;
|
||||
|
||||
// Warte bis DOM vollständig geladen ist
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initAnimations);
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function init() {
|
||||
initAnimations();
|
||||
|
||||
// Scroll Progress Indicator
|
||||
initScrollProgress();
|
||||
|
||||
// Premium Sticky Header
|
||||
initStickyHeader();
|
||||
|
||||
// Livewire Event Listener für dynamisch geladene Komponenten
|
||||
document.addEventListener('livewire:navigated', function() {
|
||||
initAnimations();
|
||||
});
|
||||
|
||||
// Fallback für ältere Livewire-Versionen
|
||||
document.addEventListener('livewire:load', function() {
|
||||
initAnimations();
|
||||
});
|
||||
|
||||
// Nach Livewire-Updates
|
||||
window.addEventListener('livewire:update', function() {
|
||||
setTimeout(() => {
|
||||
initAnimations();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function initAnimations() {
|
||||
|
|
@ -17,56 +47,56 @@
|
|||
rootMargin: '0px 0px -80px 0px'
|
||||
};
|
||||
|
||||
// Erstelle Observer
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
// Füge is-visible Klasse mit kleinem Delay hinzu für sanfteren Effekt
|
||||
setTimeout(() => {
|
||||
entry.target.classList.add('is-visible');
|
||||
}, 50);
|
||||
// Erstelle Observer wenn noch nicht vorhanden
|
||||
if (!globalObserver) {
|
||||
globalObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
// Füge is-visible Klasse mit kleinem Delay hinzu für sanfteren Effekt
|
||||
setTimeout(() => {
|
||||
entry.target.classList.add('is-visible');
|
||||
}, 50);
|
||||
|
||||
// Observer beenden nach Animation für bessere Performance
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
// Observer beenden nach Animation für bessere Performance
|
||||
globalObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
}
|
||||
|
||||
// Finde alle Elemente mit Animation-Klassen
|
||||
// Finde alle Elemente mit Animation-Klassen, die noch nicht beobachtet werden
|
||||
const animatedElements = document.querySelectorAll(
|
||||
'.scroll-animate, .fade-in, .slide-up, .slide-right, .slide-left, .scale-in'
|
||||
'.scroll-animate:not(.is-visible):not(.observed), .fade-in:not(.is-visible):not(.observed), .slide-up:not(.is-visible):not(.observed), .slide-right:not(.is-visible):not(.observed), .slide-left:not(.is-visible):not(.observed), .scale-in:not(.is-visible):not(.observed), .slide-down:not(.is-visible):not(.observed)'
|
||||
);
|
||||
|
||||
// Beobachte jedes Element
|
||||
animatedElements.forEach(el => {
|
||||
observer.observe(el);
|
||||
el.classList.add('observed'); // Markiere als beobachtet
|
||||
globalObserver.observe(el);
|
||||
});
|
||||
|
||||
// Smooth Scroll für Anchor-Links
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('a[href^="#"]');
|
||||
if (target && target.hash) {
|
||||
const targetElement = document.querySelector(target.hash);
|
||||
// Smooth Scroll für Anchor-Links (nur einmal registrieren)
|
||||
if (!window.smoothScrollInitialized) {
|
||||
document.addEventListener('click', function(e) {
|
||||
const target = e.target.closest('a[href^="#"]');
|
||||
if (target && target.hash) {
|
||||
const targetElement = document.querySelector(target.hash);
|
||||
|
||||
if (targetElement) {
|
||||
e.preventDefault();
|
||||
const headerOffset = 80;
|
||||
const elementPosition = targetElement.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
||||
if (targetElement) {
|
||||
e.preventDefault();
|
||||
const headerOffset = 80;
|
||||
const elementPosition = targetElement.getBoundingClientRect().top;
|
||||
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll Progress Indicator
|
||||
initScrollProgress();
|
||||
|
||||
// Premium Sticky Header
|
||||
initStickyHeader();
|
||||
});
|
||||
window.smoothScrollInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function initStickyHeader() {
|
||||
|
|
|
|||
85
resources/lang/de/registration.php
Normal file
85
resources/lang/de/registration.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'roles' => [
|
||||
'customer' => [
|
||||
'label' => 'Kunde',
|
||||
'description' => 'Zugang als Kunde mit Ihrer persönlichen Kundennummer.',
|
||||
],
|
||||
'broker' => [
|
||||
'label' => 'Makler',
|
||||
'description' => 'Registrierung für Makler mit eigener Makler-Nummer.',
|
||||
],
|
||||
'manufacturer' => [
|
||||
'label' => 'Hersteller',
|
||||
'description' => 'Zugang für Hersteller, um Produkte zu pflegen.',
|
||||
],
|
||||
'retailer' => [
|
||||
'label' => 'Händler',
|
||||
'description' => 'Zugang für Händler, um Sortiment und Kunden zu verwalten.',
|
||||
],
|
||||
],
|
||||
|
||||
'steps' => [
|
||||
'code_entry' => [
|
||||
'title' => 'Code eingeben',
|
||||
'description' => 'Scannen Sie den QR-Code, geben Sie Ihren Registrierungscode ein (z.B. M00100001 oder K01102513) und lassen Sie ihn prüfen.',
|
||||
],
|
||||
'create_account' => [
|
||||
'title' => 'Konto erstellen',
|
||||
'description' => 'Nach erfolgreicher Prüfung legen Sie Ihr Konto an. Rolle: :role – damit wird Ihr Zugang korrekt zugeordnet.',
|
||||
],
|
||||
'complete_onboarding' => [
|
||||
'title' => 'Onboarding abschließen',
|
||||
'description' => 'Sie werden direkt in den passenden Setup-Prozess geleitet und schließen Ihr Profil in wenigen Minuten ab.',
|
||||
],
|
||||
],
|
||||
|
||||
'messages' => [
|
||||
'code_unique' => 'Jeder Code ist einzigartig und kann nur einmalig eingelöst werden.',
|
||||
'code_format' => 'Format: Buchstabe + 4×2 Ziffern, z.B. M 99 11 22 44',
|
||||
'code_invalid' => 'Dieser Registrierungscode ist ungültig oder wurde bereits verwendet.',
|
||||
'code_accepted' => 'Code akzeptiert. Bitte registrieren Sie sich jetzt.',
|
||||
'code_problems' => 'Probleme mit dem Code? Wenden Sie sich an Ihren Ansprechpartner oder den Support.',
|
||||
],
|
||||
|
||||
'titles' => [
|
||||
'registration' => 'Registrierung',
|
||||
'access_for_role' => 'Zugang für :role',
|
||||
'enter_code' => 'Geben Sie Ihren persönlichen Code ein',
|
||||
'how_it_works' => 'So läuft Ihre Registrierung ab',
|
||||
'how_it_works_description' => 'Drei Schritte bis zum Zugang – mit Ihrem persönlichen Registrierungscode und dem passenden Onboarding für :role.',
|
||||
],
|
||||
|
||||
'actions' => [
|
||||
'check_code' => 'Code prüfen & weiter',
|
||||
'how_it_works' => 'so gehts',
|
||||
'start_now' => 'Jetzt starten',
|
||||
],
|
||||
|
||||
'thank_you' => [
|
||||
'subtitle' => 'Registrierung erfolgreich',
|
||||
'title' => 'Vielen Dank für Ihre Registrierung!',
|
||||
'description' => 'Ihr Konto wurde erfolgreich erstellt. Um die Registrierung abzuschließen, bestätigen Sie bitte Ihre E-Mail-Adresse.',
|
||||
|
||||
'email_sent_title' => 'Bestätigungs-E-Mail wurde versendet',
|
||||
'email_sent_description' => 'Wir haben eine E-Mail mit einem Bestätigungslink an :email gesendet. Bitte überprüfen Sie Ihr Postfach.',
|
||||
|
||||
'next_steps_title' => 'So geht es weiter:',
|
||||
|
||||
'step1_title' => 'E-Mail-Postfach öffnen',
|
||||
'step1_description' => 'Öffnen Sie Ihr E-Mail-Postfach und suchen Sie nach unserer Bestätigungsmail.',
|
||||
|
||||
'step2_title' => 'Bestätigungslink klicken',
|
||||
'step2_description' => 'Klicken Sie auf den Bestätigungslink in der E-Mail, um Ihre E-Mail-Adresse zu verifizieren.',
|
||||
|
||||
'step3_title' => 'Anmelden und Setup abschließen',
|
||||
'step3_description' => 'Nach der Bestätigung können Sie sich anmelden und Ihr Profil im Setup-Wizard vervollständigen.',
|
||||
|
||||
'spam_check_title' => 'Wichtiger Hinweis',
|
||||
'spam_check_description' => 'Falls Sie keine E-Mail erhalten haben, überprüfen Sie bitte auch Ihren Spam-Ordner.',
|
||||
|
||||
'already_verified' => 'E-Mail bereits bestätigt?',
|
||||
'login_button' => 'Zum Login',
|
||||
],
|
||||
];
|
||||
10
resources/lang/de/roles.php
Normal file
10
resources/lang/de/roles.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'broker' => 'Makler',
|
||||
'retailer' => 'Händler',
|
||||
'manufacturer' => 'Hersteller',
|
||||
'customer' => 'Kunde',
|
||||
'Sprache' => 'Sprache',
|
||||
'role' => 'Rolle',
|
||||
];
|
||||
85
resources/lang/en/registration.php
Normal file
85
resources/lang/en/registration.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'roles' => [
|
||||
'customer' => [
|
||||
'label' => 'Customer',
|
||||
'description' => 'Access as a customer with your personal customer number.',
|
||||
],
|
||||
'broker' => [
|
||||
'label' => 'Estate Agent',
|
||||
'description' => 'Registration for estate agents with their own agent number.',
|
||||
],
|
||||
'manufacturer' => [
|
||||
'label' => 'Manufacturer',
|
||||
'description' => 'Access for manufacturers to manage products.',
|
||||
],
|
||||
'retailer' => [
|
||||
'label' => 'Retailer',
|
||||
'description' => 'Access for retailers to manage inventory and customers.',
|
||||
],
|
||||
],
|
||||
|
||||
'steps' => [
|
||||
'code_entry' => [
|
||||
'title' => 'Enter Code',
|
||||
'description' => 'Scan the QR code, enter your registration code (e.g. M00100001 or K01102513) and verify it.',
|
||||
],
|
||||
'create_account' => [
|
||||
'title' => 'Create Account',
|
||||
'description' => 'After successful verification, create your account. Role: :role – this ensures your access is correctly assigned.',
|
||||
],
|
||||
'complete_onboarding' => [
|
||||
'title' => 'Complete Onboarding',
|
||||
'description' => 'You will be guided directly to the appropriate setup process and complete your profile in just a few minutes.',
|
||||
],
|
||||
],
|
||||
|
||||
'messages' => [
|
||||
'code_unique' => 'Each code is unique and can only be redeemed once.',
|
||||
'code_format' => 'Format: Letter + 4×2 digits, e.g. M 99 11 22 44',
|
||||
'code_invalid' => 'This registration code is invalid or has already been used.',
|
||||
'code_accepted' => 'Code accepted. Please register now.',
|
||||
'code_problems' => 'Problems with the code? Contact your representative or support.',
|
||||
],
|
||||
|
||||
'titles' => [
|
||||
'registration' => 'Registration',
|
||||
'access_for_role' => 'Access for :role',
|
||||
'enter_code' => 'Enter Your Personal Code',
|
||||
'how_it_works' => 'How Your Registration Works',
|
||||
'how_it_works_description' => 'Three steps to access – with your personal registration code and the appropriate onboarding for :role.',
|
||||
],
|
||||
|
||||
'actions' => [
|
||||
'check_code' => 'Check Code & Continue',
|
||||
'how_it_works' => 'how it works',
|
||||
'start_now' => 'Start Now',
|
||||
],
|
||||
|
||||
'thank_you' => [
|
||||
'subtitle' => 'Registration Successful',
|
||||
'title' => 'Thank You for Your Registration!',
|
||||
'description' => 'Your account has been successfully created. To complete the registration, please verify your email address.',
|
||||
|
||||
'email_sent_title' => 'Verification Email Sent',
|
||||
'email_sent_description' => 'We have sent an email with a verification link to :email. Please check your inbox.',
|
||||
|
||||
'next_steps_title' => 'Next Steps:',
|
||||
|
||||
'step1_title' => 'Open Your Email',
|
||||
'step1_description' => 'Open your email inbox and look for our verification email.',
|
||||
|
||||
'step2_title' => 'Click Verification Link',
|
||||
'step2_description' => 'Click on the verification link in the email to verify your email address.',
|
||||
|
||||
'step3_title' => 'Login and Complete Setup',
|
||||
'step3_description' => 'After verification, you can log in and complete your profile in the setup wizard.',
|
||||
|
||||
'spam_check_title' => 'Important Note',
|
||||
'spam_check_description' => 'If you did not receive an email, please check your spam folder.',
|
||||
|
||||
'already_verified' => 'Email already verified?',
|
||||
'login_button' => 'Go to Login',
|
||||
],
|
||||
];
|
||||
|
|
@ -1,57 +1,268 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Partner;
|
||||
use App\Models\User;
|
||||
use App\Models\RegistrationCode;
|
||||
use App\Helpers\ThemeHelper;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public $userRole = '';
|
||||
public $roleIcon = 'shield-check';
|
||||
public $roleName = '';
|
||||
|
||||
// Admin KPIs
|
||||
public $activeHubs = 0;
|
||||
public $plannedHubs = 0;
|
||||
public $totalPartners = 0;
|
||||
public $partnersThisMonth = 0;
|
||||
public $platformRevenue = 0;
|
||||
public $systemStatus = 'green';
|
||||
public $pendingInvitations = 0;
|
||||
public $totalCustomers = 0;
|
||||
public array $data = [];
|
||||
|
||||
// Retailer KPIs
|
||||
public $openOrders = 0;
|
||||
public $monthlyRevenue = 0;
|
||||
public $productViews = 0;
|
||||
public $stockWarnings = 0;
|
||||
public $myCustomers = 0;
|
||||
|
||||
// Manufacturer KPIs
|
||||
public $brandReach = 0;
|
||||
public $activeProducts = 0;
|
||||
public $draftProducts = 0;
|
||||
public $totalViews = 0;
|
||||
|
||||
// Broker KPIs
|
||||
public $totalCommission = 0;
|
||||
public $pendingPayout = 0;
|
||||
public $generatedLeads = 0;
|
||||
public $referralLink = '';
|
||||
public $brokerCustomers = 0;
|
||||
|
||||
// Customer Brand Data
|
||||
public $customerBrand = 'b2in';
|
||||
public $customerBrandName = 'B2IN';
|
||||
public $customerBrandLogo = '';
|
||||
public $customerBrandColors = [];
|
||||
public $customerBrokerName = '';
|
||||
public $topOffers = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
$role = $user->roles->first();
|
||||
|
||||
if ($role) {
|
||||
$this->userRole = strtolower(str_replace('-', '', $role->name));
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->roleName = $role->display_name ?? $role->name;
|
||||
}
|
||||
|
||||
// Lade rollenspezifische Daten
|
||||
match ($this->userRole) {
|
||||
'admin', 'superadmin' => $this->loadAdminData(),
|
||||
'retailer' => $this->loadRetailerData($user),
|
||||
'manufacturer' => $this->loadManufacturerData($user),
|
||||
'broker', 'estateagent' => $this->loadBrokerData($user),
|
||||
'customer' => $this->loadCustomerData($user),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function loadAdminData(): void
|
||||
{
|
||||
// Platzhalter: Aktive Hubs (später aus Hub-Tabelle)
|
||||
$this->activeHubs = 3;
|
||||
$this->plannedHubs = 2;
|
||||
|
||||
// Partner-Wachstum
|
||||
$this->totalPartners = Partner::whereIn('type', ['Retailer', 'Manufacturer', 'Broker', 'Estate-Agent'])->count();
|
||||
$this->partnersThisMonth = Partner::whereIn('type', ['Retailer', 'Manufacturer', 'Broker', 'Estate-Agent'])
|
||||
->whereMonth('created_at', now()->month)
|
||||
->count();
|
||||
|
||||
// Platzhalter: Plattform-Umsatz
|
||||
$this->platformRevenue = 125000; // Später aus Order-Tabelle
|
||||
|
||||
// System-Status (Platzhalter)
|
||||
$this->systemStatus = 'green';
|
||||
|
||||
// Onboarding-Pipeline: Codes ohne registrierte User
|
||||
$this->pendingInvitations = RegistrationCode::whereNull('used_at')->count();
|
||||
|
||||
// Kunden gesamt
|
||||
$this->totalCustomers = Partner::where('type', 'Customer')->count();
|
||||
|
||||
$this->data = [
|
||||
['date' => '2025-12-11', 'visitors' => 50],
|
||||
['date' => '2025-12-12', 'visitors' => 70],
|
||||
['date' => '2025-12-13', 'visitors' => 100],
|
||||
['date' => '2025-12-14', 'visitors' => 210],
|
||||
['date' => '2025-12-15', 'visitors' => 198],
|
||||
['date' => '2025-12-16', 'visitors' => 269],
|
||||
['date' => '2025-12-17', 'visitors' => 259],
|
||||
['date' => '2025-12-18', 'visitors' => 267],
|
||||
];
|
||||
}
|
||||
|
||||
private function loadRetailerData(User $user): void
|
||||
{
|
||||
// Platzhalter: Offene Bestellungen
|
||||
$this->openOrders = 5; // Später aus Order-Tabelle
|
||||
|
||||
// Platzhalter: Umsatz diesen Monat
|
||||
$this->monthlyRevenue = 15500; // Später aus Order-Tabelle
|
||||
|
||||
// Platzhalter: Produkt-Views
|
||||
$this->productViews = 1250; // Später aus Analytics
|
||||
|
||||
// Platzhalter: Lager-Warnungen
|
||||
$this->stockWarnings = 3; // Später aus Product-Tabelle (stock < min_stock)
|
||||
|
||||
// Meine Kunden (Kunden die diesem Händler zugeordnet sind)
|
||||
if ($user->partner_id) {
|
||||
$this->myCustomers = Partner::where('type', 'Customer')
|
||||
->where('parent_partner_id', $user->partner_id)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
private function loadManufacturerData(User $user): void
|
||||
{
|
||||
// Platzhalter: Marken-Reichweite (Händler die meine Produkte führen)
|
||||
$this->brandReach = 12; // Später aus Produkt-Zuordnungen
|
||||
|
||||
// Platzhalter: Katalog-Status
|
||||
$this->activeProducts = 45; // Später aus Product-Tabelle (is_active = true)
|
||||
$this->draftProducts = 7; // Später aus Product-Tabelle (is_active = false)
|
||||
|
||||
// Platzhalter: Gesamt-Views
|
||||
$this->totalViews = 8900; // Später aus Analytics
|
||||
}
|
||||
|
||||
private function loadBrokerData(User $user): void
|
||||
{
|
||||
// Platzhalter: Verdiente Provision
|
||||
$this->totalCommission = 4250.50; // Später aus Commission-Tabelle
|
||||
|
||||
// Platzhalter: Offene Auszahlung
|
||||
$this->pendingPayout = 850.00; // Später aus Payout-Tabelle
|
||||
|
||||
// Generierte Leads (Kunden über diesen Makler)
|
||||
if ($user->partner_id) {
|
||||
$this->generatedLeads = Partner::where('type', 'Customer')
|
||||
->where('parent_partner_id', $user->partner_id)
|
||||
->count();
|
||||
|
||||
$this->brokerCustomers = $this->generatedLeads;
|
||||
}
|
||||
|
||||
// Empfehlungs-Link (Platzhalter)
|
||||
$partner = Partner::find($user->partner_id);
|
||||
if ($partner) {
|
||||
$this->referralLink = url('/register?ref=' . $partner->partner_number);
|
||||
}
|
||||
}
|
||||
|
||||
private function loadCustomerData(User $user): void
|
||||
{
|
||||
// Kunde: Lade Brand-Daten vom Partner
|
||||
if ($user->partner_id) {
|
||||
$partner = Partner::find($user->partner_id);
|
||||
|
||||
if ($partner) {
|
||||
// Brand aus Partner-Datensatz
|
||||
$this->customerBrand = $partner->brand ?? 'b2in';
|
||||
$this->customerBrandName = ThemeHelper::getBrandName($this->customerBrand);
|
||||
$this->customerBrandLogo = ThemeHelper::getLogoPathForBrand($this->customerBrand, 'positive');
|
||||
$this->customerBrandColors = ThemeHelper::getBrandColors($this->customerBrand);
|
||||
|
||||
// Makler/Händler-Namen laden, falls vorhanden
|
||||
if ($partner->parent_partner_id) {
|
||||
$parentPartner = Partner::find($partner->parent_partner_id);
|
||||
if ($parentPartner) {
|
||||
$this->customerBrokerName = $parentPartner->display_name ?? $parentPartner->company_name ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// Top-Angebote laden (Platzhalter - später aus Product-Tabelle)
|
||||
$this->topOffers = [
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => 'Designer Sofa "Luna"',
|
||||
'description' => 'Modernes 3-Sitzer Sofa mit Samtbezug',
|
||||
'price' => 1899.00,
|
||||
'original_price' => 2499.00,
|
||||
'discount' => 24,
|
||||
'image' => 'https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=400&h=300&fit=crop',
|
||||
'category' => 'Wohnzimmer',
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => 'Esstisch "Nordic"',
|
||||
'description' => 'Massivholz Esstisch für 6-8 Personen',
|
||||
'price' => 899.00,
|
||||
'original_price' => 1299.00,
|
||||
'discount' => 31,
|
||||
'image' => 'https://images.unsplash.com/photo-1617806118233-18e1de247200?w=400&h=300&fit=crop',
|
||||
'category' => 'Esszimmer',
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => 'Boxspringbett "Royal"',
|
||||
'description' => 'Premium Boxspringbett 180x200 cm',
|
||||
'price' => 1599.00,
|
||||
'original_price' => 2199.00,
|
||||
'discount' => 27,
|
||||
'image' => 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=400&h=300&fit=crop',
|
||||
'category' => 'Schlafzimmer',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<x-layouts.app title="Dashboard">
|
||||
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700 p-4">
|
||||
<livewire:notifications />
|
||||
@volt('dashboard')
|
||||
<div class="space-y-6">
|
||||
{{-- Header mit Rollenbadge --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Dashboard') }} (Beispiel)</flux:heading>
|
||||
<flux:subheading>{{ __('Willkommen zurück') }}, {{ Auth::user()->name }}!</flux:subheading>
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>Customer</flux:table.column>
|
||||
<flux:table.column>Date</flux:table.column>
|
||||
<flux:table.column>Status</flux:table.column>
|
||||
<flux:table.column>Amount</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Lindsey Aminoff</flux:table.cell>
|
||||
<flux:table.cell>Jul 29, 10:45 AM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$49.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Hanna Lubin</flux:table.cell>
|
||||
<flux:table.cell>Jul 28, 2:15 PM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$312.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Kianna Bushevi</flux:table.cell>
|
||||
<flux:table.cell>Jul 30, 4:05 PM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="zinc" size="sm" inset="top bottom">Refunded</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$132.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
|
||||
<flux:table.row>
|
||||
<flux:table.cell>Gustavo Geidt</flux:table.cell>
|
||||
<flux:table.cell>Jul 27, 9:30 AM</flux:table.cell>
|
||||
<flux:table.cell><flux:badge color="green" size="sm" inset="top bottom">Paid</flux:badge></flux:table.cell>
|
||||
<flux:table.cell variant="strong">$31.00</flux:table.cell>
|
||||
</flux:table.row>
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
<div class="flex items-center gap-2 px-3 py-2 bg-accent-50 dark:bg-accent-900/20 rounded-lg">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5 text-accent-600 dark:text-accent-400')
|
||||
<span class="text-sm font-medium text-accent-700 dark:text-accent-300">
|
||||
{{ $roleName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative h-full flex-1 overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
|
||||
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
|
||||
|
||||
{{-- Rollenspezifisches Dashboard laden --}}
|
||||
@if(in_array($userRole, ['admin', 'superadmin']))
|
||||
@include('admin.dashboards.admin')
|
||||
@elseif($userRole === 'retailer')
|
||||
@include('admin.dashboards.retailer')
|
||||
@elseif($userRole === 'manufacturer')
|
||||
@include('admin.dashboards.manufacturer')
|
||||
@elseif(in_array($userRole, ['broker', 'estateagent']))
|
||||
@include('admin.dashboards.broker')
|
||||
@elseif($userRole === 'customer')
|
||||
@include('admin.dashboards.customer')
|
||||
@else
|
||||
<flux:card>
|
||||
<div class="text-center py-8">
|
||||
<div class="text-zinc-500">{{ __('Dashboard für Ihre Rolle wird noch entwickelt.') }}</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
</div>
|
||||
@endvolt
|
||||
</x-layouts.app>
|
||||
|
||||
|
||||
|
|
|
|||
148
resources/views/admin/dashboards/admin.blade.php
Normal file
148
resources/views/admin/dashboards/admin.blade.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<div class="space-y-6">
|
||||
{{-- KPI-Karten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{{-- Aktive Hubs --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 font-medium">{{ __('Aktive Hubs') }}</div>
|
||||
<div class="text-3xl font-bold text-blue-900 dark:text-blue-100 mt-2">{{ $activeHubs }}</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-400 mt-1">{{ $plannedHubs }} {{ __('geplant') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-map-pin', 'w-10 h-10 text-blue-400 dark:text-blue-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Partner-Wachstum --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-green-700 dark:text-green-300 font-medium">{{ __('Partner gesamt') }}</div>
|
||||
<div class="text-3xl font-bold text-green-900 dark:text-green-100 mt-2">{{ $totalPartners }}</div>
|
||||
<div class="text-xs text-green-600 dark:text-green-400 mt-1">+{{ $partnersThisMonth }} {{ __('diesen Monat') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-user-group', 'w-10 h-10 text-green-400 dark:text-green-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Plattform-Umsatz --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-purple-700 dark:text-purple-400 font-medium">{{ __('Plattform-Umsatz') }}</div>
|
||||
<div class="text-3xl font-bold text-purple-900 dark:text-purple-100 mt-2">{{ number_format($platformRevenue, 0, ',', '.') }} €</div>
|
||||
<div class="text-xs text-purple-600 dark:text-purple-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-currency-euro', 'w-10 h-10 text-purple-400 dark:text-purple-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- System-Status --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-emerald-700 dark:text-emerald-300 font-medium">{{ __('System-Status') }}</div>
|
||||
<div class="text-3xl font-bold text-emerald-900 dark:text-emerald-100 mt-2">{{ __('Optimal') }}</div>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||||
<div class="text-xs text-emerald-600 dark:text-emerald-400">{{ __('Alle Systeme laufen') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@svg('heroicon-o-server', 'w-10 h-10 text-emerald-400 dark:text-emerald-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Widgets --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Onboarding-Pipeline --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Onboarding-Pipeline') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Offene Einladungen') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center py-8 bg-orange-50 dark:bg-zinc-800 rounded-lg">
|
||||
<div class="text-center">
|
||||
<div class="text-5xl font-bold text-sky-600 dark:text-sky-400">{{ $pendingInvitations }}</div>
|
||||
<div class="text-sm text-sky-700 dark:text-sky-300 mt-2">{{ __('Einladungen ohne Registrierung') }}</div>
|
||||
<flux:button href="{{ route('admin.partners.registration-codes') }}" variant="primary" size="sm" class="mt-4">
|
||||
{{ __('Einladungen verwalten') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Partner-Übersicht --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Partner & Kunden') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Übersicht der Plattform-Nutzer') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-user', 'w-6 h-6 text-blue-500')
|
||||
<span class="font-medium">{{ __('Kunden') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold">{{ $totalCustomers }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-building-storefront', 'w-6 h-6 text-green-500')
|
||||
<span class="font-medium">{{ __('Händler') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold">{{ \App\Models\Partner::where('type', 'Retailer')->count() }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-building-office', 'w-6 h-6 text-purple-500')
|
||||
<span class="font-medium">{{ __('Hersteller') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold">{{ \App\Models\Partner::where('type', 'Manufacturer')->count() }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-briefcase', 'w-6 h-6 text-orange-500')
|
||||
<span class="font-medium">{{ __('Makler') }}</span>
|
||||
</div>
|
||||
<span class="text-2xl font-bold">{{ \App\Models\Partner::whereIn('type', ['Broker', 'Estate-Agent'])->count() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
{{-- Besucher-Statistik Chart --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Besucher-Statistik') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Tägliche Besucherzahlen der letzten Tage') }}</flux:subheading>
|
||||
|
||||
<div class="mt-6">
|
||||
@if(!empty($data))
|
||||
<flux:chart :value="$data" class="aspect-[3/1]">
|
||||
<flux:chart.svg>
|
||||
<flux:chart.line field="visitors" class="text-pink-500 dark:text-pink-400" />
|
||||
|
||||
<flux:chart.axis axis="x" field="date">
|
||||
<flux:chart.axis.line />
|
||||
<flux:chart.axis.tick />
|
||||
</flux:chart.axis>
|
||||
|
||||
<flux:chart.axis axis="y">
|
||||
<flux:chart.axis.grid />
|
||||
<flux:chart.axis.tick />
|
||||
</flux:chart.axis>
|
||||
|
||||
<flux:chart.cursor />
|
||||
</flux:chart.svg>
|
||||
|
||||
<flux:chart.tooltip>
|
||||
<flux:chart.tooltip.heading field="date" :format="['year' => 'numeric', 'month' => 'numeric', 'day' => 'numeric']" />
|
||||
<flux:chart.tooltip.value field="visitors" label="Besucher" />
|
||||
</flux:chart.tooltip>
|
||||
</flux:chart>
|
||||
@else
|
||||
<div class="text-center py-8 text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Keine Daten verfügbar') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
104
resources/views/admin/dashboards/broker.blade.php
Normal file
104
resources/views/admin/dashboards/broker.blade.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<div class="space-y-6">
|
||||
{{-- KPI-Karten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{{-- Verdiente Provision --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-green-700 dark:text-green-300 font-medium">{{ __('Verdiente Provision') }}</div>
|
||||
<div class="text-3xl font-bold text-green-900 dark:text-green-100 mt-2">{{ number_format($totalCommission, 2, ',', '.') }} €</div>
|
||||
<div class="text-xs text-green-600 dark:text-green-400 mt-1">{{ __('Lifetime Earnings (Platzhalter)') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-currency-euro', 'w-10 h-10 text-green-400 dark:text-green-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Offene Auszahlung --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 font-medium">{{ __('Offene Auszahlung') }}</div>
|
||||
<div class="text-3xl font-bold text-blue-900 dark:text-blue-100 mt-2">{{ number_format($pendingPayout, 2, ',', '.') }} €</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-banknotes', 'w-10 h-10 text-blue-400 dark:text-blue-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Generierte Leads --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-purple-700 dark:text-purple-300 font-medium">{{ __('Generierte Leads') }}</div>
|
||||
<div class="text-3xl font-bold text-purple-900 dark:text-purple-100 mt-2">{{ $generatedLeads }}</div>
|
||||
<div class="text-xs text-purple-600 dark:text-purple-400 mt-1">{{ __('Registrierte Kunden') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-user-group', 'w-10 h-10 text-purple-400 dark:text-purple-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Widgets --}}
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
{{-- Empfehlungs-Link --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Mein Empfehlungs-Link') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Teilen Sie diesen Link mit Ihren Kunden') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border-2 border-dashed border-blue-300 dark:border-blue-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 bg-white dark:bg-zinc-800 px-4 py-3 rounded font-mono text-sm break-all">
|
||||
{{ $referralLink }}
|
||||
</div>
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="clipboard"
|
||||
x-data
|
||||
@click="navigator.clipboard.writeText('{{ $referralLink }}'); $tooltip('{{ __('Link kopiert!') }}', { timeout: 2000 })"
|
||||
>
|
||||
{{ __('Kopieren') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<flux:button variant="ghost" size="sm" icon="chat-bubble-left">{{ __('Per WhatsApp teilen') }}</flux:button>
|
||||
<flux:button variant="ghost" size="sm" icon="envelope">{{ __('Per E-Mail teilen') }}</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Meine Kunden --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Meine Kunden') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Über Sie registrierte Kunden') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center py-8 bg-purple-50 dark:bg-purple-900/10 rounded-lg">
|
||||
<div class="text-center">
|
||||
<div class="text-5xl font-bold text-purple-600 dark:text-purple-400">{{ $brokerCustomers }}</div>
|
||||
<div class="text-sm text-purple-700 dark:text-purple-300 mt-2">{{ __('Registrierte Kunden') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Letzte Aktivitäten --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Letzte Aktivitäten') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Was ist neu?') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<div class="p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg text-sm">
|
||||
<div class="font-medium">{{ __('Kunde Max M. hat sich registriert') }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">{{ __('vor 2 Stunden (Platzhalter)') }}</div>
|
||||
</div>
|
||||
<div class="p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-lg text-sm">
|
||||
<div class="font-medium">{{ __('Provision gutgeschrieben: 125,00 €') }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400 mt-1">{{ __('gestern (Platzhalter)') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
184
resources/views/admin/dashboards/customer.blade.php
Normal file
184
resources/views/admin/dashboards/customer.blade.php
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Marken-Header mit Logo --}}
|
||||
<flux:card>
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-6 p-6 rounded-lg"
|
||||
style="background: linear-gradient(135deg, {{ $customerBrandColors['primary'] ?? '#2b3f51' }}15 0%, {{ $customerBrandColors['secondary'] ?? '#20a0da' }}15 100%);">
|
||||
<div class="flex-1 text-center md:text-left">
|
||||
<div class="mb-4">
|
||||
<img src="{{ asset($customerBrandLogo) }}" alt="{{ $customerBrandName }}" class="h-12 mx-auto md:mx-0 dark:hidden">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPathForBrand($customerBrand, 'negative')) }}" alt="{{ $customerBrandName }}" class="h-12 mx-auto md:mx-0 hidden dark:block">
|
||||
</div>
|
||||
<flux:heading size="xl">{{ __('Willkommen bei') }} {{ $customerBrandName }}!</flux:heading>
|
||||
<flux:subheading class="mt-2">{{ __('Ihr persönliches Kundenportal') }}</flux:subheading>
|
||||
@if($customerBrokerName)
|
||||
<div class="mt-3 flex items-center gap-2 justify-center md:justify-start">
|
||||
@svg('heroicon-o-user-circle', 'w-5 h-5 text-zinc-500 dark:text-zinc-400')
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Ihr Ansprechpartner:') }} <strong>{{ $customerBrokerName }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-32 h-32 rounded-full flex items-center justify-center"
|
||||
style="background-color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }}20;">
|
||||
@svg('heroicon-o-home', 'w-16 h-16', ['style' => 'color: ' . ($customerBrandColors['primary'] ?? '#2b3f51')])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Info-Box --}}
|
||||
<flux:card>
|
||||
<div class="p-4 rounded-lg" style="background-color: {{ $customerBrandColors['secondary'] ?? '#20a0da' }}10;">
|
||||
<div class="flex items-start gap-3">
|
||||
@svg('heroicon-o-information-circle', 'w-6 h-6 flex-shrink-0', ['style' => 'color: ' . ($customerBrandColors['secondary'] ?? '#20a0da')])
|
||||
<div>
|
||||
<div class="font-medium" style="color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }};">
|
||||
{{ __('Ihr Dashboard wird bald erweitert') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-700 dark:text-zinc-300 mt-1">
|
||||
{{ __('Hier sehen Sie bald Ihre Bestellungen, Wunschliste und Empfehlungen.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Top-Angebote --}}
|
||||
@if(!empty($topOffers))
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Top-Angebote für Sie') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Exklusive Möbel zu besonderen Preisen') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button variant="ghost" size="sm" icon="arrow-right">
|
||||
{{ __('Alle Angebote') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
|
||||
@foreach($topOffers as $offer)
|
||||
<div class="group relative overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700 hover:shadow-lg transition-shadow duration-300">
|
||||
{{-- Produktbild --}}
|
||||
<div class="relative aspect-[4/3] overflow-hidden bg-zinc-100 dark:bg-zinc-800">
|
||||
<img src="{{ $offer['image'] }}"
|
||||
alt="{{ $offer['name'] }}"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
|
||||
|
||||
{{-- Rabatt-Badge --}}
|
||||
@if($offer['discount'] > 0)
|
||||
<div class="absolute top-3 right-3 px-2 py-1 rounded-full text-xs font-bold text-white"
|
||||
style="background-color: {{ $customerBrandColors['secondary'] ?? '#20a0da' }};">
|
||||
-{{ $offer['discount'] }}%
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kategorie-Badge --}}
|
||||
<div class="absolute top-3 left-3 px-2 py-1 rounded-full text-xs font-medium bg-white/90 dark:bg-zinc-900/90 text-zinc-700 dark:text-zinc-300">
|
||||
{{ $offer['category'] }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Produktinfo --}}
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold text-lg mb-2 text-zinc-900 dark:text-zinc-100">
|
||||
{{ $offer['name'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-3">
|
||||
{{ $offer['description'] }}
|
||||
</p>
|
||||
|
||||
{{-- Preis --}}
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-2xl font-bold" style="color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }};">
|
||||
{{ number_format($offer['price'], 2, ',', '.') }} €
|
||||
</span>
|
||||
@if($offer['original_price'] > $offer['price'])
|
||||
<span class="text-sm text-zinc-500 dark:text-zinc-400 line-through">
|
||||
{{ number_format($offer['original_price'], 2, ',', '.') }} €
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Aktions-Buttons --}}
|
||||
<div class="flex gap-2">
|
||||
<flux:button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon="eye"
|
||||
class="flex-1"
|
||||
style="background-color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }}; border-color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }};">
|
||||
{{ __('Details') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="heart"
|
||||
style="color: {{ $customerBrandColors['secondary'] ?? '#20a0da' }};">
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Feature-Vorschau --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:card>
|
||||
<div class="p-4 text-center">
|
||||
<div class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center"
|
||||
style="background-color: {{ $customerBrandColors['primary'] ?? '#2b3f51' }}20;">
|
||||
@svg('heroicon-o-shopping-bag', 'w-8 h-8', ['style' => 'color: ' . ($customerBrandColors['primary'] ?? '#2b3f51')])
|
||||
</div>
|
||||
<div class="font-medium text-lg">{{ __('Meine Bestellungen') }}</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ __('Bald verfügbar') }}</div>
|
||||
<div class="mt-3">
|
||||
<flux:badge color="zinc" size="sm">{{ __('In Entwicklung') }}</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<div class="p-4 text-center">
|
||||
<div class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center"
|
||||
style="background-color: {{ $customerBrandColors['secondary'] ?? '#20a0da' }}20;">
|
||||
@svg('heroicon-o-heart', 'w-8 h-8', ['style' => 'color: ' . ($customerBrandColors['secondary'] ?? '#20a0da')])
|
||||
</div>
|
||||
<div class="font-medium text-lg">{{ __('Meine Wunschliste') }}</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ __('Bald verfügbar') }}</div>
|
||||
<div class="mt-3">
|
||||
<flux:badge color="zinc" size="sm">{{ __('In Entwicklung') }}</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card>
|
||||
<div class="p-4 text-center">
|
||||
<div class="w-16 h-16 rounded-full mx-auto mb-3 flex items-center justify-center"
|
||||
style="background-color: {{ $customerBrandColors['accent'] ?? ($customerBrandColors['secondary'] ?? '#20a0da') }}20;">
|
||||
@svg('heroicon-o-star', 'w-8 h-8', ['style' => 'color: ' . ($customerBrandColors['accent'] ?? ($customerBrandColors['secondary'] ?? '#20a0da'))])
|
||||
</div>
|
||||
<div class="font-medium text-lg">{{ __('Empfehlungen') }}</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400 mt-1">{{ __('Bald verfügbar') }}</div>
|
||||
<div class="mt-3">
|
||||
<flux:badge color="zinc" size="sm">{{ __('In Entwicklung') }}</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Marken-Info Footer --}}
|
||||
<flux:card>
|
||||
<div class="text-center py-6">
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Powered by') }}
|
||||
</div>
|
||||
<img src="{{ asset($customerBrandLogo) }}" alt="{{ $customerBrandName }}" class="h-8 mx-auto mt-2 opacity-60 dark:hidden">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPathForBrand($customerBrand, 'negative')) }}" alt="{{ $customerBrandName }}" class="h-8 mx-auto mt-2 opacity-60 hidden dark:block">
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
95
resources/views/admin/dashboards/manufacturer.blade.php
Normal file
95
resources/views/admin/dashboards/manufacturer.blade.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<div class="space-y-6">
|
||||
{{-- KPI-Karten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{{-- Marken-Reichweite --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-purple-700 dark:text-purple-300 font-medium">{{ __('Marken-Reichweite') }}</div>
|
||||
<div class="text-3xl font-bold text-purple-900 dark:text-purple-100 mt-2">{{ $brandReach }}</div>
|
||||
<div class="text-xs text-purple-600 dark:text-purple-400 mt-1">{{ __('Händler führen meine Produkte') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-globe-alt', 'w-10 h-10 text-purple-400 dark:text-purple-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Katalog-Status --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 font-medium">{{ __('Katalog-Status') }}</div>
|
||||
<div class="text-3xl font-bold text-blue-900 dark:text-blue-100 mt-2">{{ $activeProducts }}</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-400 mt-1">{{ $draftProducts }} {{ __('Entwürfe') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-cube', 'w-10 h-10 text-blue-400 dark:text-blue-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Gesamt-Views --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-green-700 dark:text-green-300 font-medium">{{ __('Gesamt-Views') }}</div>
|
||||
<div class="text-3xl font-bold text-green-900 dark:text-green-100 mt-2">{{ number_format($totalViews, 0, ',', '.') }}</div>
|
||||
<div class="text-xs text-green-600 dark:text-green-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-eye', 'w-10 h-10 text-green-400 dark:text-green-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Widgets --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Katalog-Pflege --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Katalog-Pflege') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie Ihre Produkte') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="flex items-center justify-between p-3 bg-green-50 dark:bg-green-900/10 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-check-circle', 'w-5 h-5 text-green-500')
|
||||
<span>{{ __('Aktive Produkte') }}</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg">{{ $activeProducts }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-orange-50 dark:bg-orange-900/10 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
@svg('heroicon-o-pencil', 'w-5 h-5 text-orange-500')
|
||||
<span>{{ __('Entwürfe') }}</span>
|
||||
</div>
|
||||
<span class="font-bold text-lg">{{ $draftProducts }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<flux:button variant="primary" size="sm" icon="plus">{{ __('Neues Produkt (Master)') }}</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Schnellzugriff --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Schnellzugriff') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Häufig benötigte Funktionen') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<flux:button variant="ghost" icon="photo" class="w-full justify-start">
|
||||
{{ __('Marketing-Material hochladen') }}
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="chart-bar" class="w-full justify-start">
|
||||
{{ __('Händler-Performance') }} ({{ __('Platzhalter') }})
|
||||
</flux:button>
|
||||
<flux:button variant="ghost" icon="tag" class="w-full justify-start">
|
||||
{{ __('Meine Marke bearbeiten') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
95
resources/views/admin/dashboards/retailer.blade.php
Normal file
95
resources/views/admin/dashboards/retailer.blade.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<div class="space-y-6">
|
||||
{{-- KPI-Karten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{{-- Offene Bestellungen --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-red-700 dark:text-red-300 font-medium">{{ __('Offene Bestellungen') }}</div>
|
||||
<div class="text-3xl font-bold text-red-900 dark:text-red-100 mt-2">{{ $openOrders }}</div>
|
||||
<div class="text-xs text-red-600 dark:text-red-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-shopping-cart', 'w-10 h-10 text-red-400 dark:text-red-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Umsatz --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-green-700 dark:text-green-300 font-medium">{{ __('Umsatz diesen Monat') }}</div>
|
||||
<div class="text-3xl font-bold text-green-900 dark:text-green-100 mt-2">{{ number_format($monthlyRevenue, 0, ',', '.') }} €</div>
|
||||
<div class="text-xs text-green-600 dark:text-green-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-currency-euro', 'w-10 h-10 text-green-400 dark:text-green-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Produkt-Views --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 font-medium">{{ __('Produkt-Views') }}</div>
|
||||
<div class="text-3xl font-bold text-blue-900 dark:text-blue-100 mt-2">{{ number_format($productViews, 0, ',', '.') }}</div>
|
||||
<div class="text-xs text-blue-600 dark:text-blue-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-eye', 'w-10 h-10 text-blue-400 dark:text-blue-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Lager-Warnungen --}}
|
||||
<flux:card>
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<div class="text-sm text-orange-700 dark:text-orange-300 font-medium">{{ __('Lager-Warnungen') }}</div>
|
||||
<div class="text-3xl font-bold text-orange-900 dark:text-orange-100 mt-2">{{ $stockWarnings }}</div>
|
||||
<div class="text-xs text-orange-600 dark:text-orange-400 mt-1">{{ __('Platzhalter') }}</div>
|
||||
</div>
|
||||
@svg('heroicon-o-exclamation-triangle', 'w-10 h-10 text-orange-400 dark:text-orange-600')
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Widgets --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- To-Do Liste --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('To-Do Liste') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Ihre anstehenden Aufgaben') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<div class="flex items-center gap-3 p-3 bg-red-50 dark:bg-red-900/10 rounded-lg">
|
||||
@svg('heroicon-o-shopping-cart', 'w-5 h-5 text-red-500')
|
||||
<span class="font-medium">{{ $openOrders }} {{ __('neue Bestellungen warten') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 bg-blue-50 dark:bg-blue-900/10 rounded-lg">
|
||||
@svg('heroicon-o-chat-bubble-left-right', 'w-5 h-5 text-blue-500')
|
||||
<span class="font-medium">{{ __('2 Kundenfragen offen') }} ({{ __('Platzhalter') }})</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-3 bg-orange-50 dark:bg-orange-900/10 rounded-lg">
|
||||
@svg('heroicon-o-cube', 'w-5 h-5 text-orange-500')
|
||||
<span class="font-medium">{{ $stockWarnings }} {{ __('Lager-Warnungen') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<flux:button variant="primary" size="sm" icon="plus">{{ __('Neues Produkt') }}</flux:button>
|
||||
<flux:button variant="ghost" size="sm" icon="arrow-path">{{ __('Bestände aktualisieren') }}</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Meine Kunden --}}
|
||||
<flux:card>
|
||||
<flux:heading size="lg">{{ __('Meine Kunden') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Kunden in Ihrem Einzugsgebiet') }}</flux:subheading>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center py-8 bg-blue-50 dark:bg-blue-900/10 rounded-lg">
|
||||
<div class="text-center">
|
||||
<div class="text-5xl font-bold text-blue-600 dark:text-blue-400">{{ $myCustomers }}</div>
|
||||
<div class="text-sm text-blue-700 dark:text-blue-300 mt-2">{{ __('zugeordnete Kunden') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
27
resources/views/components/error-alert-static.blade.php
Normal file
27
resources/views/components/error-alert-static.blade.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@props(['title' => null, 'light' => false])
|
||||
|
||||
@php
|
||||
$bg = $light ? 'bg-red-50 border border-red-200' : 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800';
|
||||
$icon = $light ? 'text-red-600' : 'text-red-600 dark:text-red-400';
|
||||
$titleColor = $light ? 'text-red-800' : 'text-red-800 dark:text-red-200';
|
||||
$textColor = $light ? 'text-red-700' : 'text-red-700 dark:text-red-300';
|
||||
@endphp
|
||||
|
||||
@if ($errors->any())
|
||||
<div {{ $attributes->merge(['class' => "rounded-lg p-4 $bg"]) }}>
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:icon.exclamation-circle class="h-5 w-5 {{ $icon }} mt-0.5 flex-shrink-0" />
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold {{ $titleColor }} mb-2">
|
||||
{{ $title ?? __('Bitte korrigieren Sie folgende Fehler:') }}
|
||||
</h3>
|
||||
<ul class="text-sm {{ $textColor }} space-y-1 list-disc list-inside">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
|
|
|
|||
|
|
@ -10,48 +10,114 @@
|
|||
<x-app-logo />
|
||||
</a>
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
@if(session('impersonate_from'))
|
||||
<div class="mb-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 p-3">
|
||||
<div class="flex items-start gap-3 mb-2">
|
||||
<flux:icon.exclamation-triangle class="h-5 w-5 text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-semibold text-orange-900 dark:text-orange-100">
|
||||
{{ __('Als :name eingeloggt', ['name' => Auth::user()->name]) }}
|
||||
</div>
|
||||
<div class="text-xs text-orange-700 dark:text-orange-300 mt-0.5">
|
||||
{{ __('Sie sind temporär als dieser User angemeldet') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.impersonate.leave') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:button type="submit" variant="primary" size="sm" icon="arrow-left-start-on-rectangle" class="w-full">
|
||||
{{ __('Zurück zum Admin') }}
|
||||
</flux:button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:navlist variant="outline">
|
||||
@hasrole('Customer')
|
||||
<flux:navlist.group :heading="__('Customer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="user" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
|
||||
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
@hasrole('Retailer')
|
||||
<flux:navlist.group :heading="__('Retailer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
|
||||
<flux:navlist.item icon="cube" :href="route('products.index')" :current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="plus-circle" :href="route('products.create')" :current="request()->routeIs('products.create')" wire:navigate>{{ __('Neues Produkt') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
|
||||
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
@hasrole('Manufacturer')
|
||||
<flux:navlist.group :heading="__('Manufacturer')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('Estate-Agent')" class="grid mb-4">
|
||||
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
|
||||
<flux:navlist.item icon="cube" :href="route('products.index')" :current="request()->routeIs('products.index')" wire:navigate>{{ __('Produktliste') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="plus-circle" :href="route('products.create')" :current="request()->routeIs('products.create')" wire:navigate>{{ __('Neues Produkt') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
|
||||
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
@hasrole('Broker')
|
||||
<flux:navlist.group :heading="__('Broker')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group :heading="__('Daten')" class="grid mb-4">
|
||||
<flux:navlist.item icon="building-office" :href="route('partner.my-data')" :current="request()->routeIs('partner.my-data')" wire:navigate>{{ __('Meine Daten') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
@hasrole('Admin|Super-Admin')
|
||||
<flux:navlist.group :heading="__('Info')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
|
||||
|
||||
|
||||
@hasrole('Super-Admin|Admin')
|
||||
<flux:navlist.group :heading="__('Admin')" class="grid mb-4">
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
|
||||
|
||||
<flux:navlist.group expandable expanded="false" heading="Users" class="hidden lg:grid">
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Users') }}</flux:navlist.item>
|
||||
<flux:navlist.group expandable :expanded="request()->routeIs(['admin.users', 'admin.users.permissions', 'admin.partners.invite', 'admin.partners.registration-codes'])" heading="Partner" class="grid">
|
||||
<flux:navlist.item icon="user-group" :href="route('admin.users')" :current="request()->routeIs('admin.users')" wire:navigate>{{ __('Benutzer') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')" :current="request()->routeIs('admin.partners.invite')" wire:navigate>{{ __('Partner einladen') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="key" :href="route('admin.partners.registration-codes')" :current="request()->routeIs('admin.partners.registration-codes')" wire:navigate>{{ __('Registrierungscodes') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')" :current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
<flux:navlist.group expandable :expanded="request()->routeIs('testing')" heading="Testing" class="hidden lg:grid mt-2">
|
||||
<flux:navlist.item icon="user-group" :href="route('testing.landing')" :current="request()->routeIs('testing.landing')" wire:navigate>{{ __('Register') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')" :current="request()->routeIs('admin.partners.invite')" wire:navigate>{{ __('Partner einladen') }}</flux:navlist.item>
|
||||
<flux:navlist.group :heading="__('Regionen')" class="grid mb-4">
|
||||
<flux:navlist.item icon="map" :href="route('admin.hubs.index')" :current="request()->routeIs('admin.hubs.*')" wire:navigate>{{ __('Hub-Verwaltung') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
<flux:navlist.group :heading="__('CMS')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
<flux:navlist.item icon="home" :href="route('admin.cms.cabinet')" :current="request()->routeIs('admin.cms.cabinet')" wire:navigate>{{ __('Cabinet') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
|
||||
|
||||
@endhasrole
|
||||
|
||||
@hasrole('Super-Admin')
|
||||
<flux:navlist.group :heading="__('Superadmin')" class="grid mb-4">
|
||||
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
</flux:navlist>
|
||||
|
||||
<flux:spacer />
|
||||
|
||||
@hasrole('Super-Admin|Admin')
|
||||
<flux:navlist.group :heading="__('Dokumentation')" class="grid mb-4">
|
||||
<flux:navlist.item icon="document-text" :href="route('admin.documentation')" :current="request()->routeIs('admin.documentation')" wire:navigate>{{ __('Projekt-Dokumentation') }}</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
@endhasrole
|
||||
@hasrole('Super-Admin')
|
||||
<flux:navlist variant="outline">
|
||||
<flux:navlist.group :heading="__('Resources')">
|
||||
<flux:navlist.item icon="pencil" href="https://tailwindcss.com/docs/installation/using-vite" target="_blank">
|
||||
|
|
@ -72,6 +138,8 @@
|
|||
</flux:navlist.item>
|
||||
</flux:navlist.group>
|
||||
</flux:navlist>
|
||||
@endhasrole
|
||||
|
||||
<!-- Desktop User Menu -->
|
||||
<flux:dropdown position="bottom" align="start">
|
||||
<flux:profile
|
||||
|
|
@ -105,10 +173,35 @@
|
|||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<div x-data="{
|
||||
toggle() {
|
||||
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
}">
|
||||
<flux:menu.item
|
||||
x-show="$flux.appearance !== 'dark'"
|
||||
@click="toggle()"
|
||||
icon="moon"
|
||||
>
|
||||
{{ __('Dunkel') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item
|
||||
x-show="$flux.appearance === 'dark'"
|
||||
x-cloak
|
||||
@click="toggle()"
|
||||
icon="sun"
|
||||
>
|
||||
{{ __('Hell') }}
|
||||
</flux:menu.item>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
|
|
@ -158,10 +251,35 @@
|
|||
<flux:menu.radio.group>
|
||||
<flux:menu.item :href="route('settings.profile')" icon="cog" wire:navigate>{{ __('Settings') }}</flux:menu.item>
|
||||
</flux:menu.radio.group>
|
||||
<flux:menu.separator />
|
||||
|
||||
<flux:menu.radio.group>
|
||||
<div x-data="{
|
||||
toggle() {
|
||||
$flux.appearance = $flux.appearance === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
}">
|
||||
<flux:menu.item
|
||||
x-show="$flux.appearance !== 'dark'"
|
||||
@click="toggle()"
|
||||
icon="moon"
|
||||
>
|
||||
{{ __('Dunkel') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item
|
||||
x-show="$flux.appearance === 'dark'"
|
||||
x-cloak
|
||||
@click="toggle()"
|
||||
icon="sun"
|
||||
>
|
||||
{{ __('Hell') }}
|
||||
</flux:menu.item>
|
||||
</div>
|
||||
</flux:menu.radio.group>
|
||||
|
||||
<flux:menu.separator />
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||
<form method="POST" action="{{ route('auth.logout') }}" class="w-full">
|
||||
@csrf
|
||||
<flux:menu.item as="button" type="submit" icon="arrow-right-start-on-rectangle" class="w-full">
|
||||
{{ __('Log Out') }}
|
||||
|
|
|
|||
15
resources/views/components/success-alert.blade.php
Normal file
15
resources/views/components/success-alert.blade.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
@props(['message' => null])
|
||||
|
||||
@if ($message || session('message'))
|
||||
<div {{ $attributes->merge(['class' => 'rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4']) }}>
|
||||
<div class="flex items-start gap-3">
|
||||
@svg('heroicon-o-check-circle', 'h-5 w-5 text-green-600 dark:text-green-400 mt-0.5 flex-shrink-0')
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
{{ $message ?? session('message') ?? $slot }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
174
resources/views/emails/README.md
Normal file
174
resources/views/emails/README.md
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# E-Mail-Vorlagen
|
||||
|
||||
Diese Anwendung verwendet angepasste E-Mail-Vorlagen mit einem professionellen B2IN-Design.
|
||||
|
||||
## 📧 Verfügbare E-Mail-Vorlagen
|
||||
|
||||
### 1. Partner-Einladung
|
||||
**Datei:** `partner-invitation.blade.php`
|
||||
|
||||
Wird versendet wenn ein Partner eingeladen wird.
|
||||
|
||||
**Variablen:**
|
||||
- `$contactFullName` - Vollständiger Name des Kontakts
|
||||
- `$companyName` - Firmenname
|
||||
- `$partnerType` - Partner-Typ (Rolle)
|
||||
- `$invitationUrl` - Einladungs-URL
|
||||
- `$expiresAt` - Ablaufdatum der Einladung
|
||||
- `$invitation` - Komplettes Invitation-Objekt
|
||||
|
||||
### 2. Passwort zurücksetzen
|
||||
**Klasse:** `App\Notifications\CustomResetPasswordNotification`
|
||||
|
||||
Wird versendet wenn ein Benutzer sein Passwort zurücksetzen möchte.
|
||||
|
||||
**Features:**
|
||||
- Zeitlich begrenzter Reset-Link
|
||||
- Klare Sicherheitshinweise
|
||||
- Responsives Design
|
||||
|
||||
### 3. E-Mail-Verifizierung
|
||||
**Klasse:** `App\Notifications\CustomVerifyEmailNotification`
|
||||
|
||||
Wird nach der Registrierung versendet.
|
||||
|
||||
**Features:**
|
||||
- Zeitlich begrenzter Bestätigungslink (60 Minuten)
|
||||
- Willkommenstext
|
||||
- Klare Call-to-Action
|
||||
|
||||
## 🎨 Design-System
|
||||
|
||||
Das E-Mail-Design verwendet das B2IN-Theme mit folgenden Farben:
|
||||
|
||||
- **Primary (Anthracite):** `#2b3f51`
|
||||
- **Secondary (Dynamic Blue):** `#20a0da`
|
||||
- **Font:** Instrument Sans
|
||||
|
||||
### CSS-Theme
|
||||
Die Styles befinden sich in:
|
||||
```
|
||||
resources/views/vendor/mail/html/themes/b2in.css
|
||||
```
|
||||
|
||||
## 🔧 Anpassungen
|
||||
|
||||
### Neue E-Mail-Vorlage erstellen
|
||||
|
||||
1. Erstelle eine neue Blade-Datei in `resources/views/emails/`
|
||||
2. Verwende die Mail-Komponenten:
|
||||
|
||||
```blade
|
||||
@component('mail::message')
|
||||
# Überschrift
|
||||
|
||||
Hier kommt der Inhalt.
|
||||
|
||||
@component('mail::button', ['url' => $actionUrl, 'color' => 'primary'])
|
||||
Button Text
|
||||
@endcomponent
|
||||
|
||||
@component('mail::panel')
|
||||
Wichtige Information in einem Panel
|
||||
@endcomponent
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
Ihr **{{ config('app.name') }}** Team
|
||||
@endcomponent
|
||||
```
|
||||
|
||||
### Verfügbare Komponenten
|
||||
|
||||
#### Button
|
||||
```blade
|
||||
@component('mail::button', ['url' => $url, 'color' => 'primary|success|error'])
|
||||
Button Text
|
||||
@endcomponent
|
||||
```
|
||||
|
||||
**Farben:**
|
||||
- `primary` - Blauer Gradient (Standard)
|
||||
- `success` - Grüner Gradient
|
||||
- `error` - Roter Gradient
|
||||
|
||||
#### Panel
|
||||
```blade
|
||||
@component('mail::panel')
|
||||
Wichtiger Inhalt
|
||||
@endcomponent
|
||||
```
|
||||
|
||||
#### Tabelle
|
||||
```blade
|
||||
@component('mail::table')
|
||||
| Spalte 1 | Spalte 2 |
|
||||
|:---------|:---------|
|
||||
| Wert 1 | Wert 2 |
|
||||
@endcomponent
|
||||
```
|
||||
|
||||
### Custom Notification erstellen
|
||||
|
||||
1. Erstelle eine neue Notification-Klasse in `app/Notifications/`
|
||||
2. Erweitere die gewünschte Laravel-Notification
|
||||
3. Überschreibe die `buildMailMessage` Methode
|
||||
|
||||
Beispiel:
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class CustomNotification extends ResetPassword
|
||||
{
|
||||
protected function buildMailMessage($url)
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Betreff')
|
||||
->greeting('Hallo!')
|
||||
->line('Nachrichtentext')
|
||||
->action('Button', $url)
|
||||
->salutation('Grüße');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
Alle E-Mails sind für mobile Geräte optimiert und passen sich automatisch der Bildschirmgröße an.
|
||||
|
||||
## 🌙 Dark Mode
|
||||
|
||||
Das Theme unterstützt automatisch Dark Mode für E-Mail-Clients, die dies unterstützen.
|
||||
|
||||
## 🧪 Testen
|
||||
|
||||
E-Mails können im Browser getestet werden. Laravel speichert E-Mails standardmäßig im Log:
|
||||
|
||||
```bash
|
||||
php artisan tinker
|
||||
>>> $user = App\Models\User::first();
|
||||
>>> $user->sendPasswordResetNotification('test-token');
|
||||
```
|
||||
|
||||
Die E-Mail wird in `storage/logs/laravel.log` gespeichert.
|
||||
|
||||
## 🔄 Weitere E-Mail-Typen hinzufügen
|
||||
|
||||
Falls weitere E-Mail-Typen benötigt werden:
|
||||
|
||||
1. Erstelle eine neue Mailable-Klasse: `php artisan make:mail YourMail --markdown=emails.your-mail`
|
||||
2. Erstelle die Blade-Vorlage in `resources/views/emails/`
|
||||
3. Verwende die Standard-Komponenten für konsistentes Design
|
||||
4. Sende die E-Mail mit `Mail::to($user)->send(new YourMail())`
|
||||
|
||||
## 📝 Hinweise
|
||||
|
||||
- Alle E-Mails verwenden automatisch das B2IN-Theme
|
||||
- Die Vorlagen sind mehrsprachig vorbereitet
|
||||
- Header und Footer sind in allen E-Mails konsistent
|
||||
- E-Mails werden inline-styled für beste Kompatibilität mit E-Mail-Clients
|
||||
|
||||
|
|
@ -1,47 +1,64 @@
|
|||
<x-mail::message>
|
||||
# Willkommen bei B2In!
|
||||
@component('mail::message')
|
||||
# Willkommen bei {{ config('app.name') }}!
|
||||
|
||||
@if($contactFullName)
|
||||
Hallo {{ $contactFullName }},
|
||||
|
||||
Sie wurden eingeladen, Teil unserer Plattform zu werden.
|
||||
Sie wurden eingeladen, Teil unserer innovativen Partner-Plattform zu werden.
|
||||
@else
|
||||
Sie wurden eingeladen, Teil unserer Plattform zu werden.
|
||||
Sie wurden eingeladen, Teil unserer innovativen Partner-Plattform zu werden.
|
||||
@endif
|
||||
|
||||
## Firmeninformationen
|
||||
|
||||
**Firmenname:** {{ $companyName }}
|
||||
|
||||
**Partner-Typ:** {{ $partnerType }}
|
||||
|
||||
Wir freuen uns, Sie als **{{ $partnerType }}** in unserem Netzwerk begrüßen zu dürfen!
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
Klicken Sie auf den Button unten, um Ihre Registrierung abzuschließen. Sie können dann:
|
||||
|
||||
- Ihr Unternehmensprofil vervollständigen
|
||||
- Zugriff auf unser Partner-Portal erhalten
|
||||
- Mit anderen Partnern im Netzwerk interagieren
|
||||
|
||||
<x-mail::button :url="$invitationUrl" color="primary">
|
||||
Registrierung abschließen
|
||||
</x-mail::button>
|
||||
|
||||
<x-mail::panel>
|
||||
**Wichtig:** Diese Einladung ist gültig bis zum {{ $expiresAt->format('d.m.Y H:i') }} Uhr.
|
||||
</x-mail::panel>
|
||||
|
||||
Falls Sie Fragen haben, können Sie uns jederzeit kontaktieren.
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
{{ config('app.name') }} Team
|
||||
Wir freuen uns sehr, dass Sie Teil unseres wachsenden Netzwerks werden möchten!
|
||||
|
||||
---
|
||||
|
||||
<small>
|
||||
## Ihre Einladungsdetails
|
||||
|
||||
<x-mail::table>
|
||||
| Detail | Information |
|
||||
|:-------|:------------|
|
||||
| **Firmenname** | {{ $companyName }} |
|
||||
| **Partner-Typ** | {{ $partnerType }} |
|
||||
| **E-Mail** | {{ $invitation->email }} |
|
||||
</x-mail::table>
|
||||
|
||||
---
|
||||
|
||||
## So geht es weiter
|
||||
|
||||
Klicken Sie einfach auf den Button unten, um Ihre Registrierung in wenigen Schritten abzuschließen:
|
||||
|
||||
<x-mail::button :url="$invitationUrl" color="primary">
|
||||
Jetzt registrieren
|
||||
</x-mail::button>
|
||||
|
||||
Nach erfolgreicher Registrierung können Sie:
|
||||
|
||||
**Ihr Unternehmensprofil vervollständigen** – Präsentieren Sie Ihr Unternehmen optimal
|
||||
|
||||
**Zugriff auf das Partner-Portal erhalten** – Nutzen Sie alle Funktionen unserer Plattform
|
||||
|
||||
**Mit anderen Partnern vernetzen** – Profitieren Sie von unserem starken Netzwerk
|
||||
|
||||
**Projekte verwalten und koordinieren** – Effiziente Zusammenarbeit in Echtzeit
|
||||
|
||||
---
|
||||
|
||||
<x-mail::panel>
|
||||
**Wichtig:** Diese Einladung ist gültig bis zum **{{ $expiresAt->format('d.m.Y') }}** um **{{ $expiresAt->format('H:i') }} Uhr**.
|
||||
</x-mail::panel>
|
||||
|
||||
## Haben Sie Fragen?
|
||||
|
||||
Unser Support-Team steht Ihnen jederzeit gerne zur Verfügung. Zögern Sie nicht, uns zu kontaktieren!
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
|
||||
Ihr **{{ config('app.name') }}** Team
|
||||
|
||||
@slot('subcopy')
|
||||
Falls der Button nicht funktioniert, kopieren Sie bitte den folgenden Link in Ihren Browser:
|
||||
{{ $invitationUrl }}
|
||||
</small>
|
||||
</x-mail::message>
|
||||
[{{ $invitationUrl }}]({{ $invitationUrl }})
|
||||
@endslot
|
||||
@endcomponent
|
||||
|
|
|
|||
165
resources/views/emails/test-overview.blade.php
Normal file
165
resources/views/emails/test-overview.blade.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>E-Mail-Vorlagen Vorschau</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Instrument Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="min-h-screen py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-12">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">📧 E-Mail-Vorlagen Vorschau</h1>
|
||||
<p class="text-lg text-gray-600">Testen Sie alle E-Mail-Vorlagen im B2IN-Design</p>
|
||||
</div>
|
||||
|
||||
<!-- Email Templates Grid -->
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Partner Invitation -->
|
||||
<a href="{{ route('test.email.partner-invitation') }}"
|
||||
target="_blank"
|
||||
class="block p-6 bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 border-2 border-transparent hover:border-blue-500">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-blue-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-green-600 bg-green-100 px-3 py-1 rounded-full">Aktiv</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Partner-Einladung</h3>
|
||||
<p class="text-gray-600 mb-4">E-Mail für neue Partner-Einladungen mit allen Details und Call-to-Action</p>
|
||||
<div class="flex items-center text-blue-600 font-medium">
|
||||
<span>Vorschau anzeigen</span>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Password Reset -->
|
||||
<a href="{{ route('test.email.password-reset') }}"
|
||||
target="_blank"
|
||||
class="block p-6 bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 border-2 border-transparent hover:border-blue-500">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-orange-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-green-600 bg-green-100 px-3 py-1 rounded-full">Aktiv</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Passwort zurücksetzen</h3>
|
||||
<p class="text-gray-600 mb-4">E-Mail für Passwort-Reset-Anfragen mit zeitlich begrenztem Link</p>
|
||||
<div class="flex items-center text-blue-600 font-medium">
|
||||
<span>Vorschau anzeigen</span>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Email Verification -->
|
||||
<a href="{{ route('test.email.verification') }}"
|
||||
target="_blank"
|
||||
class="block p-6 bg-white rounded-xl shadow-md hover:shadow-xl transition-shadow duration-300 border-2 border-transparent hover:border-blue-500">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-3 bg-green-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-green-600 bg-green-100 px-3 py-1 rounded-full">Aktiv</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">E-Mail-Verifizierung</h3>
|
||||
<p class="text-gray-600 mb-4">Willkommens-E-Mail mit Bestätigungslink für neue Registrierungen</p>
|
||||
<div class="flex items-center text-blue-600 font-medium">
|
||||
<span>Vorschau anzeigen</span>
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Design System Info -->
|
||||
<div class="mt-12 bg-white rounded-xl shadow-md p-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">🎨 Design-System</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Farben</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-lg" style="background-color: #2b3f51;"></div>
|
||||
<div class="ml-4">
|
||||
<p class="font-medium text-gray-900">Primary (Anthracite)</p>
|
||||
<p class="text-sm text-gray-600">#2b3f51</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-12 h-12 rounded-lg" style="background-color: #20a0da;"></div>
|
||||
<div class="ml-4">
|
||||
<p class="font-medium text-gray-900">Secondary (Dynamic Blue)</p>
|
||||
<p class="text-sm text-gray-600">#20a0da</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Typografie</h3>
|
||||
<div class="space-y-2">
|
||||
<p class="font-medium text-gray-900">Font Family</p>
|
||||
<p class="text-gray-600">Instrument Sans</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3">Features</h3>
|
||||
<ul class="space-y-2 text-gray-600">
|
||||
<li class="flex items-center">
|
||||
<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Responsive Design
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Dark Mode Support
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Inline CSS (E-Mail-Client kompatibel)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documentation Link -->
|
||||
<div class="mt-8 text-center">
|
||||
<a href="/emails/README.md" class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Dokumentation anzeigen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
<div class="hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-gray-700 ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" id="user-menu">
|
||||
<a href="{{ route('admin.settings.profile') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Profil</a>
|
||||
<a href="{{ route('admin.settings.appearance') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Einstellungen</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('auth.logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Abmelden</button>
|
||||
</form>
|
||||
|
|
|
|||
307
resources/views/livewire/admin/c-m-s/cabinet-display.blade.php
Normal file
307
resources/views/livewire/admin/c-m-s/cabinet-display.blade.php
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
<div>
|
||||
<flux:header class="mb-6">
|
||||
<flux:heading size="xl">{{ __('Cabinet Display - CMS Verwaltung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie die Inhalte der Display-Seite') }}</flux:subheading>
|
||||
</flux:header>
|
||||
|
||||
{{-- Hilfe-Banner --}}
|
||||
<flux:card class="mb-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start gap-4">
|
||||
<flux:icon.information-circle class="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">{{ __('Schnellanleitung') }}</h3>
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200 space-y-1">
|
||||
<p>• <strong>Videos:</strong> Videos müssen aufgrund der Dateigrößer vorab per SFTP hochgeladen werden. Die Position (0-100%) bestimmt den vertikalen Bildausschnitt.</p>
|
||||
<p>• <strong>Footer-Inhalte:</strong> Werden alle 30 Sekunden gewechselt. URLs werden automatisch als QR-Code angezeigt.</p>
|
||||
<p>• <strong>Footer-Inhalte:</strong> Sind alle Inhalte ausgeblendet, wird der Footer ausgeblendet und das Video auf 100% der Höhe angezeigt.</p>
|
||||
<p>• <strong>Display-URL:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded">https://cabinet.b2in.eu</code></p>
|
||||
<p>• <strong>API-Endpunkt:</strong> <code class="px-2 py-0.5 bg-blue-100 dark:bg-blue-800 rounded">{{ url('/api/display/config') }}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Success-Meldungen --}}
|
||||
@if (session()->has('success'))
|
||||
<x-success-alert>
|
||||
{{ session('success') }}
|
||||
</x-success-alert>
|
||||
@endif
|
||||
|
||||
{{-- Video-Verwaltung --}}
|
||||
<flux:card class="mb-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Video-Playlist') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Videos werden in der angegebenen Reihenfolge abgespielt') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button wire:click="openVideoModal" icon="plus">
|
||||
{{ __('Video hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if($videos->isEmpty())
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.film class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>{{ __('Noch keine Videos vorhanden. Fügen Sie Ihr erstes Video hinzu!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($videos as $index => $video)
|
||||
<div wire:key="video-{{ $video->id }}"
|
||||
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($index > 0)
|
||||
<flux:button wire:click="moveVideo({{ $video->id }}, 'up')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-up"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($index < count($videos) - 1)
|
||||
<flux:button wire:click="moveVideo({{ $video->id }}, 'down')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$video->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $video->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<span class="font-semibold text-sm">{{ $video->title ?: $video->filename }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-x-4">
|
||||
<span>📁 {{ $video->filename }}</span>
|
||||
<span>📍 Position: {{ $video->position }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button wire:click="toggleVideoStatus({{ $video->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
:icon="$video->is_active ? 'eye-slash' : 'eye'">
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="openVideoModal({{ $video->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="pencil">
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="deleteVideo({{ $video->id }})"
|
||||
wire:confirm="Möchten Sie dieses Video wirklich löschen?"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon="trash"
|
||||
class="text-red-600 hover:text-red-700">
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Footer-Content-Verwaltung --}}
|
||||
<flux:card>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Footer-Inhalte') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Inhalte werden alle 30 Sekunden im Footer gewechselt') }}</flux:subheading>
|
||||
@if($footerContents->isNotEmpty())
|
||||
<div class="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
📊 Gesamt-Klicks: <strong>{{ $footerContents->sum('clicks') }}</strong>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<flux:button wire:click="openFooterModal" icon="plus">
|
||||
{{ __('Inhalt hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
@if($footerContents->isEmpty())
|
||||
<div class="text-center py-12 text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.document-text class="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||
<p>{{ __('Noch keine Footer-Inhalte vorhanden. Fügen Sie den ersten Inhalt hinzu!') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($footerContents as $index => $footer)
|
||||
<div wire:key="footer-{{ $footer->id }}"
|
||||
class="flex items-center gap-4 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition">
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
@if($index > 0)
|
||||
<flux:button wire:click="moveFooter({{ $footer->id }}, 'up')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-up"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
@if($index < count($footerContents) - 1)
|
||||
<flux:button wire:click="moveFooter({{ $footer->id }}, 'down')"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon="chevron-down"
|
||||
class="text-zinc-400 hover:text-zinc-600">
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<flux:badge :color="$footer->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $footer->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
<span class="font-semibold text-sm">{{ $footer->headline }}</span>
|
||||
<flux:badge color="blue" size="sm">
|
||||
<flux:icon.cursor-arrow-rays class="w-3 h-3" />
|
||||
{{ $footer->clicks }} {{ __('Klicks') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400 space-y-1">
|
||||
<div>{{ $footer->subline }}</div>
|
||||
|
||||
@if($footer->url)
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.link class="w-3 h-3" />
|
||||
<span class="font-mono bg-zinc-100 dark:bg-zinc-700 px-2 py-0.5 rounded">
|
||||
{{ $footer->short_code }}
|
||||
</span>
|
||||
<button
|
||||
onclick="navigator.clipboard.writeText('{{ $footer->short_url }}'); alert('Short-Link kopiert!');"
|
||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 text-xs"
|
||||
title="Short-Link kopieren">
|
||||
📋 Short-Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-xs">
|
||||
<flux:icon.arrow-top-right-on-square class="w-3 h-3" />
|
||||
<a href="{{ $footer->url }}" target="_blank" class="hover:underline">
|
||||
{{ Str::limit($footer->url, 50) }}
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-1 text-xs text-zinc-500 dark:text-zinc-500">
|
||||
<flux:icon.x-circle class="w-3 h-3" />
|
||||
<span>Kein QR-Code (Keine URL angegeben)</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button wire:click="toggleFooterStatus({{ $footer->id }})"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
:icon="$footer->is_active ? 'eye-slash' : 'eye'"
|
||||
title="{{ $footer->is_active ? 'Deaktivieren' : 'Aktivieren' }}">
|
||||
</flux:button>
|
||||
|
||||
<flux:dropdown>
|
||||
<flux:button size="sm" variant="ghost" icon="ellipsis-vertical"></flux:button>
|
||||
<flux:menu>
|
||||
<flux:menu.item wire:click="openFooterModal({{ $footer->id }})" icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item wire:click="regenerateShortCode({{ $footer->id }})" icon="arrow-path">
|
||||
{{ __('Short-Code neu generieren') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item wire:click="resetClicks({{ $footer->id }})"
|
||||
wire:confirm="Möchten Sie den Klick-Zähler wirklich zurücksetzen?"
|
||||
icon="arrow-path-rounded-square">
|
||||
{{ __('Klicks zurücksetzen') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item wire:click="deleteFooter({{ $footer->id }})"
|
||||
wire:confirm="Möchten Sie diesen Footer-Inhalt wirklich löschen?"
|
||||
icon="trash"
|
||||
class="text-red-600">
|
||||
{{ __('Löschen') }}
|
||||
</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Video Modal --}}
|
||||
<flux:modal :open="$showVideoModal" wire:model="showVideoModal">
|
||||
<form wire:submit.prevent="saveVideo">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $videoId ? __('Video bearbeiten') : __('Video hinzufügen') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:select wire:model="videoFilename" label="Video-Datei" placeholder="Wählen Sie ein Video...">
|
||||
@foreach($availableVideos as $videoFile)
|
||||
<option value="{{ $videoFile }}">{{ $videoFile }}</option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@error('videoFilename') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="videoTitle" label="Titel (optional)" placeholder="z.B. Herbst Kollektion 2025" />
|
||||
|
||||
<flux:input wire:model="videoPosition" type="number" min="0" max="100" label="Position (%)"
|
||||
description="Vertikale Position im Video (0 = oben, 100 = unten)" />
|
||||
@error('videoPosition') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:checkbox wire:model="videoIsActive" label="Video aktiv anzeigen" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="closeVideoModal" variant="ghost">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $videoId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Footer Modal --}}
|
||||
<flux:modal :open="$showFooterModal" wire:model="showFooterModal">
|
||||
<form wire:submit.prevent="saveFooter">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ $footerId ? __('Footer-Inhalt bearbeiten') : __('Footer-Inhalt hinzufügen') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:input wire:model="footerHeadline" label="Überschrift" placeholder="z.B. Beratung & Termin" />
|
||||
@error('footerHeadline') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="footerSubline" label="Unterzeile" placeholder="z.B. Jetzt Termin vereinbaren." />
|
||||
@error('footerSubline') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:input wire:model="footerUrl" label="URL (optional)" placeholder="https://www.cabinet.de/bielefeld..."
|
||||
description="Leer lassen = Kein QR-Code wird angezeigt. Mit URL = QR-Code mit Short-Link wird generiert." />
|
||||
@error('footerUrl') <span class="text-red-600 text-sm">{{ $message }}</span> @enderror
|
||||
|
||||
<flux:checkbox wire:model="footerIsActive" label="Footer-Inhalt aktiv anzeigen" />
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<flux:button type="button" wire:click="closeFooterModal" variant="ghost">
|
||||
{{ __('Abbrechen') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ $footerId ? __('Aktualisieren') : __('Hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
508
resources/views/livewire/admin/documentation.blade.php
Normal file
508
resources/views/livewire/admin/documentation.blade.php
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, computed};
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
state(['showToc' => false]);
|
||||
|
||||
$content = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return '<p class="text-red-600">Dokumentation nicht gefunden.</p>';
|
||||
}
|
||||
|
||||
$markdown = file_get_contents($mdPath);
|
||||
|
||||
// Configure CommonMark with GitHub Flavored Markdown
|
||||
$environment = new Environment([
|
||||
'html_input' => 'allow',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new GithubFlavoredMarkdownExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
|
||||
return $converter->convert($markdown)->getContent();
|
||||
});
|
||||
|
||||
// Extract Table of Contents from markdown
|
||||
$tableOfContents = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$markdown = file_get_contents($mdPath);
|
||||
$toc = [];
|
||||
|
||||
// Extract headings (## and ###)
|
||||
preg_match_all('/^(#{2,3})\s+(.+)$/m', $markdown, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$level = strlen($match[1]);
|
||||
$title = trim($match[2]);
|
||||
$slug = Str::slug($title);
|
||||
|
||||
$toc[] = [
|
||||
'level' => $level,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
];
|
||||
}
|
||||
|
||||
return $toc;
|
||||
});
|
||||
|
||||
$fileInfo = computed(function () {
|
||||
$mdPath = base_path('dev/entwicklung.md');
|
||||
|
||||
if (!file_exists($mdPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'size' => round(filesize($mdPath) / 1024, 1) . ' KB',
|
||||
'modified' => \Carbon\Carbon::parse(filemtime($mdPath))->format('d.m.Y H:i'),
|
||||
'lines' => count(file($mdPath)),
|
||||
];
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6 pb-12">
|
||||
{{-- Sticky Header --}}
|
||||
<div class="sticky top-0 z-10 bg-zinc-50 dark:bg-zinc-800 pb-4 border-b border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Projekt-Dokumentation') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Entwicklungsstand & Technische Übersicht') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="list-bullet"
|
||||
wire:click="$toggle('showToc')"
|
||||
>
|
||||
{{ __('Inhaltsverzeichnis') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="arrow-down-tray"
|
||||
href="{{ route('admin.documentation.download') }}"
|
||||
>
|
||||
{{ __('Download') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- File Info Bar --}}
|
||||
@if($this->fileInfo)
|
||||
<div class="flex items-center gap-4 mt-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.document-text class="w-3 h-3" />
|
||||
<span>{{ $this->fileInfo['size'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.clock class="w-3 h-3" />
|
||||
<span>{{ __('Aktualisiert:') }} {{ $this->fileInfo['modified'] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<flux:icon.bars-3 class="w-3 h-3" />
|
||||
<span>{{ $this->fileInfo['lines'] }} {{ __('Zeilen') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Table of Contents Sidebar (Collapsible) --}}
|
||||
@if($showToc)
|
||||
<flux:card class="p-6 bg-gradient-to-br from-accent-50 to-blue-50 dark:from-accent-900/20 dark:to-blue-900/20 border-accent-200 dark:border-accent-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<flux:heading size="lg">{{ __('Inhaltsverzeichnis') }}</flux:heading>
|
||||
<flux:button variant="ghost" size="sm" icon="x-mark" wire:click="$set('showToc', false)" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@foreach($this->tableOfContents as $item)
|
||||
<a href="#{{ $item['slug'] }}"
|
||||
class="block py-1.5 px-3 rounded-lg hover:bg-white dark:hover:bg-zinc-800 transition-colors
|
||||
{{ $item['level'] === 2 ? 'font-semibold text-zinc-900 dark:text-zinc-100' : 'ml-4 text-sm text-zinc-700 dark:text-zinc-300' }}"
|
||||
wire:click="$set('showToc', false)">
|
||||
{{ $item['level'] === 3 ? '• ' : '' }}{{ $item['title'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Markdown Content with Enhanced Styling --}}
|
||||
<div class="relative">
|
||||
<flux:card class="p-6 lg:p-8 shadow-lg">
|
||||
<style>
|
||||
/* Force headings visibility and styling - reduced by 30% */
|
||||
.prose h1 {
|
||||
display: block !important;
|
||||
font-size: 2.1rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.2 !important;
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 1.4rem !important;
|
||||
padding-bottom: 1rem !important;
|
||||
border-bottom: 2px solid #e5e7eb !important;
|
||||
background: linear-gradient(to right, #3b82f6, #2563eb) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
display: block !important;
|
||||
font-size: 1.3125rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.3 !important;
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
border-bottom: 1px solid #e5e7eb !important;
|
||||
scroll-margin-top: 5rem !important;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
display: block !important;
|
||||
font-size: 1.05rem !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1.4 !important;
|
||||
margin-top: 1.4rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
scroll-margin-top: 5rem !important;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
display: block !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.5 !important;
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Dark mode headings */
|
||||
.dark .prose h1 {
|
||||
border-bottom-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .prose h2 {
|
||||
border-bottom-color: #374151 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .prose h3 {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .prose h4 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Force list indentation and visibility - reduced by 30% */
|
||||
.prose ul {
|
||||
list-style-type: disc !important;
|
||||
margin-left: 1.75rem !important;
|
||||
padding-left: 0.7rem !important;
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.prose ol {
|
||||
list-style-type: decimal !important;
|
||||
margin-left: 1.75rem !important;
|
||||
padding-left: 0.7rem !important;
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
display: list-item !important;
|
||||
margin-bottom: 0.35rem !important;
|
||||
padding-left: 0.35rem !important;
|
||||
font-size: 0.95rem !important;
|
||||
line-height: 1.5 !important;
|
||||
}
|
||||
|
||||
.prose ul ul, .prose ol ol,
|
||||
.prose ul ol, .prose ol ul {
|
||||
margin-left: 1.4rem !important;
|
||||
margin-top: 0.35rem !important;
|
||||
margin-bottom: 0.35rem !important;
|
||||
}
|
||||
|
||||
/* Paragraphs - reduced by 30% */
|
||||
.prose p {
|
||||
margin-top: 0.7rem !important;
|
||||
margin-bottom: 0.7rem !important;
|
||||
line-height: 1.6 !important;
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
/* Strong/Bold */
|
||||
.prose strong {
|
||||
font-weight: 700 !important;
|
||||
color: #111827 !important;
|
||||
font-size: 0.95rem !important;
|
||||
}
|
||||
|
||||
.dark .prose strong {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* Code - reduced by 30% */
|
||||
.prose code {
|
||||
background-color: #f3f4f6 !important;
|
||||
padding: 0.175rem 0.35rem !important;
|
||||
border-radius: 0.25rem !important;
|
||||
font-size: 0.8rem !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.dark .prose code {
|
||||
background-color: #1f2937 !important;
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
/* HR - reduced by 30% */
|
||||
.prose hr {
|
||||
margin-top: 2.1rem !important;
|
||||
margin-bottom: 2.1rem !important;
|
||||
border-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .prose hr {
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="prose prose-zinc dark:prose-invert max-w-none
|
||||
{{-- Headings --}}
|
||||
prose-headings:font-bold prose-headings:tracking-tight
|
||||
prose-h1:text-5xl prose-h1:mb-8 prose-h1:mt-12 prose-h1:pb-6
|
||||
prose-h1:border-b-2 prose-h1:border-accent-300 dark:prose-h1:border-accent-700
|
||||
prose-h1:text-transparent prose-h1:bg-clip-text prose-h1:bg-gradient-to-r prose-h1:from-accent-600 prose-h1:to-blue-600 dark:prose-h1:from-accent-400 dark:prose-h1:to-blue-400
|
||||
|
||||
prose-h2:text-3xl prose-h2:mb-6 prose-h2:mt-12 prose-h2:pb-3
|
||||
prose-h2:border-b prose-h2:border-zinc-200 dark:prose-h2:border-zinc-700
|
||||
prose-h2:text-zinc-900 dark:prose-h2:text-zinc-100
|
||||
prose-h2:scroll-mt-20
|
||||
|
||||
prose-h3:text-2xl prose-h3:mb-4 prose-h3:mt-8
|
||||
prose-h3:text-zinc-800 dark:prose-h3:text-zinc-200
|
||||
prose-h3:scroll-mt-20
|
||||
|
||||
prose-h4:text-xl prose-h4:mb-3 prose-h4:mt-6
|
||||
prose-h4:text-zinc-700 dark:prose-h4:text-zinc-300
|
||||
|
||||
{{-- Paragraphs & Text --}}
|
||||
prose-p:mb-6 prose-p:leading-relaxed prose-p:text-zinc-700 dark:prose-p:text-zinc-300
|
||||
|
||||
{{-- Links --}}
|
||||
prose-a:text-accent-600 dark:prose-a:text-accent-400
|
||||
prose-a:font-medium prose-a:no-underline
|
||||
prose-a:transition-all prose-a:duration-200
|
||||
hover:prose-a:text-accent-700 dark:hover:prose-a:text-accent-300
|
||||
hover:prose-a:underline hover:prose-a:decoration-2 hover:prose-a:underline-offset-4
|
||||
|
||||
{{-- Strong & Emphasis --}}
|
||||
prose-strong:text-zinc-900 dark:prose-strong:text-zinc-100
|
||||
prose-strong:font-bold
|
||||
prose-em:text-zinc-800 dark:prose-em:text-zinc-200
|
||||
|
||||
{{-- Code --}}
|
||||
prose-code:bg-accent-100 dark:prose-code:bg-accent-900/30
|
||||
prose-code:text-accent-900 dark:prose-code:text-accent-100
|
||||
prose-code:px-2 prose-code:py-1 prose-code:rounded-md
|
||||
prose-code:text-sm prose-code:font-mono prose-code:font-semibold
|
||||
prose-code:before:content-none prose-code:after:content-none
|
||||
prose-code:border prose-code:border-accent-200 dark:prose-code:border-accent-800
|
||||
|
||||
{{-- Code Blocks --}}
|
||||
prose-pre:bg-gradient-to-br prose-pre:from-zinc-900 prose-pre:to-zinc-800
|
||||
dark:prose-pre:from-zinc-950 dark:prose-pre:to-zinc-900
|
||||
prose-pre:p-6 prose-pre:rounded-xl prose-pre:overflow-x-auto
|
||||
prose-pre:shadow-xl prose-pre:border prose-pre:border-zinc-700 dark:prose-pre:border-zinc-800
|
||||
prose-pre:my-8
|
||||
|
||||
{{-- Lists --}}
|
||||
prose-ul:list-disc prose-ul:ml-8 prose-ul:pl-6 prose-ul:mb-6 prose-ul:space-y-2
|
||||
prose-ol:list-decimal prose-ol:ml-8 prose-ol:pl-6 prose-ol:mb-6 prose-ol:space-y-2
|
||||
prose-li:text-zinc-700 dark:prose-li:text-zinc-300
|
||||
prose-li:leading-relaxed prose-li:pl-3
|
||||
prose-li:marker:text-accent-500 dark:prose-li:marker:text-accent-400
|
||||
prose-li:marker:font-bold
|
||||
|
||||
{{-- Nested Lists --}}
|
||||
prose-ul prose-ul:ml-8 prose-ul prose-ul:space-y-1
|
||||
prose-ol prose-ol:ml-8 prose-ol prose-ol:space-y-1
|
||||
|
||||
{{-- Blockquotes --}}
|
||||
prose-blockquote:border-l-4 prose-blockquote:border-accent-500 dark:prose-blockquote:border-accent-400
|
||||
prose-blockquote:bg-accent-50 dark:prose-blockquote:bg-accent-900/10
|
||||
prose-blockquote:pl-6 prose-blockquote:pr-4 prose-blockquote:py-4
|
||||
prose-blockquote:italic prose-blockquote:text-zinc-700 dark:prose-blockquote:text-zinc-300
|
||||
prose-blockquote:rounded-r-lg prose-blockquote:my-8
|
||||
prose-blockquote:shadow-sm
|
||||
|
||||
{{-- HR --}}
|
||||
prose-hr:border-zinc-300 dark:prose-hr:border-zinc-600
|
||||
prose-hr:my-12 prose-hr:border-t-2
|
||||
|
||||
{{-- Tables --}}
|
||||
prose-table:border-collapse prose-table:w-full
|
||||
prose-table:shadow-lg prose-table:rounded-lg prose-table:overflow-hidden
|
||||
prose-table:my-8
|
||||
|
||||
prose-thead:bg-gradient-to-r prose-thead:from-accent-100 prose-thead:to-blue-100
|
||||
dark:prose-thead:from-accent-900/40 dark:prose-thead:to-blue-900/40
|
||||
|
||||
prose-th:p-4 prose-th:text-left prose-th:font-bold
|
||||
prose-th:text-zinc-900 dark:prose-th:text-zinc-100
|
||||
prose-th:border-b-2 prose-th:border-accent-300 dark:prose-th:border-accent-700
|
||||
|
||||
prose-td:p-4 prose-td:border-b prose-td:border-zinc-200 dark:prose-td:border-zinc-700
|
||||
prose-td:text-zinc-700 dark:prose-td:text-zinc-300
|
||||
|
||||
prose-tr:transition-colors
|
||||
hover:prose-tr:bg-zinc-50 dark:hover:prose-tr:bg-zinc-800/50
|
||||
|
||||
{{-- Images --}}
|
||||
prose-img:rounded-xl prose-img:shadow-2xl prose-img:my-8
|
||||
prose-img:border prose-img:border-zinc-200 dark:prose-img:border-zinc-700
|
||||
">
|
||||
{!! $this->content !!}
|
||||
</div>
|
||||
|
||||
{{-- Scroll to top button --}}
|
||||
<div class="flex justify-center mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="arrow-up"
|
||||
onclick="window.scrollTo({top: 0, behavior: 'smooth'})"
|
||||
>
|
||||
{{ __('Zurück nach oben') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Quick Reference Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<flux:card class="p-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-blue-500 rounded-lg">
|
||||
<flux:icon.cube class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-blue-900 dark:text-blue-100">{{ __('Module') }}</flux:heading>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-blue-800 dark:text-blue-200">
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Multi-Domain-System
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Benutzer-Management
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Partner-Verwaltung
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
Hub-System
|
||||
</li>
|
||||
</ul>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-6 bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-purple-500 rounded-lg">
|
||||
<flux:icon.code-bracket class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-purple-900 dark:text-purple-100">{{ __('Technologien') }}</flux:heading>
|
||||
</div>
|
||||
<ul class="space-y-2 text-sm text-purple-800 dark:text-purple-200">
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Laravel 12
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Livewire 3 + Volt
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Flux UI Pro
|
||||
</li>
|
||||
<li class="flex items-center gap-2">
|
||||
<flux:icon.check class="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||
Spatie Permissions
|
||||
</li>
|
||||
</ul>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-6 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-2 bg-green-500 rounded-lg">
|
||||
<flux:icon.chart-bar class="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<flux:heading size="sm" class="text-green-900 dark:text-green-100">{{ __('Status-Legende') }}</flux:heading>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="green" size="sm">✓</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('Vollständig implementiert') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="yellow" size="sm">●</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('In Entwicklung') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:badge color="zinc" size="sm">○</flux:badge>
|
||||
<span class="text-green-800 dark:text-green-200">{{ __('Geplant') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
{{-- Footer Metadata --}}
|
||||
<flux:card class="p-6 bg-gradient-to-r from-zinc-50 to-zinc-100 dark:from-zinc-900 dark:to-zinc-800 border-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-6 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.document-text class="w-4 h-4" />
|
||||
<span class="font-mono">entwicklung.md</span>
|
||||
</div>
|
||||
@if($this->fileInfo)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.clock class="w-4 h-4" />
|
||||
<span>{{ __('Zuletzt aktualisiert:') }} {{ $this->fileInfo['modified'] }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<flux:badge color="accent" size="sm">
|
||||
{{ __('Live-Dokumentation') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
208
resources/views/livewire/admin/hubs/index.blade.php
Normal file
208
resources/views/livewire/admin/hubs/index.blade.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, computed};
|
||||
|
||||
state(['search' => '']);
|
||||
|
||||
$hubs = computed(function () {
|
||||
return \App\Models\Hub::with(['locations', 'partners'])
|
||||
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
|
||||
->get()
|
||||
->map(function ($hub) {
|
||||
$retailers = $hub->partners()->where('type', 'Retailer')->count();
|
||||
$brokers = $hub->partners()->where('type', 'Estate-Agent')->count();
|
||||
|
||||
return [
|
||||
'id' => $hub->id,
|
||||
'name' => $hub->name,
|
||||
'slug' => $hub->slug,
|
||||
'keyvisual' => $hub->keyvisual_url ?? '/images/default-keyvisual.jpg',
|
||||
'emblem' => $hub->emblem_url,
|
||||
'is_active' => $hub->is_active,
|
||||
'locations_count' => $hub->locations->count(),
|
||||
'retailers_count' => $retailers,
|
||||
'brokers_count' => $brokers,
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header mit Suche --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Hub-Verwaltung') }} (in Entwicklung)</flux:heading>
|
||||
<flux:subheading>{{ __('Regionale Marktplätze & Postleitzahlen-Zuordnung') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button variant="primary" icon="plus" :href="route('admin.hubs.create')" wire:navigate>
|
||||
{{ __('Neuer Hub') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Info-Banner --}}
|
||||
<flux:card class="p-4 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:icon.information-circle class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-semibold text-blue-900 dark:text-blue-100">
|
||||
{{ __('Konzept: Heimatgefühl + Weltmarkt') }}
|
||||
</div>
|
||||
<div class="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
{{ __('Jeder Hub filtert lokale Händler heraus, behält aber globale Hersteller bei. Kunden fühlen sich "zuhause" mit Zugriff auf das volle Sortiment.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Suchfeld --}}
|
||||
<flux:input
|
||||
wire:model.live.debounce="search"
|
||||
placeholder="{{ __('Hub suchen...') }}"
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
|
||||
{{-- Hubs als Karten-Grid --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@forelse($this->hubs as $hub)
|
||||
<a href="{{ route('admin.hubs.edit', $hub['id']) }}"
|
||||
wire:navigate
|
||||
class="block relative overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700 hover:shadow-lg hover:border-accent-300 dark:hover:border-accent-700 transition-all group">
|
||||
|
||||
{{-- Keyvisual Hintergrund --}}
|
||||
<div class="relative h-48 bg-cover bg-center"
|
||||
style="background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url('{{ $hub['keyvisual'] }}')">
|
||||
|
||||
{{-- Wappen oben rechts --}}
|
||||
@if($hub['emblem'])
|
||||
<div class="absolute top-3 right-3 w-12 h-12 bg-white rounded-full p-2 shadow-lg">
|
||||
<img src="{{ $hub['emblem'] }}" alt="Wappen" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Status Badge oben links --}}
|
||||
<div class="absolute top-3 left-3">
|
||||
<flux:badge :color="$hub['is_active'] ? 'green' : 'zinc'" size="sm">
|
||||
{{ $hub['is_active'] ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
{{-- Hub Name --}}
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4">
|
||||
<h3 class="text-2xl font-bold text-white group-hover:text-accent-200 transition-colors">
|
||||
{{ $hub['name'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-200">{{ $hub['slug'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Metriken --}}
|
||||
<div class="p-4 bg-white dark:bg-zinc-900">
|
||||
<div class="grid grid-cols-3 gap-2 text-center">
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['locations_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('PLZs') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['retailers_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Händler') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 bg-zinc-50 dark:bg-zinc-800 rounded group-hover:bg-accent-50 dark:group-hover:bg-accent-900/20 transition-colors">
|
||||
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $hub['brokers_count'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Makler') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="col-span-3">
|
||||
<flux:card class="p-12">
|
||||
<div class="text-center">
|
||||
<flux:icon.map class="w-16 h-16 text-zinc-400 mx-auto mb-4" />
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Noch keine Hubs angelegt') }}</flux:heading>
|
||||
<flux:subheading class="mb-4">
|
||||
{{ __('Erstellen Sie Ihren ersten regionalen Marktplatz') }}
|
||||
</flux:subheading>
|
||||
<flux:button variant="primary" icon="plus" :href="route('admin.hubs.create')" wire:navigate>
|
||||
{{ __('Ersten Hub erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- Statistik-Übersicht --}}
|
||||
@if($this->hubs->isNotEmpty())
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-8">
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||
<flux:icon.map class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->count() }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Gesamt Hubs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-green-100 dark:bg-green-900/20 rounded-lg">
|
||||
<flux:icon.check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->where('is_active', true)->count() }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Hubs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
|
||||
<flux:icon.map-pin class="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->sum('locations_count') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('PLZ-Gebiete') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-orange-100 dark:bg-orange-900/20 rounded-lg">
|
||||
<flux:icon.building-storefront class="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $this->hubs->sum('retailers_count') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Partner gesamt') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
482
resources/views/livewire/admin/hubs/manage.blade.php
Normal file
482
resources/views/livewire/admin/hubs/manage.blade.php
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, mount, computed};
|
||||
|
||||
state([
|
||||
'hubId' => null,
|
||||
'activeTab' => 'identity',
|
||||
'importMethod' => 'single',
|
||||
|
||||
// Identität
|
||||
'name' => '',
|
||||
'slug' => '',
|
||||
'keyvisual' => null,
|
||||
'emblem' => null,
|
||||
'is_active' => false,
|
||||
|
||||
// PLZ-Management
|
||||
'zipSearch' => '',
|
||||
'newZipCode' => '',
|
||||
'newCityName' => '',
|
||||
'rangeStart' => '',
|
||||
'rangeEnd' => '',
|
||||
'csvFile' => null,
|
||||
]);
|
||||
|
||||
mount(function ($hubId = null) {
|
||||
if ($hubId) {
|
||||
$hub = \App\Models\Hub::findOrFail($hubId);
|
||||
$this->hubId = $hub->id;
|
||||
$this->name = $hub->name;
|
||||
$this->slug = $hub->slug;
|
||||
$this->is_active = $hub->is_active;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-generate slug from name
|
||||
$updatedName = function ($value) {
|
||||
if (!$this->hubId) { // Only auto-generate for new hubs
|
||||
$this->slug = \Illuminate\Support\Str::slug($value);
|
||||
}
|
||||
};
|
||||
|
||||
$locations = computed(function () {
|
||||
if (!$this->hubId) return collect();
|
||||
|
||||
return \App\Models\HubLocation::where('hub_id', $this->hubId)
|
||||
->when($this->zipSearch, fn($q) => $q->where('zip_code', 'like', "%{$this->zipSearch}%")
|
||||
->orWhere('city_name', 'like', "%{$this->zipSearch}%"))
|
||||
->orderBy('zip_code')
|
||||
->paginate(50);
|
||||
});
|
||||
|
||||
$partners = computed(function () {
|
||||
if (!$this->hubId) return collect();
|
||||
|
||||
return \App\Models\Partner::where('hub_id', $this->hubId)
|
||||
->get();
|
||||
});
|
||||
|
||||
// Dummy save function
|
||||
$save = function () {
|
||||
// In production: Validation und Speicherung
|
||||
session()->flash('message', __('Hub gespeichert (Dummy-Funktion)'));
|
||||
};
|
||||
|
||||
// Dummy functions für PLZ-Management
|
||||
$addSingleZip = function () {
|
||||
session()->flash('message', __('PLZ hinzugefügt (Dummy-Funktion)'));
|
||||
};
|
||||
|
||||
$addZipRange = function () {
|
||||
session()->flash('message', __('PLZ-Bereich importiert (Dummy-Funktion)'));
|
||||
};
|
||||
|
||||
$importCsv = function () {
|
||||
session()->flash('message', __('CSV importiert (Dummy-Funktion)'));
|
||||
};
|
||||
|
||||
$deleteLocation = function ($id) {
|
||||
session()->flash('message', __('PLZ gelöscht (Dummy-Funktion)'));
|
||||
};
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">
|
||||
{{ $hubId ? __('Hub bearbeiten') : __('Neuer Hub') }} (in Entwicklung)
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ $name ?: __('Regionalen Marktplatz konfigurieren') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button variant="ghost" :href="route('admin.hubs.index')" wire:navigate>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
<flux:button variant="primary" icon="check" wire:click="save">
|
||||
{{ __('Speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Flash Message --}}
|
||||
@if (session()->has('message'))
|
||||
<flux:card class="p-4 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
|
||||
<div class="flex items-center gap-3">
|
||||
<flux:icon.check-circle class="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
<span class="text-sm text-green-900 dark:text-green-100">{{ session('message') }}</span>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Tabs --}}
|
||||
<flux:tabs wire:model.live="activeTab" variant="segmented">
|
||||
<flux:tab name="identity" icon="identification">{{ __('Identität & Design') }}</flux:tab>
|
||||
<flux:tab name="geography" icon="map">{{ __('Geografie & PLZ') }}</flux:tab>
|
||||
<flux:tab name="partners" icon="user-group">{{ __('Partner-Monitor') }}</flux:tab>
|
||||
</flux:tabs>
|
||||
|
||||
{{-- TAB 1: Identität & Design --}}
|
||||
@if($activeTab === 'identity')
|
||||
<flux:card class="p-6 space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{{-- Basis-Informationen --}}
|
||||
<div class="space-y-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Hub-Name') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model.live="name" placeholder="Ostwestfalen-Lippe" />
|
||||
<flux:description>{{ __('Wird dem Kunden angezeigt, z.B. "Region Ostwestfalen-Lippe"') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('URL-Slug') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="slug" placeholder="owl" />
|
||||
<flux:description>{{ __('Für saubere URLs, z.B. b2in.de/region/owl') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Status') }}</flux:label>
|
||||
<flux:checkbox wire:model="is_active">
|
||||
{{ __('Hub ist aktiv und für Kunden sichtbar') }}
|
||||
</flux:checkbox>
|
||||
<flux:description>
|
||||
{{ __('Inaktive Hubs sind im "Coming Soon"-Modus') }}
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- Vorschau --}}
|
||||
<div class="p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg">
|
||||
<h4 class="font-semibold mb-3 text-zinc-900 dark:text-zinc-100">{{ __('Vorschau: Kunden-Landingpage') }}</h4>
|
||||
<div class="relative h-48 rounded-lg overflow-hidden bg-zinc-200 dark:bg-zinc-700 shadow-lg">
|
||||
@if($keyvisual)
|
||||
<img src="{{ $keyvisual }}" class="w-full h-full object-cover" alt="Keyvisual" />
|
||||
@else
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center">
|
||||
<flux:icon.photo class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
|
||||
<p class="text-sm text-zinc-500">{{ __('Keyvisual hochladen') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
|
||||
<h3 class="text-xl font-bold text-white">
|
||||
{{ $name ?: __('Ihr Hub-Name') }}
|
||||
</h3>
|
||||
<p class="text-sm text-zinc-200">
|
||||
{{ __('Die besten Marken Europas, von Händlern aus Ihrer Nachbarschaft') }}
|
||||
</p>
|
||||
</div>
|
||||
@if($emblem)
|
||||
<div class="absolute top-3 right-3 w-12 h-12 bg-white rounded-full p-2 shadow-lg">
|
||||
<img src="{{ $emblem }}" alt="Emblem" class="w-full h-full object-contain" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Keyvisual Upload --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Keyvisual (Hintergrundbild)') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="keyvisual" accept="image/*" /> --}}
|
||||
<flux:description>
|
||||
{{ __('Atmosphärisches Regionalbild für emotionalen Einstieg') }}
|
||||
<br>
|
||||
<span class="text-xs">
|
||||
{{ __('Beispiele: Hermannsdenkmal (OWL), Skyline (Frankfurt), Hafen (Hamburg)') }}
|
||||
• {{ __('Empfohlen: 1920x800px, max. 2MB') }}
|
||||
</span>
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
|
||||
{{-- Wappen Upload --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Wappen / Emblem') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="emblem" accept="image/*" /> --}}
|
||||
<flux:description>
|
||||
{{ __('Offizielles Wappen oder Logo der Region für Vertrauen & Offizialität') }}
|
||||
<br>
|
||||
<span class="text-xs">
|
||||
{{ __('Beispiele: Sparrenburg-Logo, Landeswappen') }}
|
||||
• {{ __('Empfohlen: 256x256px, PNG mit transparentem Hintergrund') }}
|
||||
</span>
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- TAB 2: Geografie & PLZ --}}
|
||||
@if($activeTab === 'geography')
|
||||
<div class="space-y-6">
|
||||
|
||||
{{-- Info-Box --}}
|
||||
<flux:card class="p-4 bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
|
||||
<div class="flex items-start gap-3">
|
||||
<flux:icon.light-bulb class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
|
||||
{{ __('Die Mapping-Engine') }}
|
||||
</div>
|
||||
<div class="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
{{ __('Hier ordnen Sie Postleitzahlen diesem Hub zu. Gibt ein Kunde seine PLZ ein, wird er automatisch diesem regionalen Marktplatz zugewiesen und sieht lokale Händler.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- PLZ-Import Tools --}}
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('Postleitzahlen hinzufügen') }}</flux:heading>
|
||||
|
||||
<flux:tabs wire:model.live="importMethod" variant="segmented">
|
||||
<flux:tab name="single" icon="plus">{{ __('Einzeln') }}</flux:tab>
|
||||
<flux:tab name="range" icon="arrows-right-left">{{ __('Bereich') }}</flux:tab>
|
||||
<flux:tab name="csv" icon="document">{{ __('CSV-Import') }}</flux:tab>
|
||||
</flux:tabs>
|
||||
|
||||
<div class="mt-6">
|
||||
{{-- Einzelne PLZ --}}
|
||||
@if($importMethod === 'single')
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="newZipCode" placeholder="33602" />
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="newCityName" placeholder="Bielefeld" />
|
||||
</flux:field>
|
||||
</div>
|
||||
<flux:button wire:click="addSingleZip" icon="plus">
|
||||
{{ __('PLZ hinzufügen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- PLZ-Bereich --}}
|
||||
@if($importMethod === 'range')
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Von PLZ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="rangeStart" placeholder="33000" />
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Bis PLZ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="rangeEnd" placeholder="33999" />
|
||||
</flux:field>
|
||||
</div>
|
||||
<flux:button wire:click="addZipRange" icon="arrows-right-left">
|
||||
{{ __('Bereich importieren') }}
|
||||
</flux:button>
|
||||
<flux:description>
|
||||
⚠️ {{ __('Generiert automatisch alle PLZs im Bereich. Kann bei großen Bereichen mehrere Minuten dauern!') }}
|
||||
</flux:description>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- CSV-Import --}}
|
||||
@if($importMethod === 'csv')
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('CSV-Datei') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="file" wire:model="csvFile" accept=".csv" />
|
||||
<flux:description>
|
||||
{{ __('Format: PLZ,Stadt (eine Zeile pro Eintrag)') }}
|
||||
<br>
|
||||
<span class="text-xs">{{ __('Beispiel:') }}</span>
|
||||
<code class="text-xs bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded">
|
||||
33602,Bielefeld<br>33603,Bielefeld<br>33604,Bielefeld
|
||||
</code>
|
||||
</flux:description>
|
||||
</flux:field>
|
||||
<flux:button wire:click="importCsv" icon="arrow-up-tray">
|
||||
{{ __('CSV importieren') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- PLZ-Liste --}}
|
||||
<flux:card>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<flux:heading size="lg">
|
||||
{{ __('Zugeordnete Postleitzahlen') }}
|
||||
@if($hubId)
|
||||
<span class="text-sm font-normal text-zinc-600 dark:text-zinc-400">
|
||||
({{ $this->locations->total() }} {{ __('gesamt') }})
|
||||
</span>
|
||||
@endif
|
||||
</flux:heading>
|
||||
<flux:input
|
||||
wire:model.live.debounce="zipSearch"
|
||||
placeholder="{{ __('PLZ oder Stadt suchen...') }}"
|
||||
icon="magnifying-glass"
|
||||
class="w-64"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@if($hubId)
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('PLZ') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Stadt') }}</flux:table.column>
|
||||
<flux:table.column class="text-right w-32">{{ __('Aktion') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@forelse($this->locations as $location)
|
||||
<flux:table.row :key="$location->id">
|
||||
<flux:table.cell>
|
||||
<span class="font-mono">{{ $location->zip_code }}</span>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>{{ $location->city_name }}</flux:table.cell>
|
||||
<flux:table.cell class="text-right">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
icon="trash"
|
||||
wire:click="deleteLocation({{ $location->id }})"
|
||||
/>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="3" class="text-center py-8">
|
||||
<flux:icon.map-pin class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Noch keine PLZs zugeordnet') }}
|
||||
</p>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($this->locations->hasPages())
|
||||
<div class="mt-4 border-t border-zinc-200 dark:border-zinc-700 pt-4">
|
||||
{{ $this->locations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center py-8 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Speichern Sie zuerst den Hub, um PLZs hinzuzufügen') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 3: Partner-Monitor --}}
|
||||
@if($activeTab === 'partners')
|
||||
<flux:card class="p-6">
|
||||
<flux:heading size="lg" class="mb-4">
|
||||
{{ __('Partner in diesem Hub') }}
|
||||
@if($hubId)
|
||||
<span class="text-sm font-normal text-zinc-600 dark:text-zinc-400">
|
||||
({{ $this->partners->count() }} {{ __('Partner') }})
|
||||
</span>
|
||||
@endif
|
||||
</flux:heading>
|
||||
|
||||
{{-- Info --}}
|
||||
<flux:card class="p-4 mb-4 bg-zinc-50 dark:bg-zinc-800 border-0">
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<strong>{{ __('Logik:') }}</strong>
|
||||
<ul class="list-disc ml-5 mt-2 space-y-1">
|
||||
<li>{{ __('Händler (Retailer) → Werden diesem Hub fest zugeordnet') }}</li>
|
||||
<li>{{ __('Hersteller (Manufacturer) → Sind "Hub-agnostisch", in allen Hubs sichtbar') }}</li>
|
||||
<li>{{ __('Makler (Estate-Agent) → Werden Hub zugeordnet für regionale Kundenakquise') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
@if($hubId)
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column>{{ __('Name') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Typ') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Stadt') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Lieferradius') }}</flux:table.column>
|
||||
<flux:table.column class="text-center">{{ __('Status') }}</flux:table.column>
|
||||
<flux:table.column class="text-right">{{ __('Aktion') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@forelse($this->partners as $partner)
|
||||
<flux:table.row :key="$partner->id">
|
||||
<flux:table.cell>
|
||||
<div class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $partner->company_name }}
|
||||
</div>
|
||||
@if($partner->display_name && $partner->display_name !== $partner->company_name)
|
||||
<div class="text-xs text-zinc-500">{{ $partner->display_name }}</div>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
@php
|
||||
$typeColors = [
|
||||
'Retailer' => 'blue',
|
||||
'Manufacturer' => 'purple',
|
||||
'Estate-Agent' => 'green',
|
||||
];
|
||||
@endphp
|
||||
<flux:badge :color="$typeColors[$partner->type] ?? 'zinc'" size="sm">
|
||||
{{ $partner->type }}
|
||||
</flux:badge>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
{{ $partner->city ?? '-' }}
|
||||
</flux:table.cell>
|
||||
<flux:table.cell>
|
||||
@if($partner->delivery_radius_km)
|
||||
<span class="text-sm">{{ $partner->delivery_radius_km }} km</span>
|
||||
@else
|
||||
<span class="text-sm text-zinc-400">-</span>
|
||||
@endif
|
||||
</flux:table.cell>
|
||||
<flux:table.cell class="text-center">
|
||||
<flux:badge :color="$partner->is_active ? 'green' : 'zinc'" size="sm">
|
||||
{{ $partner->is_active ? __('Aktiv') : __('Inaktiv') }}
|
||||
</flux:badge>
|
||||
</flux:table.cell>
|
||||
<flux:table.cell class="text-right">
|
||||
<flux:button variant="ghost" size="sm" icon="eye">
|
||||
{{ __('Details') }}
|
||||
</flux:button>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="6" class="text-center py-8">
|
||||
<flux:icon.user-group class="w-12 h-12 text-zinc-400 mx-auto mb-2" />
|
||||
<p class="text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Noch keine Partner in diesem Hub') }}
|
||||
</p>
|
||||
<p class="text-xs text-zinc-500 mt-2">
|
||||
{{ __('Partner werden automatisch beim Onboarding zugeordnet (basierend auf PLZ)') }}
|
||||
</p>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
@else
|
||||
<div class="text-center py-8 text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Speichern Sie zuerst den Hub, um Partner-Zuordnungen zu sehen') }}
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ new class extends Component {
|
|||
public string $email = '';
|
||||
public bool $showSuccessMessage = false;
|
||||
public ?PartnerInvitation $lastInvitation = null;
|
||||
public int $expiryWeeks = 1;
|
||||
|
||||
public function getPartnerRoles()
|
||||
{
|
||||
|
|
@ -44,6 +45,7 @@ new class extends Component {
|
|||
'contactLastName' => 'nullable|string|max:255',
|
||||
'roleId' => 'required|exists:roles,id|in:' . implode(',', $availableRoleIds),
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
'expiryWeeks' => 'required|integer|in:1,2,3,4',
|
||||
], [
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'companyName.max' => __('Der Firmenname darf maximal 255 Zeichen lang sein.'),
|
||||
|
|
@ -55,6 +57,8 @@ new class extends Component {
|
|||
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
|
||||
'email.max' => __('Die E-Mail-Adresse darf maximal 255 Zeichen lang sein.'),
|
||||
'email.unique' => __('Diese E-Mail-Adresse ist bereits als Benutzer registriert.'),
|
||||
'expiryWeeks.required' => __('Bitte wählen Sie eine Gültigkeitsdauer aus.'),
|
||||
'expiryWeeks.in' => __('Die Gültigkeitsdauer muss zwischen 1 und 4 Wochen liegen.'),
|
||||
]);
|
||||
|
||||
// Prüfe ob bereits eine aktive Einladung existiert
|
||||
|
|
@ -76,7 +80,7 @@ new class extends Component {
|
|||
'email' => $this->email,
|
||||
'token' => PartnerInvitation::generateToken(),
|
||||
'status' => 'pending',
|
||||
'expires_at' => now()->addDays(7), // 7 Tage gültig
|
||||
'expires_at' => now()->addWeeks($this->expiryWeeks),
|
||||
'invited_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
|
|
@ -91,11 +95,12 @@ new class extends Component {
|
|||
$this->showSuccessMessage = true;
|
||||
|
||||
// Reset Form
|
||||
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email']);
|
||||
$this->reset(['companyName', 'contactFirstName', 'contactLastName', 'roleId', 'email', 'expiryWeeks']);
|
||||
|
||||
// Setze default Rolle wieder
|
||||
$firstRole = $this->getPartnerRoles()->first();
|
||||
$this->roleId = $firstRole?->id;
|
||||
$this->expiryWeeks = 1;
|
||||
|
||||
session()->flash('message', __('Einladung erfolgreich versendet!'));
|
||||
} catch (\Exception $e) {
|
||||
|
|
@ -226,6 +231,19 @@ new class extends Component {
|
|||
@error('roleId') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gültigkeit der Einladung') }}</flux:label>
|
||||
<flux:description>{{ __('Wählen Sie zwischen 1 und 4 Wochen') }}</flux:description>
|
||||
<flux:select wire:model="expiryWeeks">
|
||||
@for($i = 1; $i <= 4; $i++)
|
||||
<flux:select.option :value="$i">
|
||||
{{ $i }} {{ $i === 1 ? __('Woche') : __('Wochen') }}
|
||||
</flux:select.option>
|
||||
@endfor
|
||||
</flux:select>
|
||||
@error('expiryWeeks') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('E-Mail Adresse') }}</flux:label>
|
||||
<flux:description>{{ __('E-Mail des Ansprechpartners') }}</flux:description>
|
||||
|
|
|
|||
1254
resources/views/livewire/admin/partners/registration-codes.blade.php
Normal file
1254
resources/views/livewire/admin/partners/registration-codes.blade.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,412 @@
|
|||
<?php
|
||||
|
||||
use App\Models\RegistrationCode;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Livewire\Volt\Component;
|
||||
use function Livewire\Volt\{layout, title};
|
||||
|
||||
layout('components.layouts.app');
|
||||
title('Registrierung testen');
|
||||
|
||||
new class extends Component {
|
||||
public array $roleOptions = [];
|
||||
public string $selectedRole = 'broker';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadRoleOptions();
|
||||
}
|
||||
|
||||
private function loadRoleOptions(): void
|
||||
{
|
||||
$roles = Role::whereNotNull('reg_prefix')->where('can_be_invited', true)->orderBy('id', 'asc')->get();
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$key = strtolower(str_replace('-', '', $role->name));
|
||||
$slug = strtolower($role->reg_prefix);
|
||||
|
||||
$this->roleOptions[$key] = [
|
||||
'label' => $role->display_name ?? $role->name,
|
||||
'prefix' => $role->reg_prefix,
|
||||
'slug' => $slug,
|
||||
'color' => $role->color ?? 'zinc',
|
||||
'icon' => $role->icon ?? 'key',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($this->roleOptions)) {
|
||||
$this->selectedRole = array_key_first($this->roleOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
// Lade verfügbare Codes für ausgewählte Rolle
|
||||
$availableCodes = RegistrationCode::where('role', $this->selectedRole)
|
||||
->where('status', RegistrationCode::STATUS_AVAILABLE)
|
||||
->orderBy('created_at', 'desc')
|
||||
->take(10)
|
||||
->get();
|
||||
|
||||
// Lade letzte verwendete Codes
|
||||
$recentUsedCodes = RegistrationCode::where('status', RegistrationCode::STATUS_USED)
|
||||
->with('usedBy')
|
||||
->orderBy('used_at', 'desc')
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'roleOptions' => $this->roleOptions,
|
||||
'availableCodes' => $availableCodes,
|
||||
'recentUsedCodes' => $recentUsedCodes,
|
||||
];
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6 p-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Registrierung testen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Teste den kompletten Registrierungsprozess mit echten Codes') }}</flux:subheading>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Info Box --}}
|
||||
<flux:card class="bg-accent-50 dark:bg-accent-900/20 border-accent-200 dark:border-accent-800">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="h-12 w-12 rounded-lg bg-accent-100 dark:bg-accent-900/40 flex items-center justify-center">
|
||||
@svg('heroicon-o-information-circle', 'h-6 w-6 text-accent-600 dark:text-accent-400')
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<flux:heading size="md" class="mb-2">{{ __('Wie funktioniert das Testing?') }}</flux:heading>
|
||||
<flux:subheading class="mb-3">
|
||||
{{ __('Wähle eine Rolle aus, kopiere einen verfügbaren Code und teste den gesamten Prozess:') }}
|
||||
</flux:subheading>
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
<li>{{ __('Code-Eingabe auf der Landing-Page') }}</li>
|
||||
<li>{{ __('Account-Erstellung mit persönlichen Daten') }}</li>
|
||||
<li>{{ __('Setup-Wizard für Partner-Profil') }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Rollen-Auswahl --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Rolle wählen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Wähle die Rolle, für die du den Registrierungsprozess testen möchtest') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
@foreach($roleOptions as $key => $meta)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('selectedRole', '{{ $key }}')"
|
||||
class="p-4 rounded-lg border-2 transition-all {{ $selectedRole === $key ? 'border-accent-500 bg-accent-50 dark:bg-accent-900/20' : 'border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600' }}"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="h-12 w-12 rounded-lg flex items-center justify-center {{ $selectedRole === $key ? 'bg-accent-100 dark:bg-accent-900/40' : 'bg-zinc-100 dark:bg-zinc-800' }}">
|
||||
@svg('heroicon-o-'.$meta['icon'], 'h-6 w-6 ' . ($selectedRole === $key ? 'text-accent-600 dark:text-accent-400' : 'text-zinc-600 dark:text-zinc-400'))
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="font-semibold text-sm">{{ $meta['label'] }}</div>
|
||||
<div class="text-xs text-zinc-500">{{ __('Prefix:') }} {{ $meta['prefix'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Landing Page Link --}}
|
||||
@if($selectedRole && isset($roleOptions[$selectedRole]))
|
||||
<flux:card class="shadow-elegant bg-gradient-to-r from-accent-50 to-blue-50 dark:from-accent-900/20 dark:to-blue-900/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-arrow-top-right-on-square', 'inline-block h-5 w-5')
|
||||
{{ __('Landing-Page aufrufen') }}
|
||||
</flux:heading>
|
||||
<flux:subheading class="mb-4">
|
||||
{{ __('Starte den Registrierungsprozess für :role', ['role' => $roleOptions[$selectedRole]['label']]) }}
|
||||
</flux:subheading>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="arrow-right"
|
||||
href="{{ config('domains.domain_b2in_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('B2In') }}
|
||||
</flux:button>
|
||||
@if($selectedRole === 'customer')
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="sparkles"
|
||||
href="{{ config('domains.domain_style2own_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Style2Own') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="home-modern"
|
||||
href="{{ config('domains.domain_stileigentum_url') . route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug']], false) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Stileigentum') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
@if($selectedRole === 'customer')
|
||||
<div class="mt-3 text-xs text-zinc-600 dark:text-zinc-400">
|
||||
<flux:icon.information-circle class="inline-block h-4 w-4" />
|
||||
{{ __('Für Kunden stehen drei Landing-Page-Varianten zur Verfügung') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="h-20 w-20 rounded-2xl bg-white dark:bg-zinc-800 flex items-center justify-center shadow-lg">
|
||||
@svg('heroicon-o-'.$roleOptions[$selectedRole]['icon'], 'h-10 w-10 text-accent-600 dark:text-accent-400')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Verfügbare Codes --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-key', 'inline-block h-5 w-5')
|
||||
{{ __('Verfügbare Codes für :role', ['role' => $roleOptions[$selectedRole]['label'] ?? 'diese Rolle']) }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Kopiere einen dieser Codes für deinen Test') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if($availableCodes->isEmpty())
|
||||
<div class="text-center py-8">
|
||||
<flux:icon.exclamation-triangle class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:heading size="md" class="mt-4">{{ __('Keine verfügbaren Codes') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Erstelle zuerst Codes in der Registrierungscode-Verwaltung') }}
|
||||
</flux:subheading>
|
||||
<div class="mt-4">
|
||||
<flux:button
|
||||
variant="primary"
|
||||
icon="plus"
|
||||
href="{{ route('admin.partners.registration-codes') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Zur Code-Verwaltung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($availableCodes as $code)
|
||||
<div class="p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="font-mono text-xl font-bold text-zinc-900 dark:text-white">
|
||||
{{ $code->code }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick="navigator.clipboard.writeText('{{ $code->code }}');
|
||||
alert('Code kopiert: {{ $code->code }}');"
|
||||
class="p-2 rounded-lg bg-accent-100 dark:bg-accent-900/40 hover:bg-accent-200 dark:hover:bg-accent-900/60 transition-colors"
|
||||
title="{{ __('Code kopieren') }}"
|
||||
>
|
||||
@svg('heroicon-o-clipboard-document', 'h-5 w-5 text-accent-600 dark:text-accent-400')
|
||||
</button>
|
||||
</div>
|
||||
@if($code->name)
|
||||
<div class="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1">
|
||||
{{ $code->name }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.calendar class="h-4 w-4" />
|
||||
{{ __('Erstellt:') }} {{ $code->created_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
@if($code->expires_at)
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<flux:icon.clock class="h-4 w-4" />
|
||||
{{ __('Gültig bis:') }} {{ $code->expires_at->format('d.m.Y') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Test-Prozess Steps --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-clipboard-document-check', 'inline-block h-5 w-5')
|
||||
{{ __('Test-Prozess') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Folge diesen Schritten für einen vollständigen Test') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-blue-500 text-white flex items-center justify-center font-bold">
|
||||
1
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Code kopieren') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Wähle einen verfügbaren Code aus der Liste oben und kopiere ihn') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-green-500 text-white flex items-center justify-center font-bold">
|
||||
2
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Landing-Page öffnen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400 mb-2">
|
||||
{{ __('Öffne die Landing-Page in einem neuen Tab (Private/Inkognito-Modus empfohlen)') }}
|
||||
</div>
|
||||
<flux:button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
icon="arrow-top-right-on-square"
|
||||
href="{{ route('registration.landing', ['role' => $roleOptions[$selectedRole]['slug'] ?? 'e']) }}"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Landing-Page öffnen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-purple-500 text-white flex items-center justify-center font-bold">
|
||||
3
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Code eingeben') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Gib den kopierten Code auf der Landing-Page ein und klicke auf "Code prüfen"') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-orange-500 text-white flex items-center justify-center font-bold">
|
||||
4
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Account erstellen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Fülle das Registrierungsformular mit Test-Daten aus (verwende eine Test-Email)') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 p-4 rounded-lg bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800">
|
||||
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-teal-500 text-white flex items-center justify-center font-bold">
|
||||
5
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-zinc-900 dark:text-white mb-1">
|
||||
{{ __('Setup-Wizard durchlaufen') }}
|
||||
</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Vervollständige das Partner-Profil im Setup-Wizard') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Letzte verwendete Codes --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="mb-6">
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
@svg('heroicon-o-check-circle', 'inline-block h-5 w-5')
|
||||
{{ __('Kürzlich getestete Codes') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>{{ __('Diese Codes wurden bereits verwendet') }}</flux:subheading>
|
||||
</div>
|
||||
|
||||
@if($recentUsedCodes->isEmpty())
|
||||
<div class="text-center py-8 text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Noch keine Codes verwendet') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($recentUsedCodes as $code)
|
||||
<div class="flex items-center justify-between p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="font-mono font-semibold text-zinc-900 dark:text-white">
|
||||
{{ $code->code }}
|
||||
</div>
|
||||
<flux:badge size="sm" color="zinc">
|
||||
{{ $roleOptions[$code->role]['label'] ?? $code->role }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
<div class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
@if($code->usedBy)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.user class="h-4 w-4" />
|
||||
{{ $code->usedBy->name }}
|
||||
@if($code->used_at)
|
||||
<span class="text-xs">• {{ $code->used_at->diffForHumans() }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Quick Actions --}}
|
||||
<flux:card class="shadow-elegant bg-zinc-50 dark:bg-zinc-800/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="md" class="mb-2">{{ __('Weitere Aktionen') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Nützliche Links für das Testing') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="key"
|
||||
href="{{ route('admin.partners.registration-codes') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Code-Verwaltung') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
variant="ghost"
|
||||
icon="users"
|
||||
href="{{ route('admin.users') }}"
|
||||
wire:navigate
|
||||
>
|
||||
{{ __('Benutzer-Verwaltung') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\WithPagination;
|
||||
use function Livewire\Volt\{layout, title, state};
|
||||
|
|
@ -13,17 +14,32 @@ new class extends Component {
|
|||
|
||||
public string $search = '';
|
||||
public string $roleFilter = '';
|
||||
public string $sortField = 'name';
|
||||
public string $sortDirection = 'asc';
|
||||
public string $parentPartnerFilter = '';
|
||||
public string $brandFilter = '';
|
||||
public string $setupStatusFilter = '';
|
||||
public string $sortField = 'created_at';
|
||||
public string $sortDirection = 'desc';
|
||||
|
||||
// Modal state
|
||||
// Modal state for roles
|
||||
public bool $showRoleModal = false;
|
||||
public ?int $selectedUserId = null;
|
||||
public array $selectedRoles = [];
|
||||
|
||||
// Modal state for editing user
|
||||
public bool $showEditModal = false;
|
||||
public ?int $editUserId = null;
|
||||
public string $userName = '';
|
||||
public string $displayName = '';
|
||||
public string $userEmail = '';
|
||||
public bool $emailVerified = false;
|
||||
|
||||
// Modal state for viewing partner data
|
||||
public bool $showViewModal = false;
|
||||
public ?int $viewUserId = null;
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
$query = User::with('roles')
|
||||
$query = User::with(['roles', 'registrationCode', 'partner.parentPartner'])
|
||||
->when($this->search, fn($q, $search) =>
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
|
|
@ -32,14 +48,62 @@ new class extends Component {
|
|||
$q->whereHas('roles', fn($roleQuery) =>
|
||||
$roleQuery->where('name', $role)
|
||||
)
|
||||
)
|
||||
->when($this->parentPartnerFilter, fn($q, $parentPartnerId) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->where('parent_partner_id', $parentPartnerId)
|
||||
)
|
||||
)
|
||||
->when($this->brandFilter, fn($q, $brand) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->where('brand', $brand)
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'completed', fn($q) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->whereNotNull('setup_completed_at')
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'pending', fn($q) =>
|
||||
$q->whereHas('partner', fn($partnerQuery) =>
|
||||
$partnerQuery->whereNull('setup_completed_at')
|
||||
)
|
||||
)
|
||||
->when($this->setupStatusFilter === 'no_partner', fn($q) =>
|
||||
$q->whereNull('partner_id')
|
||||
);
|
||||
|
||||
// Finde alle Partner, die als parent_partner_id verwendet werden
|
||||
$parentPartnerIds = \App\Models\Partner::whereNotNull('parent_partner_id')
|
||||
->distinct()
|
||||
->pluck('parent_partner_id');
|
||||
|
||||
$availableParentPartners = \App\Models\Partner::whereIn('id', $parentPartnerIds)
|
||||
->orderBy('company_name')
|
||||
->get();
|
||||
|
||||
// Finde alle verfügbaren Brands
|
||||
$availableBrands = \App\Models\Partner::whereNotNull('brand')
|
||||
->distinct()
|
||||
->pluck('brand')
|
||||
->sort();
|
||||
|
||||
// Zähle User mit abgeschlossenem Setup
|
||||
$setupCompletedCount = User::whereHas('partner', function($q) {
|
||||
$q->whereNotNull('setup_completed_at');
|
||||
})->count();
|
||||
|
||||
return [
|
||||
'users' => $query->orderBy($this->sortField, $this->sortDirection)->paginate(15),
|
||||
'totalUsers' => User::count(),
|
||||
'verifiedUsers' => User::whereNotNull('email_verified_at')->count(),
|
||||
'setupCompletedUsers' => $setupCompletedCount,
|
||||
'availableRoles' => \Spatie\Permission\Models\Role::orderBy('name')->get(),
|
||||
'availableParentPartners' => $availableParentPartners,
|
||||
'availableBrands' => $availableBrands,
|
||||
'selectedUser' => $this->selectedUserId ? User::find($this->selectedUserId) : null,
|
||||
'editUser' => $this->editUserId ? User::find($this->editUserId) : null,
|
||||
'viewUser' => $this->viewUserId ? User::with(['partner.brand', 'roles'])->find($this->viewUserId) : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +127,21 @@ new class extends Component {
|
|||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingParentPartnerFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingBrandFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingSetupStatusFilter(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function openRoleModal(int $userId): void
|
||||
{
|
||||
$user = User::with('roles')->findOrFail($userId);
|
||||
|
|
@ -94,22 +173,123 @@ new class extends Component {
|
|||
$this->selectedUserId = null;
|
||||
$this->selectedRoles = [];
|
||||
}
|
||||
|
||||
public function openEditModal(int $userId): void
|
||||
{
|
||||
$user = User::findOrFail($userId);
|
||||
$this->editUserId = $userId;
|
||||
$this->userName = $user->name;
|
||||
$this->displayName = $user->display_name ?? '';
|
||||
$this->userEmail = $user->email;
|
||||
$this->emailVerified = !is_null($user->email_verified_at);
|
||||
$this->showEditModal = true;
|
||||
}
|
||||
|
||||
public function saveUser(): void
|
||||
{
|
||||
if (!$this->editUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::with('roles')->findOrFail($this->editUserId);
|
||||
|
||||
// Prüfe ob User eine Rolle hat, die display_name erfordert
|
||||
$requiresDisplayName = $user->roles->whereIn('name', ['Broker', 'Retailer', 'Manufacturer'])->isNotEmpty();
|
||||
|
||||
$rules = [
|
||||
'userName' => 'required|string|max:255',
|
||||
'displayName' => $requiresDisplayName ? 'required|string|max:255' : 'nullable|string|max:255',
|
||||
'userEmail' => 'required|email|max:255|unique:users,email,' . $this->editUserId,
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'displayName.required' => __('Name für die Zuordnung von Kunden ist für Makler, Händler und Hersteller erforderlich.'),
|
||||
];
|
||||
|
||||
$this->validate($rules, $messages);
|
||||
|
||||
$user->update([
|
||||
'name' => $this->userName,
|
||||
'display_name' => $this->displayName ?: null,
|
||||
'email' => $this->userEmail,
|
||||
'email_verified_at' => $this->emailVerified ? ($user->email_verified_at ?? now()) : null,
|
||||
]);
|
||||
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['editUserId', 'userName', 'displayName', 'userEmail', 'emailVerified']);
|
||||
|
||||
session()->flash('message', __('User updated successfully!'));
|
||||
}
|
||||
|
||||
public function closeEditModal(): void
|
||||
{
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['editUserId', 'userName', 'displayName', 'userEmail', 'emailVerified']);
|
||||
}
|
||||
|
||||
public function openViewModal(int $userId): void
|
||||
{
|
||||
$this->viewUserId = $userId;
|
||||
$this->showViewModal = true;
|
||||
}
|
||||
|
||||
public function closeViewModal(): void
|
||||
{
|
||||
$this->showViewModal = false;
|
||||
$this->viewUserId = null;
|
||||
}
|
||||
|
||||
public function deleteUser(int $userId): void
|
||||
{
|
||||
$user = User::findOrFail($userId);
|
||||
|
||||
// Prüfe auf Verknüpfungen
|
||||
if ($user->hasDependencies()) {
|
||||
// Anonymisiere statt zu löschen
|
||||
$user->anonymize();
|
||||
session()->flash('message', __('User wurde anonymisiert, da Verknüpfungen existieren.'));
|
||||
} else {
|
||||
// Soft Delete
|
||||
$user->delete();
|
||||
session()->flash('message', __('User deleted successfully!'));
|
||||
}
|
||||
|
||||
// Schließe ggf. offene Modals
|
||||
$this->showRoleModal = false;
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedUserId', 'editUserId']);
|
||||
}
|
||||
|
||||
public function loginAsUser(int $userId): void
|
||||
{
|
||||
$currentUser = Auth::user();
|
||||
$targetUser = User::findOrFail($userId);
|
||||
|
||||
// Speichere den aktuellen Admin-User in der Session
|
||||
session(['impersonate_from' => $currentUser->id]);
|
||||
|
||||
// Logge als Ziel-User ein
|
||||
Auth::login($targetUser);
|
||||
|
||||
// Weiterleitung zum Dashboard
|
||||
$this->redirect(route('dashboard'), navigate: false);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6 p-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Users Management') }}</flux:heading>
|
||||
<flux:heading size="xl" class="mb-2">{{ __('Benutzerverwaltung') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage users and their roles in your application') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{{-- <div class="flex gap-2">
|
||||
<flux:button variant="primary" icon="plus">{{ __('Create User') }}</flux:button>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
|
||||
{{-- Statistics --}}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -128,8 +308,20 @@ new class extends Component {
|
|||
<flux:subheading>{{ __('Verified Users') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $verifiedUsers }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.shield-check class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/20">
|
||||
<flux:icon.shield-check class="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:subheading>{{ __('Setup abgeschlossen') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $setupCompletedUsers }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/20">
|
||||
<flux:icon.check-circle class="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -140,8 +332,8 @@ new class extends Component {
|
|||
<flux:subheading>{{ __('Active Roles') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">{{ $availableRoles->count() }}</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.user-group class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/20">
|
||||
<flux:icon.user-group class="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
|
@ -149,7 +341,7 @@ new class extends Component {
|
|||
|
||||
{{-- Filters --}}
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
||||
<flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="{{ __('Search users...') }}" />
|
||||
|
||||
<flux:select wire:model.live="roleFilter" placeholder="{{ __('All Roles') }}">
|
||||
|
|
@ -159,8 +351,31 @@ new class extends Component {
|
|||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
@if($search || $roleFilter)
|
||||
<flux:button wire:click="$set('search', ''); $set('roleFilter', '')" variant="ghost" icon="x-mark">
|
||||
<flux:select wire:model.live="brandFilter" placeholder="{{ __('Alle Brands') }}">
|
||||
<flux:select.option value="">{{ __('Alle Brands') }}</flux:select.option>
|
||||
@foreach($availableBrands as $brand)
|
||||
<flux:select.option :value="$brand">{{ strtoupper($brand) }}</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select variant="listbox" searchable wire:model.live="parentPartnerFilter" placeholder="{{ __('Alle Partner-Zuordnungen') }}">
|
||||
<flux:select.option value="">{{ __('Alle Partner-Zuordnungen') }}</flux:select.option>
|
||||
@foreach($availableParentPartners as $partner)
|
||||
<flux:select.option :value="$partner->id">
|
||||
{{ $partner->company_name }}
|
||||
</flux:select.option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="setupStatusFilter" placeholder="{{ __('Setup Status') }}">
|
||||
<flux:select.option value="">{{ __('Alle Setup-Status') }}</flux:select.option>
|
||||
<flux:select.option value="completed">{{ __('Setup abgeschlossen') }}</flux:select.option>
|
||||
<flux:select.option value="pending">{{ __('Setup ausstehend') }}</flux:select.option>
|
||||
<flux:select.option value="no_partner">{{ __('Kein Partner') }}</flux:select.option>
|
||||
</flux:select>
|
||||
|
||||
@if($search || $roleFilter || $parentPartnerFilter || $brandFilter || $setupStatusFilter)
|
||||
<flux:button wire:click="$set('search', ''); $set('roleFilter', ''); $set('parentPartnerFilter', ''); $set('brandFilter', ''); $set('setupStatusFilter', '')" variant="ghost" icon="x-mark">
|
||||
{{ __('Clear Filters') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
|
|
@ -187,9 +402,17 @@ new class extends Component {
|
|||
@endif
|
||||
</div>
|
||||
</flux:table.column>
|
||||
<flux:table.column>{{ __('Roles') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('roles.role') }}</flux:table.column>
|
||||
<flux:table.column class="w-48" wire:click="sortBy('created_at')" class="cursor-pointer">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ __('Registrierung & Setup') }}
|
||||
@if($sortField === 'created_at')
|
||||
<flux:icon.{{ $sortDirection === 'asc' ? 'chevron-up' : 'chevron-down' }} variant="micro" />
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.column>
|
||||
<flux:table.column class="w-32">{{ __('Status') }}</flux:table.column>
|
||||
<flux:table.column class="w-32">{{ __('Actions') }}</flux:table.column>
|
||||
<flux:table.column class="w-40">{{ __('Actions') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
|
|
@ -202,8 +425,32 @@ new class extends Component {
|
|||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-zinc-900 dark:text-white">{{ $user->name }}</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('ID:') }} {{ $user->id }}
|
||||
@if($user->display_name)
|
||||
<div class="text-xs font-medium text-accent-600 dark:text-accent-400">
|
||||
{{ $user->display_name }}
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
@if($user->registrationCode)
|
||||
<span class="font-mono">{{ $user->registrationCode->code }}</span>
|
||||
@else
|
||||
<span class="text-zinc-400">{{ __('Kein Code') }}</span>
|
||||
@endif
|
||||
@if($user->partner && $user->partner->brand)
|
||||
<span class="text-zinc-300 dark:text-zinc-600">•</span>
|
||||
@php
|
||||
$brandColors = [
|
||||
'b2in' => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
'style2own' => 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||
'stileigentum' => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
'b2a' => 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400',
|
||||
];
|
||||
$brandColor = $brandColors[$user->partner->brand] ?? 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400';
|
||||
@endphp
|
||||
<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium {{ $brandColor }}">
|
||||
{{ strtoupper($user->partner->brand) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -234,6 +481,45 @@ new class extends Component {
|
|||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
<div class="flex flex-col space-y-1 text-sm">
|
||||
{{-- Registrierungsdatum --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.user-plus variant="micro" class="text-zinc-400" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-zinc-900 dark:text-white">
|
||||
{{ $user->created_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Registriert') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Setup-Abschluss --}}
|
||||
@if($user->partner && $user->partner->setup_completed_at)
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.check-circle variant="micro" class="text-green-500" />
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">
|
||||
{{ $user->partner->setup_completed_at->format('d.m.Y H:i') }}
|
||||
</span>
|
||||
<span class="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{{ __('Setup abgeschlossen') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:icon.clock variant="micro" class="text-orange-400" />
|
||||
<span class="text-xs text-orange-600 dark:text-orange-400">
|
||||
{{ __('Setup ausstehend') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
<flux:table.cell>
|
||||
@if($user->email_verified_at)
|
||||
<flux:badge size="sm" color="green" icon="check-circle">
|
||||
|
|
@ -248,21 +534,37 @@ new class extends Component {
|
|||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
@if($user->partner_id)
|
||||
<flux:button size="sm" variant="ghost" icon="eye"
|
||||
wire:click="openViewModal({{ $user->id }})"
|
||||
tooltip="{{ __('Partner-Daten ansehen') }}"></flux:button>
|
||||
@endif
|
||||
@if($user->id !== Auth::id())
|
||||
<flux:button size="sm" variant="ghost" icon="arrow-right-start-on-rectangle"
|
||||
wire:click="loginAsUser({{ $user->id }})"
|
||||
wire:confirm="{{ __('Als :name einloggen? Sie können später zurück zum Admin wechseln.', ['name' => $user->name]) }}"
|
||||
tooltip="{{ __('Login als User') }}"></flux:button>
|
||||
@endif
|
||||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||||
wire:click="openEditModal({{ $user->id }})"
|
||||
tooltip="{{ __('Edit User') }}"></flux:button>
|
||||
<flux:button size="sm" variant="ghost" icon="trash"
|
||||
tooltip="{{ __('Delete User') }}"></flux:button>
|
||||
@if($user->id !== Auth::id())
|
||||
<flux:button size="sm" variant="ghost" icon="trash"
|
||||
wire:click="deleteUser({{ $user->id }})"
|
||||
wire:confirm="{{ __('Möchten Sie diesen Benutzer wirklich löschen? Falls Verknüpfungen existieren, wird der Benutzer anonymisiert.') }}"
|
||||
tooltip="{{ __('Delete User') }}"></flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="5">
|
||||
<flux:table.cell colspan="6">
|
||||
<div class="py-12 text-center">
|
||||
<flux:icon.users variant="outline" class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<flux:heading size="lg" class="mt-4">{{ __('No users found') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
@if($search || $roleFilter)
|
||||
@if($search || $roleFilter || $parentPartnerFilter || $brandFilter || $setupStatusFilter)
|
||||
{{ __('Try adjusting your filters.') }}
|
||||
@else
|
||||
{{ __('Get started by creating a new user.') }}
|
||||
|
|
@ -299,7 +601,7 @@ new class extends Component {
|
|||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Roles') }}</flux:label>
|
||||
<flux:label>{{ __('roles.role') }}</flux:label>
|
||||
<flux:description>{{ __('Select one or multiple roles for this user') }}</flux:description>
|
||||
|
||||
<div class="space-y-2 mt-3">
|
||||
|
|
@ -343,6 +645,363 @@ new class extends Component {
|
|||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Edit User Modal --}}
|
||||
<flux:modal name="edit-user-modal" :variant="'flyout'" wire:model="showEditModal">
|
||||
<form wire:submit="saveUser" class="space-y-6 max-w-2xl">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Edit User') }}</flux:heading>
|
||||
<flux:subheading>
|
||||
@if($editUser)
|
||||
{{ __('Editing user') }}: <strong>{{ $editUser->name }}</strong>
|
||||
@endif
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Name') }}</flux:label>
|
||||
<flux:input wire:model="userName" placeholder="{{ __('Enter user name') }}" />
|
||||
@error('userName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>
|
||||
{{ __('Name für die Zuordnung von Kunden') }}
|
||||
@if($editUser && $editUser->roles->whereIn('name', ['Broker', 'Retailer', 'Manufacturer'])->isNotEmpty())
|
||||
<span class="text-red-500">*</span>
|
||||
@endif
|
||||
</flux:label>
|
||||
<flux:description>
|
||||
{{ __('Pflichtfeld für Makler, Händler und Hersteller') }}
|
||||
</flux:description>
|
||||
<flux:input wire:model="displayName" placeholder="{{ __('z.B. Max Mustermann Immobilien') }}" />
|
||||
@error('displayName') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Email') }}</flux:label>
|
||||
<flux:input type="email" wire:model="userEmail" placeholder="{{ __('Enter email address') }}" />
|
||||
@error('userEmail') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Email Verification') }}</flux:label>
|
||||
<flux:description>{{ __('Set whether the email address is verified') }}</flux:description>
|
||||
<flux:checkbox wire:model="emailVerified" :label="__('Email verified')" />
|
||||
</flux:field>
|
||||
|
||||
@if($editUser && $editUser->registrationCode)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Registration Code') }}</flux:label>
|
||||
<div class="rounded-lg bg-zinc-50 p-4 dark:bg-zinc-800/50">
|
||||
<span class="font-mono font-semibold text-lg">{{ $editUser->registrationCode->code }}</span>
|
||||
<div class="text-sm text-zinc-500 mt-1">
|
||||
{{ __('Registered with this code') }}
|
||||
</div>
|
||||
</div>
|
||||
</flux:field>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex justify-between gap-2">
|
||||
<flux:button type="button" variant="ghost" wire:click="closeEditModal">
|
||||
{{ __('Cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary">
|
||||
{{ __('Save User') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:modal>
|
||||
|
||||
{{-- View Partner Data Modal --}}
|
||||
<flux:modal name="view-partner-modal" :variant="'flyout'" wire:model="showViewModal" class="md:w-2xl">
|
||||
@if($viewUser && $viewUser->partner)
|
||||
@php
|
||||
$partner = $viewUser->partner;
|
||||
$normalizedType = strtolower(str_replace('-', '', $partner->type));
|
||||
$isCustomer = $normalizedType === 'customer';
|
||||
$isBroker = $normalizedType === 'broker' || $normalizedType === 'estateagent';
|
||||
$isRetailer = $normalizedType === 'retailer';
|
||||
$isManufacturer = $normalizedType === 'manufacturer';
|
||||
|
||||
$role = $viewUser->roles->first();
|
||||
$roleIcon = $role?->icon ?? 'shield-check';
|
||||
$roleName = $role?->display_name ?? $role?->name ?? '-';
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">{{ __('Partner-Daten') }}</flux:heading>
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5 text-accent-600 dark:text-accent-400')
|
||||
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $roleName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Benutzer') }}: <strong>{{ $viewUser->name }}</strong>
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Firmeninformationen --}}
|
||||
@if (!$isCustomer)
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Firmeninformationen') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Firmenname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->company_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($isBroker)
|
||||
<div>
|
||||
<flux:label>{{ __('Anzeigename') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->display_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($partner->description)
|
||||
<div>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
{{ $partner->description }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
@endif
|
||||
|
||||
{{-- Persönliche Daten --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Persönliche Daten') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Anrede') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->salutation ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Vorname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->first_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Nachname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->last_name ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Adresse --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Adresse') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="col-span-3">
|
||||
<flux:label>{{ __('Straße') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->street ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Hausnummer') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->house_number ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Postleitzahl') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->zip ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<flux:label>{{ __('Ort') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->city ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Land') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->country ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Kontaktdaten --}}
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Kontaktdaten') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Telefon') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->phone ?: '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!$isCustomer)
|
||||
<div>
|
||||
<flux:label>{{ __('Website') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
@if($partner->website)
|
||||
<a href="{{ $partner->website }}" target="_blank" class="text-accent-600 hover:text-accent-700 dark:text-accent-400 dark:hover:text-accent-300">
|
||||
{{ $partner->website }}
|
||||
</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Liefergebiete für Händler --}}
|
||||
@if ($isRetailer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Liefergebiete') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Lieferradius') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->delivery_radius_km ? $partner->delivery_radius_km . ' km' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Montageradius') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
{{ $partner->assembly_radius_km ? $partner->assembly_radius_km . ' km' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Marke für Hersteller --}}
|
||||
@if ($isManufacturer && $partner->brand)
|
||||
<flux:separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:subheading class="font-semibold text-zinc-900 dark:text-white">
|
||||
{{ __('Markeninformationen') }}
|
||||
</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<flux:label>{{ __('Markenname') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-900 dark:text-white">
|
||||
@if(isset($partner->brand->name))
|
||||
{{ $partner->brand->name }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(isset($partner->brand->description))
|
||||
<div>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
||||
<div class="mt-1 text-sm text-zinc-700 dark:text-zinc-300">
|
||||
{{ $partner->brand->description }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div>
|
||||
<flux:label>{{ __('Status') }}</flux:label>
|
||||
<div class="mt-1">
|
||||
@if(isset($partner->brand->is_active) && $partner->brand->is_active === true)
|
||||
<flux:badge color="green" size="sm" icon="check-circle">{{ __('Aktiv') }}</flux:badge>
|
||||
@else
|
||||
<flux:badge color="zinc" size="sm" icon="x-circle">{{ __('Inaktiv') }}</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Close Button --}}
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="button" variant="primary" wire:click="closeViewModal">
|
||||
{{ __('Schließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('Partner-Daten') }}</flux:heading>
|
||||
<flux:subheading class="mt-2">
|
||||
{{ __('Dieser Benutzer hat keine Partner-Daten') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="button" variant="primary" wire:click="closeViewModal">
|
||||
{{ __('Schließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</flux:modal>
|
||||
|
||||
{{-- Success Message --}}
|
||||
@if (session()->has('message'))
|
||||
<flux:toast :variant="'success'">
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ new class extends Component {
|
|||
public string $roleIcon = 'shield-check';
|
||||
public string $roleColor = 'zinc';
|
||||
public array $rolePermissions = [];
|
||||
public ?string $regPrefix = null;
|
||||
public ?string $regDescription = null;
|
||||
public ?int $regStartNumber = null;
|
||||
|
||||
public function with(): array
|
||||
{
|
||||
return [
|
||||
'roles' => Role::with('permissions')->orderBy('name')->get(),
|
||||
'roles' => Role::with('permissions')->orderBy('id', 'asc')->get(),
|
||||
'permissions' => Permission::with('roles')->orderBy('name')->get(),
|
||||
'allPermissions' => Permission::orderBy('name')->get(),
|
||||
'selectedRole' => $this->selectedRoleId ? Role::find($this->selectedRoleId) : null,
|
||||
|
|
@ -46,6 +49,9 @@ new class extends Component {
|
|||
$this->roleColor = $role->color ?? 'zinc';
|
||||
$this->rolePermissions = $role->permissions->pluck('name')->toArray();
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->regPrefix = $role->reg_prefix;
|
||||
$this->regDescription = $role->reg_description;
|
||||
$this->regStartNumber = $role->reg_start_number;
|
||||
$this->showEditModal = true;
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +64,8 @@ new class extends Component {
|
|||
$this->validate([
|
||||
'roleName' => 'required|string|max:255',
|
||||
'roleColor' => 'required|string|max:50',
|
||||
'regPrefix' => 'nullable|string|size:1',
|
||||
'regStartNumber' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
$role = Role::findOrFail($this->selectedRoleId);
|
||||
|
|
@ -66,12 +74,15 @@ new class extends Component {
|
|||
'display_name' => $this->roleDisplayName,
|
||||
'icon' => $this->roleIcon,
|
||||
'color' => $this->roleColor,
|
||||
'reg_prefix' => $this->regPrefix,
|
||||
'reg_description' => $this->regDescription,
|
||||
'reg_start_number' => $this->regStartNumber,
|
||||
]);
|
||||
|
||||
$role->syncPermissions($this->rolePermissions);
|
||||
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions', 'regPrefix', 'regDescription', 'regStartNumber']);
|
||||
|
||||
session()->flash('message', __('Role updated successfully!'));
|
||||
}
|
||||
|
|
@ -79,7 +90,7 @@ new class extends Component {
|
|||
public function closeEditModal(): void
|
||||
{
|
||||
$this->showEditModal = false;
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions']);
|
||||
$this->reset(['selectedRoleId', 'roleName', 'roleDisplayName', 'roleIcon', 'roleColor', 'rolePermissions', 'regPrefix', 'regDescription', 'regStartNumber']);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
|
|
@ -90,10 +101,10 @@ new class extends Component {
|
|||
<flux:heading size="xl" class="mb-2">{{ __('Permissions & Roles Management') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Manage roles and permissions for your application') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
{{-- <div class="flex gap-2">
|
||||
<flux:button variant="primary" icon="plus">{{ __('Create Role') }}</flux:button>
|
||||
<flux:button variant="ghost" icon="shield-check">{{ __('Create Permission') }}</flux:button>
|
||||
</div>
|
||||
</div> --}}
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
|
|
@ -185,8 +196,7 @@ new class extends Component {
|
|||
|
||||
<flux:table.cell>
|
||||
<div class="flex gap-2">
|
||||
<flux:button size="sm" variant="ghost" icon="eye"
|
||||
tooltip="{{ __('View Details') }}"></flux:button>
|
||||
|
||||
<flux:button size="sm" variant="ghost" icon="pencil"
|
||||
wire:click="openEditModal({{ $role->id }})"
|
||||
tooltip="{{ __('Edit Role') }}"></flux:button>
|
||||
|
|
@ -211,7 +221,7 @@ new class extends Component {
|
|||
</flux:card>
|
||||
|
||||
{{-- Role Statistics --}}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
|
|
@ -236,19 +246,7 @@ new class extends Component {
|
|||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="shadow-elegant">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:subheading>{{ __('Avg. Permissions/Role') }}</flux:subheading>
|
||||
<flux:heading size="2xl" class="mt-2">
|
||||
{{ $roles->count() > 0 ? number_format($roles->sum(fn($r) => $r->permissions->count()) / $roles->count(), 1) : 0 }}
|
||||
</flux:heading>
|
||||
</div>
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-accent-100 dark:bg-accent-900/20">
|
||||
<flux:icon.chart-bar class="h-6 w-6 text-accent-600 dark:text-accent-400" />
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -414,6 +412,34 @@ new class extends Component {
|
|||
</div>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:subheading>{{ __('Registrierungscode-Einstellungen') }}</flux:subheading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Code-Prefix') }}</flux:label>
|
||||
<flux:input wire:model="regPrefix" maxlength="1" placeholder="z.B. K, M, H" />
|
||||
<flux:description>{{ __('Einzelner Buchstabe für Registrierungscodes') }}</flux:description>
|
||||
@error('regPrefix') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Startnummer') }}</flux:label>
|
||||
<flux:input wire:model="regStartNumber" type="number" placeholder="10000000" />
|
||||
<flux:description>{{ __('Startnummer für Code-Generierung') }}</flux:description>
|
||||
@error('regStartNumber') <flux:error>{{ $message }}</flux:error> @enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Beschreibung') }}</flux:label>
|
||||
<flux:input wire:model="regDescription" placeholder="Code-Beschreibung" />
|
||||
<flux:description>{{ __('Optionale Beschreibung') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Permissions') }}</flux:label>
|
||||
<flux:description>{{ __('Select permissions for this role') }}</flux:description>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\RegistrationCode;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
|
@ -13,12 +14,25 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
protected ?RegistrationCode $registrationCode = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadRegistrationCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->loadRegistrationCode();
|
||||
|
||||
if (!$this->registrationCode || !$this->registrationCode->isAvailable()) {
|
||||
$this->addError('registration_code', __('Registrierungscode fehlt oder ist ungültig. Bitte starten Sie über den QR-Link erneut.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:' . User::class],
|
||||
|
|
@ -26,13 +40,36 @@ new #[Layout('components.layouts.auth')] class extends Component {
|
|||
]);
|
||||
|
||||
$validated['password'] = Hash::make($validated['password']);
|
||||
$validated['display_name'] = $this->registrationCode->name ?? null; // Name aus Registrierungscode übernehmen
|
||||
|
||||
event(new Registered(($user = User::create($validated))));
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
// Registrierungscode als verwendet markieren
|
||||
$this->registrationCode->markUsed($user);
|
||||
session()->forget(['registration_code_id', 'registration_role']);
|
||||
|
||||
$this->redirectIntended(route('dashboard', absolute: false), navigate: true);
|
||||
}
|
||||
|
||||
protected function loadRegistrationCode(): void
|
||||
{
|
||||
if ($this->registrationCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$codeId = session('registration_code_id');
|
||||
$role = session('registration_role');
|
||||
|
||||
if (!$codeId || !$role) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registrationCode = RegistrationCode::where('id', $codeId)
|
||||
->where('role', $role)
|
||||
->first();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
|
|
|
|||
299
resources/views/livewire/partner/create-account.blade.php
Normal file
299
resources/views/livewire/partner/create-account.blade.php
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
<?php
|
||||
|
||||
use App\Models\RegistrationCode;
|
||||
use App\Models\Partner;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
new #[Layout('web.layouts.web-master-slot'), Title('Willkommen bei B2In')] class extends Component {
|
||||
public string $firstName = '';
|
||||
public string $lastName = '';
|
||||
public string $email = '';
|
||||
public string $password = '';
|
||||
public string $password_confirmation = '';
|
||||
public bool $acceptTerms = false;
|
||||
|
||||
protected ?RegistrationCode $registrationCode = null;
|
||||
protected ?string $roleKey = null;
|
||||
protected ?string $roleSlug = null;
|
||||
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->loadRegistrationCode();
|
||||
$this->roleSlug = session('registration_slug') ?? null;
|
||||
if (!$this->registrationCode || !$this->roleKey) {
|
||||
session()->flash('message', __('Registrierungscode fehlt oder ist ungültig. Bitte starten Sie erneut über den QR-Link.'));
|
||||
$this->redirect(route('registration.landing', ['role' => $this->roleSlug]), navigate: true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional: E-Mail vorbelegen, wenn im Code gespeichert (meta)
|
||||
if ($this->registrationCode->metadata['email'] ?? false) {
|
||||
$this->email = $this->registrationCode->metadata['email'];
|
||||
}
|
||||
}
|
||||
|
||||
public function createAccount(): void
|
||||
{
|
||||
$this->loadRegistrationCode();
|
||||
|
||||
if (!$this->registrationCode || !$this->roleKey || !$this->registrationCode->isAvailable()) {
|
||||
$this->addError('registration_code', __('Registrierungscode fehlt oder ist ungültig. Bitte starten Sie erneut über den QR-Link.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'firstName' => 'required|string|max:255',
|
||||
'lastName' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'acceptTerms' => 'accepted',
|
||||
], [
|
||||
'firstName.required' => __('Bitte geben Sie Ihren Vornamen ein.'),
|
||||
'lastName.required' => __('Bitte geben Sie Ihren Nachnamen ein.'),
|
||||
'email.required' => __('Bitte geben Sie Ihre E-Mail-Adresse ein.'),
|
||||
'email.email' => __('Bitte geben Sie eine gültige E-Mail-Adresse ein.'),
|
||||
'email.unique' => __('Diese E-Mail-Adresse ist bereits registriert.'),
|
||||
'password.required' => __('Bitte vergeben Sie ein Passwort.'),
|
||||
'password.min' => __('Das Passwort muss mindestens 8 Zeichen haben.'),
|
||||
'password.confirmed' => __('Die Passwörter stimmen nicht überein.'),
|
||||
'acceptTerms.accepted' => __('Bitte akzeptieren Sie die AGB und Datenschutzbestimmungen.'),
|
||||
]);
|
||||
|
||||
try {
|
||||
\DB::beginTransaction();
|
||||
|
||||
// Parent-Partner-ID ermitteln (Makler oder Händler, für Kunden)
|
||||
$parentPartnerId = null;
|
||||
if ($this->registrationCode->assigned_to_code_id) {
|
||||
$assignedCode = RegistrationCode::with('partner')->find($this->registrationCode->assigned_to_code_id);
|
||||
if ($assignedCode && $assignedCode->partner_id) {
|
||||
$parentPartnerId = $assignedCode->partner_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Aktuelle Marke/Theme ermitteln (aus Domain)
|
||||
$brand = config('app.theme', 'b2in');
|
||||
|
||||
// Partner anlegen (minimal, wird im Wizard vervollständigt)
|
||||
$partnerName = __('roles.' . $this->registrationCode->role . ' :code', ['code' => $this->registrationCode->code]);
|
||||
$partner = Partner::create([
|
||||
'company_name' => $partnerName,
|
||||
'slug' => Str::slug($partnerName . '-' . $this->registrationCode->id),
|
||||
'type' => $this->roleKey,
|
||||
'brand' => $brand,
|
||||
'parent_partner_id' => $parentPartnerId,
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
// User anlegen
|
||||
$user = User::create([
|
||||
'partner_id' => $partner->id,
|
||||
'name' => trim($this->firstName . ' ' . $this->lastName),
|
||||
'display_name' => $this->registrationCode->name ?? null, // Name aus Registrierungscode übernehmen
|
||||
'email' => $this->email,
|
||||
'password' => Hash::make($this->password),
|
||||
'email_verified_at' => null, // E-Mail muss verifiziert werden
|
||||
]);
|
||||
|
||||
// Rolle zuweisen
|
||||
if ($role = Role::whereRaw('LOWER(REPLACE(name, "-", "")) = ?', [strtolower($this->roleKey)])->first()) {
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
// Registrierungscode verbrauchen
|
||||
$this->registrationCode->markUsed($user);
|
||||
|
||||
// E-Mail-Verifizierung senden
|
||||
$user->sendEmailVerificationNotification();
|
||||
|
||||
session()->forget(['registration_code_id', 'registration_role', 'registration_slug']);
|
||||
|
||||
\DB::commit();
|
||||
|
||||
// Weiterleitung zur Danke-Seite (User ist NICHT eingeloggt)
|
||||
$this->redirect(route('registration.thank-you', ['email' => $user->email]), navigate: true);
|
||||
} catch (\Exception $e) {
|
||||
\DB::rollBack();
|
||||
$this->addError('email', __('Fehler beim Erstellen des Kontos: ') . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadRegistrationCode(): void
|
||||
{
|
||||
if ($this->registrationCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$codeId = session('registration_code_id');
|
||||
$role = session('registration_role');
|
||||
|
||||
if (!$codeId || !$role) {
|
||||
return;
|
||||
}
|
||||
$this->registrationCode = RegistrationCode::where('id', $codeId)
|
||||
->where('role', $role)
|
||||
->first();
|
||||
|
||||
$this->roleKey = $role;
|
||||
|
||||
// Falls Rolle (z.B. customer) nicht unterstützt, Session leeren
|
||||
if (!$this->roleKey || !$this->registrationCode?->isAvailable()) {
|
||||
session()->forget(['registration_code_id', 'registration_role', 'registration_slug']);
|
||||
$this->registrationCode = null;
|
||||
$this->roleKey = null;
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<div class="max-w-3xl mx-auto px-6 space-y-8 pt-20 pb-40">
|
||||
{{-- Header --}}
|
||||
<div class="text-center space-y-2">
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-accent-500">{{ __('Konto anlegen') }}</p>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-foreground">
|
||||
{{ __('Zugang erstellen & Setup starten') }}
|
||||
</h1>
|
||||
<p class="text-base text-muted-foreground">
|
||||
{{ __('Mit Ihrem Registrierungscode haben Sie den Zugang freigeschaltet. Legen Sie jetzt Ihr persönliches Konto an.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-card/80 backdrop-blur shadow-2xl border border-border rounded-3xl p-6 md:p-8 space-y-6">
|
||||
<div class="flex flex-col gap-3 p-4 rounded-2xl bg-blue-50 border border-blue-100">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-10 w-10 rounded-xl bg-accent-100 dark:bg-accent-900/40 flex items-center justify-center">
|
||||
@svg('heroicon-o-key', 'h-5 w-5 text-accent-600')
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('Code bestätigt') }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ __('Fortschritt: Konto anlegen, danach Setup-Wizard abschließen.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<form wire:submit="createAccount" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">{{ __('Vorname') }}</label>
|
||||
<input
|
||||
wire:model="firstName"
|
||||
type="text"
|
||||
placeholder="{{ __('z.B. Max') }}"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
@error('firstName') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">{{ __('Nachname') }}</label>
|
||||
<input
|
||||
wire:model="lastName"
|
||||
type="text"
|
||||
placeholder="{{ __('z.B. Mustermann') }}"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
@error('lastName') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">{{ __('E-Mail-Adresse') }}</label>
|
||||
<input
|
||||
wire:model="email"
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
@error('email') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">{{ __('Passwort') }}</label>
|
||||
<input
|
||||
wire:model="password"
|
||||
type="password"
|
||||
placeholder="{{ __('Mindestens 8 Zeichen') }}"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
@error('password') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">{{ __('Passwort bestätigen') }}</label>
|
||||
<input
|
||||
wire:model="password_confirmation"
|
||||
type="password"
|
||||
placeholder="{{ __('Passwort wiederholen') }}"
|
||||
class="w-full px-4 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
@error('password_confirmation') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="group relative inline-flex w-8 shrink-0 rounded-full bg-gray-200 p-px inset-ring inset-ring-gray-900/5 outline-offset-2 outline-lime-600 transition-colors duration-200 ease-in-out has-checked:bg-lime-600 has-focus-visible:outline-2">
|
||||
<span class="size-4 rounded-full bg-white shadow-xs ring-1 ring-gray-900/5 transition-transform duration-200 ease-in-out group-has-checked:translate-x-3.5"></span>
|
||||
<input id="acceptTerms" type="checkbox" name="acceptTerms" aria-label="Agree to policies" class="absolute inset-0 appearance-none focus:outline-hidden" wire:model="acceptTerms" />
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ __('Ich akzeptiere die') }}
|
||||
<a href="#" class="text-accent-600 hover:text-accent-900">{{ __('AGB') }}</a>
|
||||
{{ __('und') }}
|
||||
<a href="#" class="text-accent-600 hover:text-accent-900">{{ __('Datenschutzbestimmungen') }}</a>.
|
||||
</div>
|
||||
</div>
|
||||
@error('acceptTerms') <div class="text-sm text-red-600 mt-1">{{ $message }}</div> @enderror
|
||||
</div>
|
||||
|
||||
<x-error-alert-static light />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn-secondary-accent small cursor-pointer" wire:target="createAccount">
|
||||
<span class="flex items-center gap-2">
|
||||
@svg('heroicon-o-arrow-right', 'h-5 w-5')
|
||||
{{ __('Konto erstellen & Setup starten') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 p-4 rounded-2xl bg-lime-50 border border-lime-200">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="h-10 w-10 rounded-xl bg-lime-100 flex items-center justify-center flex-shrink-0">
|
||||
@svg('heroicon-o-arrow-right-circle', 'h-5 w-5 text-lime-600')
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('Verifizieren Sie Ihre E-Mail-Adresse') }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ __('Nach erfolgreicher Registrierung erhalten Sie einen Verifizierungslink per E-Mail. Bitte klicken Sie auf den Link, um Ihre E-Mail-Adresse zu verifizieren.') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
{{ __('Probleme? Bitte wenden Sie sich an Ihren Ansprechpartner oder den Support.') }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<livewire:web.components.ui.footer />
|
||||
</div>
|
||||
441
resources/views/livewire/partner/my-data.blade.php
Normal file
441
resources/views/livewire/partner/my-data.blade.php
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Partner;
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.app'), Title('Meine Daten')] class extends Component {
|
||||
public Partner $partner;
|
||||
public string $partnerType;
|
||||
|
||||
// Stammdaten
|
||||
public string $companyName = '';
|
||||
public string $displayName = '';
|
||||
public string $salutation = '';
|
||||
public string $firstName = '';
|
||||
public string $lastName = '';
|
||||
public string $description = '';
|
||||
public string $street = '';
|
||||
public string $houseNumber = '';
|
||||
public string $zip = '';
|
||||
public string $city = '';
|
||||
public string $country = 'Deutschland';
|
||||
public string $phone = '';
|
||||
public string $website = '';
|
||||
|
||||
// Retailer - Liefergebiete
|
||||
public ?int $deliveryRadius = null;
|
||||
public ?int $assemblyRadius = null;
|
||||
|
||||
// Manufacturer - Marke
|
||||
public string $brandName = '';
|
||||
public string $brandDescription = '';
|
||||
|
||||
public string $roleIcon = 'shield-check';
|
||||
public string $roleName = '-';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user->partner_id) {
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
return;
|
||||
}
|
||||
|
||||
$role = $user->roles->first();
|
||||
if ($role) {
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->roleName = $role->display_name ?? $role->name;
|
||||
}
|
||||
|
||||
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
|
||||
$this->partnerType = $this->partner->type;
|
||||
|
||||
// Vorausfüllen: Partner-Daten
|
||||
$this->companyName = $this->partner->company_name ?? '';
|
||||
$this->displayName = $this->partner->display_name ?? '';
|
||||
$this->salutation = $this->partner->salutation ?? '';
|
||||
$this->firstName = $this->partner->first_name ?? '';
|
||||
$this->lastName = $this->partner->last_name ?? '';
|
||||
$this->description = $this->partner->description ?? '';
|
||||
$this->street = $this->partner->street ?? '';
|
||||
$this->houseNumber = $this->partner->house_number ?? '';
|
||||
$this->zip = $this->partner->zip ?? '';
|
||||
$this->city = $this->partner->city ?? '';
|
||||
$this->country = $this->partner->country ?? 'Deutschland';
|
||||
$this->phone = $this->partner->phone ?? '';
|
||||
$this->website = $this->partner->website ?? '';
|
||||
$this->deliveryRadius = $this->partner->delivery_radius_km;
|
||||
$this->assemblyRadius = $this->partner->assembly_radius_km;
|
||||
|
||||
// Ersetze Platzhalter wie "roles.broker M10000004" durch Übersetzung + ID
|
||||
if(strpos($this->companyName, 'roles.') !== false) {
|
||||
$parts = explode(' ', $this->companyName, 2);
|
||||
$translatedRole = isset($parts[0]) ? __($parts[0]) : $this->companyName;
|
||||
$partnerId = isset($parts[1]) ? ' ' . $parts[1] : '';
|
||||
$this->companyName = $translatedRole . $partnerId;
|
||||
}
|
||||
|
||||
// Namen aus User übernehmen, falls Partner-Felder leer sind
|
||||
if (empty($this->firstName) && empty($this->lastName)) {
|
||||
$nameParts = explode(' ', $user->name, 2);
|
||||
$this->firstName = $nameParts[0] ?? '';
|
||||
$this->lastName = $nameParts[1] ?? '';
|
||||
}
|
||||
|
||||
// Marke laden für Manufacturer
|
||||
if (strtolower(str_replace('-', '', $this->partnerType)) === 'manufacturer') {
|
||||
$brand = Brand::where('partner_id', $this->partner->id)->first();
|
||||
if ($brand) {
|
||||
$this->brandName = $brand->name;
|
||||
$this->brandDescription = $brand->description ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function saveData(): void
|
||||
{
|
||||
$normalizedType = strtolower(str_replace('-', '', $this->partnerType));
|
||||
$isCustomer = $normalizedType === 'customer';
|
||||
$isBroker = $normalizedType === 'broker' || $normalizedType === 'estateagent';
|
||||
$isRetailer = $normalizedType === 'retailer';
|
||||
$isManufacturer = $normalizedType === 'manufacturer';
|
||||
|
||||
$rules = [
|
||||
'salutation' => 'required|in:Herr,Frau,Divers',
|
||||
'firstName' => 'required|string|max:255',
|
||||
'lastName' => 'required|string|max:255',
|
||||
'street' => 'required|string|max:255',
|
||||
'houseNumber' => 'required|string|max:20',
|
||||
'zip' => 'required|string|max:10',
|
||||
'city' => 'required|string|max:255',
|
||||
'country' => 'required|string|max:255',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
];
|
||||
|
||||
if (!$isCustomer) {
|
||||
$rules['companyName'] = 'required|string|max:255';
|
||||
$rules['description'] = 'nullable|string|max:1000';
|
||||
$rules['website'] = 'nullable|url|max:255';
|
||||
}
|
||||
|
||||
if ($isBroker) {
|
||||
$rules['displayName'] = 'required|string|max:255';
|
||||
}
|
||||
|
||||
if ($isRetailer) {
|
||||
$rules['deliveryRadius'] = 'required|integer|min:1|max:500';
|
||||
$rules['assemblyRadius'] = 'required|integer|min:1|max:500';
|
||||
}
|
||||
|
||||
if ($isManufacturer) {
|
||||
$rules['brandName'] = 'required|string|max:255';
|
||||
$rules['brandDescription'] = 'nullable|string|max:1000';
|
||||
}
|
||||
|
||||
$this->validate($rules, [
|
||||
'salutation.required' => __('Bitte wählen Sie eine Anrede.'),
|
||||
'firstName.required' => __('Bitte geben Sie einen Vornamen ein.'),
|
||||
'lastName.required' => __('Bitte geben Sie einen Nachnamen ein.'),
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'displayName.required' => __('Bitte geben Sie einen Anzeigenamen ein.'),
|
||||
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
||||
'houseNumber.required' => __('Bitte geben Sie eine Hausnummer ein.'),
|
||||
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
||||
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
||||
'country.required' => __('Bitte wählen Sie ein Land.'),
|
||||
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
||||
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
|
||||
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
|
||||
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
|
||||
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
|
||||
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
|
||||
]);
|
||||
|
||||
// Update Partner
|
||||
$updateData = [
|
||||
'salutation' => $this->salutation,
|
||||
'first_name' => $this->firstName,
|
||||
'last_name' => $this->lastName,
|
||||
'street' => $this->street,
|
||||
'house_number' => $this->houseNumber,
|
||||
'zip' => $this->zip,
|
||||
'city' => $this->city,
|
||||
'country' => $this->country,
|
||||
'phone' => $this->phone,
|
||||
];
|
||||
|
||||
if (!$isCustomer) {
|
||||
$updateData['company_name'] = $this->companyName;
|
||||
$updateData['description'] = $this->description;
|
||||
$updateData['website'] = $this->website;
|
||||
}
|
||||
|
||||
if ($isBroker) {
|
||||
$updateData['display_name'] = $this->displayName;
|
||||
}
|
||||
|
||||
if ($isRetailer) {
|
||||
$updateData['delivery_radius_km'] = $this->deliveryRadius;
|
||||
$updateData['assembly_radius_km'] = $this->assemblyRadius;
|
||||
}
|
||||
|
||||
$this->partner->update($updateData);
|
||||
|
||||
// Für Manufacturer: Marke aktualisieren oder erstellen
|
||||
if ($isManufacturer) {
|
||||
Brand::updateOrCreate(
|
||||
['partner_id' => $this->partner->id],
|
||||
[
|
||||
'name' => $this->brandName,
|
||||
'slug' => Str::slug($this->brandName),
|
||||
'description' => $this->brandDescription,
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
session()->flash('message', __('Ihre Daten wurden erfolgreich aktualisiert.'));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Meine Daten') }}</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie Ihre Firmendaten und Kontaktinformationen') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-6 h-6 text-accent-600 dark:text-accent-400')
|
||||
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ $roleName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Success Message --}}
|
||||
<x-success-alert />
|
||||
|
||||
{{-- Formular --}}
|
||||
<flux:card>
|
||||
<form wire:submit="saveData" class="space-y-6">
|
||||
@php
|
||||
$isCustomer = strtolower(str_replace('-', '', $partnerType)) === 'customer';
|
||||
$isRetailer = strtolower(str_replace('-', '', $partnerType)) === 'retailer';
|
||||
$isManufacturer = strtolower(str_replace('-', '', $partnerType)) === 'manufacturer';
|
||||
$isBroker = strtolower(str_replace('-', '', $partnerType)) === 'broker' || strtolower(str_replace('-', '', $partnerType)) === 'estateagent';
|
||||
@endphp
|
||||
|
||||
{{-- Firmenname (nur für Nicht-Kunden) --}}
|
||||
@if (!$isCustomer)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="companyName" name="companyName" icon="building-office" placeholder="{{ __('z.B. Müller GmbH') }}" />
|
||||
@error('companyName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
@if ($isBroker)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzeigename') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Der Name, der Ihren Kunden angezeigt wird') }}</flux:description>
|
||||
<flux:input wire:model="displayName" name="displayName" icon="user" placeholder="{{ __('z.B. Max Schmidt') }}" />
|
||||
@error('displayName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }} ({{ __('optional') }})</flux:label>
|
||||
<flux:textarea wire:model="description" name="description" rows="3" placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
||||
@error('description')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
@endif
|
||||
|
||||
{{-- Persönliche Daten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anrede') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="salutation" name="salutation">
|
||||
<flux:select.option value="">{{ __('Bitte wählen') }}</flux:select.option>
|
||||
<flux:select.option value="Herr">{{ __('Herr') }}</flux:select.option>
|
||||
<flux:select.option value="Frau">{{ __('Frau') }}</flux:select.option>
|
||||
<flux:select.option value="Divers">{{ __('Divers') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('salutation')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Vorname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="firstName" name="firstName" icon="user" />
|
||||
@error('firstName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nachname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="lastName" name="lastName" icon="user" />
|
||||
@error('lastName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Adresse --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<flux:field class="md:col-span-3">
|
||||
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="street" name="street" icon="map-pin" placeholder="{{ __('Musterstraße') }}" />
|
||||
@error('street')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Hausnummer') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="houseNumber" name="houseNumber" placeholder="{{ __('123a') }}" />
|
||||
@error('houseNumber')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="zip" name="zip" icon="map" placeholder="{{ __('12345') }}" />
|
||||
@error('zip')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field class="md:col-span-2">
|
||||
<flux:label>{{ __('Ort') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="city" name="city" icon="building-office" placeholder="{{ __('Musterstadt') }}" />
|
||||
@error('city')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="country" name="country">
|
||||
<flux:select.option value="Deutschland">{{ __('Deutschland') }}</flux:select.option>
|
||||
<flux:select.option value="Österreich">{{ __('Österreich') }}</flux:select.option>
|
||||
<flux:select.option value="Schweiz">{{ __('Schweiz') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('country')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Telefon') }}</flux:label>
|
||||
<flux:input wire:model="phone" name="phone" type="tel" icon="phone" placeholder="{{ __('optional') }}" />
|
||||
@error('phone')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
@if (!$isCustomer)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Website') }}</flux:label>
|
||||
<flux:input wire:model="website" name="website" type="url" icon="globe-alt" placeholder="https://..." />
|
||||
@error('website')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
{{-- Liefergebiete für Händler (Retailer) --}}
|
||||
@if ($isRetailer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200 font-medium mb-1">
|
||||
{{ __('Liefergebiete') }}
|
||||
</p>
|
||||
<p class="text-xs text-blue-600 dark:text-blue-300">
|
||||
{{ __('Definieren Sie, in welchem Umkreis Sie liefern und montieren können.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferradius (km)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
||||
<flux:input wire:model="deliveryRadius" name="deliveryRadius" type="number" min="1" max="500" placeholder="z.B. 50" />
|
||||
@error('deliveryRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageradius (km)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
||||
<flux:input wire:model="assemblyRadius" name="assemblyRadius" type="number" min="1" max="500" placeholder="z.B. 30" />
|
||||
@error('assemblyRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Marke für Hersteller (Manufacturer) --}}
|
||||
@if ($isManufacturer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg">
|
||||
<p class="text-sm text-purple-800 dark:text-purple-200 font-medium mb-1">
|
||||
{{ __('Ihre Marke') }}
|
||||
</p>
|
||||
<p class="text-xs text-purple-600 dark:text-purple-300">
|
||||
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="brandName" name="brandName" icon="tag" placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
||||
@error('brandName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }} ({{ __('optional') }})</flux:label>
|
||||
<flux:textarea wire:model="brandDescription" name="brandDescription" rows="4" placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
||||
@error('brandDescription')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle">
|
||||
{{ __('Änderungen speichern') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Models\Partner;
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\RegistrationCode;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -20,12 +21,19 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
// Schritt 1: Stammdaten (alle Rollen)
|
||||
public string $companyName = '';
|
||||
public $logo = null;
|
||||
public string $displayName = '';
|
||||
public string $salutation = '';
|
||||
public string $firstName = '';
|
||||
public string $lastName = '';
|
||||
public string $description = '';
|
||||
public string $street = '';
|
||||
public string $houseNumber = '';
|
||||
public string $zip = '';
|
||||
public string $city = '';
|
||||
public string $country = 'Deutschland';
|
||||
public string $phone = '';
|
||||
public string $website = '';
|
||||
public $logo = null;
|
||||
|
||||
// Schritt 2: Retailer - Liefergebiete
|
||||
public ?int $deliveryRadius = null;
|
||||
|
|
@ -38,6 +46,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
public string $roleIcon = 'shield-check';
|
||||
public string $roleName = '-';
|
||||
protected ?RegistrationCode $registrationCode = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
|
|
@ -55,79 +64,162 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
}
|
||||
|
||||
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
|
||||
|
||||
// Wenn Setup bereits abgeschlossen, direkt zum Dashboard
|
||||
if ($this->partner->setup_completed) {
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->partnerType = $this->partner->type;
|
||||
|
||||
// Vorausfüllen
|
||||
$this->companyName = $this->partner->company_name;
|
||||
// Vorausfüllen: Partner-Daten
|
||||
$this->companyName = $this->partner->company_name ?? '';
|
||||
$this->displayName = $this->partner->display_name ?? '';
|
||||
$this->salutation = $this->partner->salutation ?? '';
|
||||
$this->firstName = $this->partner->first_name ?? '';
|
||||
$this->lastName = $this->partner->last_name ?? '';
|
||||
$this->description = $this->partner->description ?? '';
|
||||
$this->website = '';
|
||||
$this->street = $this->partner->street ?? '';
|
||||
$this->houseNumber = $this->partner->house_number ?? '';
|
||||
$this->zip = $this->partner->zip ?? '';
|
||||
$this->city = $this->partner->city ?? '';
|
||||
$this->country = $this->partner->country ?? 'Deutschland';
|
||||
$this->phone = $this->partner->phone ?? '';
|
||||
$this->website = $this->partner->website ?? '';
|
||||
$this->deliveryRadius = $this->partner->delivery_radius_km;
|
||||
$this->assemblyRadius = $this->partner->assembly_radius_km;
|
||||
|
||||
// Ersetze Platzhalter wie "roles.broker M10000004" durch Übersetzung + ID
|
||||
if(strpos($this->companyName, 'roles.') !== false) {
|
||||
$parts = explode(' ', $this->companyName, 2);
|
||||
$translatedRole = isset($parts[0]) ? __($parts[0]) : $this->companyName;
|
||||
$partnerId = isset($parts[1]) ? ' ' . $parts[1] : '';
|
||||
$this->companyName = $translatedRole . $partnerId;
|
||||
}
|
||||
|
||||
// Namen aus User übernehmen, falls Partner-Felder leer sind
|
||||
if (empty($this->firstName) && empty($this->lastName)) {
|
||||
$nameParts = explode(' ', $user->name, 2);
|
||||
$this->firstName = $nameParts[0] ?? '';
|
||||
$this->lastName = $nameParts[1] ?? '';
|
||||
}
|
||||
// Definiere Schritte basierend auf Rolle
|
||||
$this->defineSteps();
|
||||
}
|
||||
|
||||
protected function defineSteps(): void
|
||||
{
|
||||
switch ($this->partnerType) {
|
||||
case 'Retailer':
|
||||
$this->steps = ['Stammdaten', 'Liefergebiete', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'Manufacturer':
|
||||
$this->steps = ['Stammdaten', 'Marke anlegen', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'Estate-Agent':
|
||||
$this->steps = ['Profil', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
default:
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
}
|
||||
// Alle Rollen haben nur noch 2 Schritte: Stammdaten + Fertig
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
}
|
||||
|
||||
public function saveStep1(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'companyName' => 'required|string|max:255',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'street' => 'required|string|max:255',
|
||||
'zip' => 'required|string|max:10',
|
||||
'city' => 'required|string|max:255',
|
||||
'website' => 'nullable|url|max:255',
|
||||
'logo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
],
|
||||
[
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
||||
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
||||
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
||||
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
||||
'logo.image' => __('Das Logo muss eine Bilddatei sein.'),
|
||||
'logo.mimes' => __('Das Logo muss im Format JPG, PNG oder WebP sein.'),
|
||||
'logo.max' => __('Das Logo darf maximal 2 MB groß sein.'),
|
||||
],
|
||||
);
|
||||
$normalizedType = strtolower(str_replace('-', '', $this->partnerType));
|
||||
$isCustomer = $normalizedType === 'customer';
|
||||
$isBroker = $normalizedType === 'broker' || $normalizedType === 'estateagent';
|
||||
$isRetailer = $normalizedType === 'retailer';
|
||||
$isManufacturer = $normalizedType === 'manufacturer';
|
||||
|
||||
// Speichere Logo falls hochgeladen
|
||||
$logoPath = null;
|
||||
if ($this->logo) {
|
||||
$logoPath = $this->logo->store('partner-logos', 'public');
|
||||
$rules = [
|
||||
'salutation' => 'required|in:Herr,Frau,Divers',
|
||||
'firstName' => 'required|string|max:255',
|
||||
'lastName' => 'required|string|max:255',
|
||||
'street' => 'required|string|max:255',
|
||||
'houseNumber' => 'required|string|max:20',
|
||||
'zip' => 'required|string|max:10',
|
||||
'city' => 'required|string|max:255',
|
||||
'country' => 'required|string|max:255',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
];
|
||||
|
||||
if (!$isCustomer) {
|
||||
$rules['companyName'] = 'required|string|max:255';
|
||||
$rules['description'] = 'nullable|string|max:1000';
|
||||
$rules['website'] = 'nullable|url|max:255';
|
||||
}
|
||||
|
||||
// Update Partner
|
||||
$this->partner->update([
|
||||
'company_name' => $this->companyName,
|
||||
'description' => $this->description,
|
||||
'logo_url' => $logoPath ?? $this->partner->logo_url,
|
||||
// Für Broker ist displayName Pflicht
|
||||
if ($isBroker) {
|
||||
$rules['displayName'] = 'required|string|max:255';
|
||||
}
|
||||
|
||||
// Für Retailer sind Liefergebiete Pflicht
|
||||
if ($isRetailer) {
|
||||
$rules['deliveryRadius'] = 'required|integer|min:1|max:500';
|
||||
$rules['assemblyRadius'] = 'required|integer|min:1|max:500';
|
||||
}
|
||||
|
||||
// Für Manufacturer ist Markenname Pflicht
|
||||
if ($isManufacturer) {
|
||||
$rules['brandName'] = 'required|string|max:255';
|
||||
$rules['brandDescription'] = 'nullable|string|max:1000';
|
||||
}
|
||||
|
||||
$this->validate($rules, [
|
||||
'salutation.required' => __('Bitte wählen Sie eine Anrede.'),
|
||||
'firstName.required' => __('Bitte geben Sie einen Vornamen ein.'),
|
||||
'lastName.required' => __('Bitte geben Sie einen Nachnamen ein.'),
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'displayName.required' => __('Bitte geben Sie einen Anzeigenamen ein.'),
|
||||
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
||||
'houseNumber.required' => __('Bitte geben Sie eine Hausnummer ein.'),
|
||||
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
||||
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
||||
'country.required' => __('Bitte wählen Sie ein Land.'),
|
||||
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
||||
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
|
||||
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
|
||||
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
|
||||
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
|
||||
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
|
||||
]);
|
||||
|
||||
// TODO: Adresse speichern (separates Address-Model oder JSON-Feld)
|
||||
// Update Partner
|
||||
$updateData = [
|
||||
'salutation' => $this->salutation,
|
||||
'first_name' => $this->firstName,
|
||||
'last_name' => $this->lastName,
|
||||
'street' => $this->street,
|
||||
'house_number' => $this->houseNumber,
|
||||
'zip' => $this->zip,
|
||||
'city' => $this->city,
|
||||
'country' => $this->country,
|
||||
'phone' => $this->phone,
|
||||
];
|
||||
|
||||
$this->currentStep = 2;
|
||||
if (!$isCustomer) {
|
||||
$updateData['company_name'] = $this->companyName;
|
||||
$updateData['description'] = $this->description;
|
||||
$updateData['website'] = $this->website;
|
||||
}
|
||||
|
||||
if ($isBroker) {
|
||||
$updateData['display_name'] = $this->displayName;
|
||||
}
|
||||
|
||||
if ($isRetailer) {
|
||||
$updateData['delivery_radius_km'] = $this->deliveryRadius;
|
||||
$updateData['assembly_radius_km'] = $this->assemblyRadius;
|
||||
}
|
||||
|
||||
$this->partner->update($updateData);
|
||||
|
||||
// Für Manufacturer: Marke erstellen
|
||||
if ($isManufacturer) {
|
||||
Brand::create([
|
||||
'partner_id' => $this->partner->id,
|
||||
'name' => $this->brandName,
|
||||
'slug' => Str::slug($this->brandName),
|
||||
'description' => $this->brandDescription,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Alle Rollen: Setup abschließen
|
||||
$this->completeSetup();
|
||||
}
|
||||
|
||||
public function saveStep2Retailer(): void
|
||||
|
|
@ -188,6 +280,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
$this->completeSetup();
|
||||
}
|
||||
|
||||
|
||||
protected function completeSetup(): void
|
||||
{
|
||||
$this->partner->update([
|
||||
|
|
@ -196,6 +289,12 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
'setup_completed_at' => now(),
|
||||
]);
|
||||
|
||||
// Markiere Registrierungscode als verbraucht, falls vorhanden
|
||||
if ($this->registrationCode && $this->registrationCode->isAvailable()) {
|
||||
$this->registrationCode->markUsed(Auth::user());
|
||||
session()->forget(['registration_code_id', 'registration_role']);
|
||||
}
|
||||
|
||||
$this->currentStep = $this->totalSteps;
|
||||
}
|
||||
|
||||
|
|
@ -204,14 +303,35 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
$this->redirect(route('dashboard'), navigate: true);
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
public function handleLogout(): void
|
||||
{
|
||||
Auth::logout();
|
||||
|
||||
request()->session()->invalidate();
|
||||
request()->session()->regenerateToken();
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$this->redirect(route('home'), navigate: true);
|
||||
session()->invalidate();
|
||||
session()->regenerateToken();
|
||||
|
||||
$this->redirect(route('home'), navigate: false);
|
||||
}
|
||||
|
||||
protected function loadRegistrationCode(): void
|
||||
{
|
||||
$codeId = session('registration_code_id');
|
||||
$role = session('registration_role');
|
||||
|
||||
if (!$codeId || !$role) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registrationCode = RegistrationCode::where('id', $codeId)
|
||||
->where('role', $role)
|
||||
->first();
|
||||
|
||||
if (!$this->registrationCode || !$this->registrationCode->isAvailable()) {
|
||||
// Ungültig/verbraucht -> Session aufräumen
|
||||
session()->forget(['registration_code_id', 'registration_role']);
|
||||
$this->registrationCode = null;
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
|
|
@ -222,6 +342,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
|
||||
{{ __('Vervollständigen Sie Ihr Profil') }}
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Progress Indicator --}}
|
||||
|
|
@ -231,20 +352,15 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<flux:card class="shadow-2xl">
|
||||
{{-- Step 1: Stammdaten (für alle Rollen) --}}
|
||||
@if ($currentStep === 1)
|
||||
<form wire:submit="saveStep1" class="space-y-6">
|
||||
@php
|
||||
$isCustomer = strtolower(str_replace('-', '', $partnerType)) === 'customer';
|
||||
@endphp
|
||||
<form wire:submit.prevent="saveStep1" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5')
|
||||
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Ihre Stammdaten') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Ihre Stammdaten') }}
|
||||
@else
|
||||
{{ __('Ihr Profil') }}
|
||||
@endif
|
||||
/ {{ $roleName }}
|
||||
{{ __('Ihre Stammdaten') }} / {{ $roleName }}
|
||||
</div>
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
|
|
@ -254,117 +370,221 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="companyName" icon="building-office" />
|
||||
@error('companyName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
{{-- Firmenname (nur für Nicht-Kunden) --}}
|
||||
@if (!$isCustomer)
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Logo (optional)') }}</flux:label>
|
||||
<flux:description>{{ __('Laden Sie Ihr Firmenlogo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input type="file" wire:model.live="logo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
||||
|
||||
@error('logo')
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="companyName" name="companyName" icon="building-office" placeholder="{{ __('z.B. Müller GmbH') }}" />
|
||||
@error('companyName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<div wire:loading wire:target="logo" class="text-sm text-accent-600 dark:text-accent-400">
|
||||
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
||||
{{ __('Wird hochgeladen...') }}
|
||||
</div>
|
||||
@php
|
||||
$isBroker = strtolower(str_replace('-', '', $partnerType)) === 'broker' || strtolower(str_replace('-', '', $partnerType)) === 'estateagent';
|
||||
@endphp
|
||||
|
||||
@if ($logo)
|
||||
<div wire:loading.remove wire:target="logo">
|
||||
@try
|
||||
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="{{ $logo->temporaryUrl() }}" class="h-16 w-16 object-contain rounded border"
|
||||
alt="Logo Preview">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400">{{ $logo->getClientOriginalName() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@catch(\Exception $e)
|
||||
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
||||
</div>
|
||||
@endtry
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:field>
|
||||
@if ($isBroker)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzeigename') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Der Name, der Ihren Kunden angezeigt wird (kann vom Firmennamen abweichen)') }}</flux:description>
|
||||
<flux:input wire:model="displayName" name="displayName" icon="user" placeholder="{{ __('z.B. Max Schmidt oder Immobilien Schmidt') }}" />
|
||||
@error('displayName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="description" rows="4"
|
||||
placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
||||
@error('description')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }} ({{ __('optional') }})</flux:label>
|
||||
<flux:textarea wire:model="description" name="description" rows="3" placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
||||
@error('description')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
@endif
|
||||
|
||||
{{-- Persönliche Daten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anrede') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="salutation" name="salutation">
|
||||
<flux:select.option value="">{{ __('Bitte wählen') }}</flux:select.option>
|
||||
<flux:select.option value="Herr">{{ __('Herr') }}</flux:select.option>
|
||||
<flux:select.option value="Frau">{{ __('Frau') }}</flux:select.option>
|
||||
<flux:select.option value="Divers">{{ __('Divers') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('salutation')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Vorname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="firstName" name="firstName" icon="user" />
|
||||
@error('firstName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nachname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="lastName" name="lastName" icon="user" />
|
||||
@error('lastName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field class="md:col-span-2">
|
||||
{{-- Adresse --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<flux:field class="md:col-span-3">
|
||||
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="street" wire:/>
|
||||
<flux:input wire:model="street" name="street" icon="map-pin" placeholder="{{ __('Musterstraße') }}" />
|
||||
@error('street')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('PLZ') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="zip" />
|
||||
<flux:label>{{ __('Hausnummer') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="houseNumber" name="houseNumber" placeholder="{{ __('123a') }}" />
|
||||
@error('houseNumber')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="zip" name="zip" icon="map" placeholder="{{ __('12345') }}" />
|
||||
@error('zip')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field class="md:col-span-2">
|
||||
<flux:label>{{ __('Ort') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="city" name="city" icon="building-office" placeholder="{{ __('Musterstadt') }}" />
|
||||
@error('city')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stadt') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="city" icon="map-pin" />
|
||||
@error('city')
|
||||
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="country" name="country">
|
||||
<flux:select.option value="Deutschland">{{ __('Deutschland') }}</flux:select.option>
|
||||
<flux:select.option value="Österreich">{{ __('Österreich') }}</flux:select.option>
|
||||
<flux:select.option value="Schweiz">{{ __('Schweiz') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('country')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Website (optional)') }}</flux:label>
|
||||
<flux:input wire:model="website" type="url" icon="globe-alt" placeholder="https://..." />
|
||||
@error('website')
|
||||
<flux:label>{{ __('Telefon') }}</flux:label>
|
||||
<flux:input wire:model="phone" name="phone" type="tel" icon="phone" placeholder="{{ __('optional') }}" />
|
||||
@error('phone')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
@if (!$isCustomer)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Website') }}</flux:label>
|
||||
<flux:input wire:model="website" name="website" type="url" icon="globe-alt" placeholder="https://..." />
|
||||
@error('website')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
{{-- Liefergebiete für Händler (Retailer) --}}
|
||||
@php
|
||||
$isRetailer = strtolower(str_replace('-', '', $partnerType)) === 'retailer';
|
||||
@endphp
|
||||
@if ($isRetailer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200 font-medium mb-1">
|
||||
{{ __('Liefergebiete') }}
|
||||
</p>
|
||||
<p class="text-xs text-blue-600 dark:text-blue-300">
|
||||
{{ __('Definieren Sie, in welchem Umkreis Sie liefern und montieren können.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferradius (km)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
||||
<flux:input wire:model="deliveryRadius" name="deliveryRadius" type="number" min="1" max="500" placeholder="z.B. 50" />
|
||||
@error('deliveryRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageradius (km)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
||||
<flux:input wire:model="assemblyRadius" name="assemblyRadius" type="number" min="1" max="500" placeholder="z.B. 30" />
|
||||
@error('assemblyRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Marke für Hersteller (Manufacturer) --}}
|
||||
@php
|
||||
$isManufacturer = strtolower(str_replace('-', '', $partnerType)) === 'manufacturer';
|
||||
@endphp
|
||||
@if ($isManufacturer)
|
||||
<flux:separator />
|
||||
|
||||
<div class="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg">
|
||||
<p class="text-sm text-purple-800 dark:text-purple-200 font-medium mb-1">
|
||||
{{ __('Ihre Marke') }}
|
||||
</p>
|
||||
<p class="text-xs text-purple-600 dark:text-purple-300">
|
||||
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. Sie können später weitere Marken hinzufügen.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="brandName" name="brandName" icon="tag" placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
||||
@error('brandName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }} ({{ __('optional') }})</flux:label>
|
||||
<flux:textarea wire:model="brandDescription" name="brandDescription" rows="4" placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
||||
@error('brandDescription')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-between">
|
||||
|
||||
<flux:button type="button" variant="outline" icon="arrow-left-start-on-rectangle"
|
||||
wire:click="logout">
|
||||
{{ __('Logout') }}
|
||||
</flux:button>
|
||||
<flux:button type="submit" variant="primary" icon="arrow-right">
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Weiter zu Liefergebiete') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Weiter zu Marke') }}
|
||||
@else
|
||||
{{ __('Setup abschließen') }}
|
||||
@endif
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle" class="cursor-pointer" wire:target="saveStep1">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -372,7 +592,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
{{-- Step 2: Retailer - Liefergebiete --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Retailer')
|
||||
<form wire:submit="saveStep2Retailer" class="space-y-6">
|
||||
<form wire:submit.prevent="saveStep2Retailer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
🚚 {{ __('Ihre Liefergebiete') }}
|
||||
|
|
@ -388,7 +608,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<flux:label>{{ __('Lieferradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="deliveryRadius" type="number" min="1" max="500"
|
||||
<flux:input wire:model="deliveryRadius" name="deliveryRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
|
|
@ -401,7 +621,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<flux:label>{{ __('Montageradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="assemblyRadius" type="number" min="1" max="500"
|
||||
<flux:input wire:model="assemblyRadius" name="assemblyRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
|
|
@ -415,7 +635,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
|
@ -424,7 +644,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
{{-- Step 2: Manufacturer - Marke anlegen --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Manufacturer')
|
||||
<form wire:submit="saveStep2Manufacturer" class="space-y-6">
|
||||
<form wire:submit.prevent="saveStep2Manufacturer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
™️ {{ __('Ihre Marke') }}
|
||||
|
|
@ -438,7 +658,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="brandName" icon="tag"
|
||||
<flux:input wire:model="brandName" name="brandName" icon="tag"
|
||||
placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
||||
@error('brandName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
|
|
@ -450,7 +670,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<flux:description>{{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input type="file" wire:model.live="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
<input type="file" wire:model.live="brandLogo" name="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
||||
|
||||
@error('brandLogo')
|
||||
|
|
@ -487,7 +707,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="brandDescription" rows="4"
|
||||
<flux:textarea wire:model="brandDescription" name="brandDescription" rows="4"
|
||||
placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
||||
@error('brandDescription')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
|
|
@ -499,7 +719,7 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
|
@ -508,17 +728,52 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
|
||||
{{-- Final Step: Fertig! --}}
|
||||
@if ($currentStep === $totalSteps)
|
||||
<div class="text-center space-y-6 py-8">
|
||||
<div class="text-center space-y-6 py-8" id="success-step"
|
||||
x-data
|
||||
x-init="
|
||||
setTimeout(function() {
|
||||
let script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js';
|
||||
script.onload = function() {
|
||||
|
||||
let duration = 3 * 1000;
|
||||
let end = Date.now() + duration;
|
||||
|
||||
(function frame() {
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: { x: 0 },
|
||||
colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
});
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: { x: 1 },
|
||||
colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
});
|
||||
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
}());
|
||||
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}, 200);
|
||||
">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20 animate-bounce">
|
||||
<flux:icon.check-circle class="h-12 w-12 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">
|
||||
✅ {{ __('Einrichtung abgeschlossen!') }}
|
||||
✅ {{ __('Einrichtung abgeschlossen!') }}1
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
@if ($partnerType === 'Retailer')
|
||||
|
|
@ -534,16 +789,27 @@ new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends C
|
|||
<flux:separator />
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
@if ($partnerType !== 'Estate-Agent')
|
||||
<flux:button variant="primary" icon="plus" size="lg">
|
||||
{{ __('Erstes Produkt anlegen') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
<flux:button wire:click="goToDashboard" variant="outline" icon="home" size="lg">
|
||||
|
||||
<button type="button" wire:click="goToDashboard"
|
||||
class="inline-flex items-center justify-center gap-2 px-6 py-3 text-base font-semibold text-white bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 rounded-lg transition-colors">
|
||||
@svg('heroicon-o-home', 'w-5 h-5')
|
||||
{{ __('Zum Dashboard') }}
|
||||
</flux:button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<form method="POST" action="{{ route('auth.logout') }}" class="inline">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white transition-colors cursor-pointer rounded-lg">
|
||||
@svg('heroicon-o-arrow-left-start-on-rectangle', 'w-5 h-5')
|
||||
{{ __('Logout') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
719
resources/views/livewire/partner/setup-wizard_bak.blade.php
Normal file
719
resources/views/livewire/partner/setup-wizard_bak.blade.php
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Partner;
|
||||
use App\Models\Brand;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\RegistrationCode;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\WithFileUploads;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('components.layouts.guest'), Title('Setup-Wizard')] class extends Component {
|
||||
use WithFileUploads;
|
||||
|
||||
public Partner $partner;
|
||||
public string $partnerType;
|
||||
public int $currentStep = 1;
|
||||
public int $totalSteps;
|
||||
public array $steps = [];
|
||||
|
||||
// Schritt 1: Stammdaten (alle Rollen)
|
||||
public string $companyName = '';
|
||||
public string $displayName = '';
|
||||
public string $salutation = '';
|
||||
public string $firstName = '';
|
||||
public string $lastName = '';
|
||||
public string $description = '';
|
||||
public string $street = '';
|
||||
public string $houseNumber = '';
|
||||
public string $zip = '';
|
||||
public string $city = '';
|
||||
public string $country = 'Deutschland';
|
||||
public string $phone = '';
|
||||
public string $website = '';
|
||||
public $logo = null;
|
||||
|
||||
// Schritt 2: Retailer - Liefergebiete
|
||||
public ?int $deliveryRadius = null;
|
||||
public ?int $assemblyRadius = null;
|
||||
|
||||
// Schritt 2: Manufacturer - Marke
|
||||
public string $brandName = '';
|
||||
public $brandLogo = null;
|
||||
public string $brandDescription = '';
|
||||
|
||||
public string $roleIcon = 'shield-check';
|
||||
public string $roleName = '-';
|
||||
protected ?RegistrationCode $registrationCode = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user->partner_id) {
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
return;
|
||||
}
|
||||
|
||||
$role = $user->roles->first();
|
||||
if ($role) {
|
||||
$this->roleIcon = $role->icon ?? 'shield-check';
|
||||
$this->roleName = $role->display_name ?? $role->name;
|
||||
}
|
||||
|
||||
$this->partner = Partner::with('users')->findOrFail($user->partner_id);
|
||||
$this->partnerType = $this->partner->type;
|
||||
|
||||
// Vorausfüllen: Partner-Daten
|
||||
$this->companyName = $this->partner->company_name ?? '';
|
||||
$this->displayName = $this->partner->display_name ?? '';
|
||||
$this->salutation = $this->partner->salutation ?? '';
|
||||
$this->firstName = $this->partner->first_name ?? '';
|
||||
$this->lastName = $this->partner->last_name ?? '';
|
||||
$this->description = $this->partner->description ?? '';
|
||||
$this->street = $this->partner->street ?? '';
|
||||
$this->houseNumber = $this->partner->house_number ?? '';
|
||||
$this->zip = $this->partner->zip ?? '';
|
||||
$this->city = $this->partner->city ?? '';
|
||||
$this->country = $this->partner->country ?? 'Deutschland';
|
||||
$this->phone = $this->partner->phone ?? '';
|
||||
$this->website = $this->partner->website ?? '';
|
||||
|
||||
// Namen aus User übernehmen, falls Partner-Felder leer sind
|
||||
if (empty($this->firstName) && empty($this->lastName)) {
|
||||
$nameParts = explode(' ', $user->name, 2);
|
||||
$this->firstName = $nameParts[0] ?? '';
|
||||
$this->lastName = $nameParts[1] ?? '';
|
||||
}
|
||||
// Definiere Schritte basierend auf Rolle
|
||||
$this->defineSteps();
|
||||
}
|
||||
|
||||
protected function defineSteps(): void
|
||||
{
|
||||
$normalizedType = strtolower(str_replace('-', '', $this->partnerType));
|
||||
|
||||
switch ($normalizedType) {
|
||||
case 'retailer':
|
||||
$this->steps = ['Stammdaten', 'Liefergebiete', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$this->steps = ['Stammdaten', 'Marke anlegen', 'Fertig'];
|
||||
$this->totalSteps = 3;
|
||||
break;
|
||||
case 'broker':
|
||||
case 'estateagent':
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
case 'customer':
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
default:
|
||||
$this->steps = ['Stammdaten', 'Fertig'];
|
||||
$this->totalSteps = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveStep1(): void
|
||||
{
|
||||
$normalizedType = strtolower(str_replace('-', '', $this->partnerType));
|
||||
$isCustomer = $normalizedType === 'customer';
|
||||
$isBroker = $normalizedType === 'broker' || $normalizedType === 'estateagent';
|
||||
$rules = [
|
||||
'salutation' => 'required|in:Herr,Frau,Divers',
|
||||
'firstName' => 'required|string|max:255',
|
||||
'lastName' => 'required|string|max:255',
|
||||
'street' => 'required|string|max:255',
|
||||
'houseNumber' => 'required|string|max:20',
|
||||
'zip' => 'required|string|max:10',
|
||||
'city' => 'required|string|max:255',
|
||||
'country' => 'required|string|max:255',
|
||||
'phone' => 'nullable|string|max:50',
|
||||
];
|
||||
|
||||
if (!$isCustomer) {
|
||||
$rules['companyName'] = 'required|string|max:255';
|
||||
$rules['description'] = 'nullable|string|max:1000';
|
||||
$rules['website'] = 'nullable|url|max:255';
|
||||
}
|
||||
|
||||
// Für Broker ist displayName Pflicht
|
||||
if ($isBroker) {
|
||||
$rules['displayName'] = 'required|string|max:255';
|
||||
}
|
||||
|
||||
$this->validate($rules, [
|
||||
'salutation.required' => __('Bitte wählen Sie eine Anrede.'),
|
||||
'firstName.required' => __('Bitte geben Sie einen Vornamen ein.'),
|
||||
'lastName.required' => __('Bitte geben Sie einen Nachnamen ein.'),
|
||||
'companyName.required' => __('Bitte geben Sie einen Firmennamen ein.'),
|
||||
'displayName.required' => __('Bitte geben Sie einen Anzeigenamen ein.'),
|
||||
'street.required' => __('Bitte geben Sie eine Straße ein.'),
|
||||
'houseNumber.required' => __('Bitte geben Sie eine Hausnummer ein.'),
|
||||
'zip.required' => __('Bitte geben Sie eine Postleitzahl ein.'),
|
||||
'city.required' => __('Bitte geben Sie eine Stadt ein.'),
|
||||
'country.required' => __('Bitte wählen Sie ein Land.'),
|
||||
'website.url' => __('Bitte geben Sie eine gültige URL ein.'),
|
||||
]);
|
||||
|
||||
// Update Partner
|
||||
$updateData = [
|
||||
'salutation' => $this->salutation,
|
||||
'first_name' => $this->firstName,
|
||||
'last_name' => $this->lastName,
|
||||
'street' => $this->street,
|
||||
'house_number' => $this->houseNumber,
|
||||
'zip' => $this->zip,
|
||||
'city' => $this->city,
|
||||
'country' => $this->country,
|
||||
'phone' => $this->phone,
|
||||
];
|
||||
|
||||
if (!$isCustomer) {
|
||||
$updateData['company_name'] = $this->companyName;
|
||||
$updateData['description'] = $this->description;
|
||||
$updateData['website'] = $this->website;
|
||||
}
|
||||
|
||||
if ($isBroker) {
|
||||
$updateData['display_name'] = $this->displayName;
|
||||
}
|
||||
|
||||
$this->partner->update($updateData);
|
||||
|
||||
// Für Broker und Kunden: Direkt zum Erfolg
|
||||
if ($isBroker || $isCustomer) {
|
||||
$this->completeSetup();
|
||||
} else {
|
||||
$this->currentStep = 2;
|
||||
}
|
||||
}
|
||||
|
||||
public function saveStep2Retailer(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'deliveryRadius' => 'required|integer|min:1|max:500',
|
||||
'assemblyRadius' => 'required|integer|min:1|max:500',
|
||||
],
|
||||
[
|
||||
'deliveryRadius.required' => __('Bitte geben Sie einen Lieferradius ein.'),
|
||||
'deliveryRadius.min' => __('Der Lieferradius muss mindestens 1 km betragen.'),
|
||||
'assemblyRadius.required' => __('Bitte geben Sie einen Montageradius ein.'),
|
||||
'assemblyRadius.min' => __('Der Montageradius muss mindestens 1 km betragen.'),
|
||||
],
|
||||
);
|
||||
|
||||
$this->partner->update([
|
||||
'delivery_radius_km' => $this->deliveryRadius,
|
||||
'assembly_radius_km' => $this->assemblyRadius,
|
||||
]);
|
||||
|
||||
$this->completeSetup();
|
||||
}
|
||||
|
||||
public function saveStep2Manufacturer(): void
|
||||
{
|
||||
$this->validate(
|
||||
[
|
||||
'brandName' => 'required|string|max:255',
|
||||
'brandDescription' => 'nullable|string|max:1000',
|
||||
'brandLogo' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048',
|
||||
],
|
||||
[
|
||||
'brandName.required' => __('Bitte geben Sie einen Markennamen ein.'),
|
||||
'brandLogo.image' => __('Das Marken-Logo muss eine Bilddatei sein.'),
|
||||
'brandLogo.mimes' => __('Das Marken-Logo muss im Format JPG, PNG oder WebP sein.'),
|
||||
'brandLogo.max' => __('Das Marken-Logo darf maximal 2 MB groß sein.'),
|
||||
],
|
||||
);
|
||||
|
||||
// Speichere Brand-Logo falls hochgeladen
|
||||
$brandLogoPath = null;
|
||||
if ($this->brandLogo) {
|
||||
$brandLogoPath = $this->brandLogo->store('brand-logos', 'public');
|
||||
}
|
||||
|
||||
// Erstelle Brand
|
||||
Brand::create([
|
||||
'partner_id' => $this->partner->id,
|
||||
'name' => $this->brandName,
|
||||
'slug' => Str::slug($this->brandName),
|
||||
'description' => $this->brandDescription,
|
||||
'logo_url' => $brandLogoPath,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->completeSetup();
|
||||
}
|
||||
|
||||
|
||||
protected function completeSetup(): void
|
||||
{
|
||||
$this->partner->update([
|
||||
'is_active' => true,
|
||||
'setup_completed' => true,
|
||||
'setup_completed_at' => now(),
|
||||
]);
|
||||
|
||||
// Markiere Registrierungscode als verbraucht, falls vorhanden
|
||||
if ($this->registrationCode && $this->registrationCode->isAvailable()) {
|
||||
$this->registrationCode->markUsed(Auth::user());
|
||||
session()->forget(['registration_code_id', 'registration_role']);
|
||||
}
|
||||
|
||||
$this->currentStep = $this->totalSteps;
|
||||
}
|
||||
|
||||
public function goToDashboard(): void
|
||||
{
|
||||
$this->redirect(route('dashboard'), navigate: true);
|
||||
}
|
||||
|
||||
public function handleLogout(): void
|
||||
{
|
||||
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
session()->invalidate();
|
||||
session()->regenerateToken();
|
||||
|
||||
$this->redirect(route('home'), navigate: false);
|
||||
}
|
||||
|
||||
protected function loadRegistrationCode(): void
|
||||
{
|
||||
$codeId = session('registration_code_id');
|
||||
$role = session('registration_role');
|
||||
|
||||
if (!$codeId || !$role) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->registrationCode = RegistrationCode::where('id', $codeId)
|
||||
->where('role', $role)
|
||||
->first();
|
||||
|
||||
if (!$this->registrationCode || !$this->registrationCode->isAvailable()) {
|
||||
// Ungültig/verbraucht -> Session aufräumen
|
||||
session()->forget(['registration_code_id', 'registration_role']);
|
||||
$this->registrationCode = null;
|
||||
}
|
||||
}
|
||||
}; ?>
|
||||
|
||||
{{-- Konfetti Script laden --}}
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||
|
||||
<div class="w-full max-w-3xl">
|
||||
{{-- Header --}}
|
||||
<div class="text-center mb-8">
|
||||
@include('partials.logo-head')
|
||||
<h1 class="text-3xl font-bold text-zinc-900 dark:text-white mb-2">
|
||||
{{ __('Vervollständigen Sie Ihr Profil') }}
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Progress Indicator --}}
|
||||
<x-wizard-progress :currentStep="$currentStep" :totalSteps="$totalSteps" :steps="$steps" />
|
||||
|
||||
{{-- Wizard Content --}}
|
||||
<flux:card class="shadow-2xl">
|
||||
{{-- Step 1: Stammdaten (für alle Rollen) --}}
|
||||
@if ($currentStep === 1)
|
||||
@php
|
||||
$isCustomer = strtolower(str_replace('-', '', $partnerType)) === 'customer';
|
||||
@endphp
|
||||
<form wire:submit.prevent="saveStep1" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-'.$roleIcon, 'w-5 h-5')
|
||||
{{ __('Ihre Stammdaten') }} / {{ $roleName }}
|
||||
</div>
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Diese Informationen helfen uns, Ihr Profil zu vervollständigen.') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Firmenname (nur für Nicht-Kunden) --}}
|
||||
@if (!$isCustomer)
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Firmenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="companyName" name="companyName" icon="building-office" placeholder="{{ __('z.B. Müller GmbH') }}" />
|
||||
@error('companyName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
@php
|
||||
$isBroker = strtolower(str_replace('-', '', $partnerType)) === 'broker' || strtolower(str_replace('-', '', $partnerType)) === 'estateagent';
|
||||
@endphp
|
||||
|
||||
@if ($isBroker)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzeigename') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Der Name, der Ihren Kunden angezeigt wird (kann vom Firmennamen abweichen)') }}</flux:description>
|
||||
<flux:input wire:model="displayName" name="displayName" icon="user" placeholder="{{ __('z.B. Max Schmidt oder Immobilien Schmidt') }}" />
|
||||
@error('displayName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="description" name="description" rows="3" placeholder="{{ __('Ein kurzer Text über Ihr Unternehmen...') }}" />
|
||||
@error('description')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
@endif
|
||||
|
||||
{{-- Persönliche Daten --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anrede') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="salutation" name="salutation">
|
||||
<flux:select.option value="">{{ __('Bitte wählen') }}</flux:select.option>
|
||||
<flux:select.option value="Herr">{{ __('Herr') }}</flux:select.option>
|
||||
<flux:select.option value="Frau">{{ __('Frau') }}</flux:select.option>
|
||||
<flux:select.option value="Divers">{{ __('Divers') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('salutation')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Vorname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="firstName" name="firstName" icon="user" value="{{ $firstName }}" />
|
||||
@error('firstName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nachname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="lastName" name="lastName" icon="user" value="{{ $lastName }}" />
|
||||
@error('lastName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
{{-- Adresse --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<flux:field class="md:col-span-3">
|
||||
<flux:label>{{ __('Straße') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="street" name="street" icon="map-pin" placeholder="{{ __('Musterstraße') }}" />
|
||||
@error('street')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Hausnummer') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="houseNumber" name="houseNumber" placeholder="{{ __('123a') }}" />
|
||||
@error('houseNumber')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Postleitzahl') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="zip" name="zip" icon="map" placeholder="{{ __('12345') }}" />
|
||||
@error('zip')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field class="md:col-span-2">
|
||||
<flux:label>{{ __('Ort') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="city" name="city" icon="building-office" placeholder="{{ __('Musterstadt') }}" />
|
||||
@error('city')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Land') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="country" name="country">
|
||||
<flux:select.option value="Deutschland">{{ __('Deutschland') }}</flux:select.option>
|
||||
<flux:select.option value="Österreich">{{ __('Österreich') }}</flux:select.option>
|
||||
<flux:select.option value="Schweiz">{{ __('Schweiz') }}</flux:select.option>
|
||||
</flux:select>
|
||||
@error('country')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Telefon') }}</flux:label>
|
||||
<flux:input wire:model="phone" name="phone" type="tel" icon="phone" placeholder="{{ __('optional') }}" />
|
||||
@error('phone')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
@if (!$isCustomer)
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Website') }}</flux:label>
|
||||
<flux:input wire:model="website" name="website" type="url" icon="globe-alt" placeholder="https://..." />
|
||||
@error('website')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
@php
|
||||
$normalized = strtolower(str_replace('-', '', $partnerType));
|
||||
$isBrokerOrCustomer = $normalized === 'broker' || $normalized === 'estateagent' || $isCustomer;
|
||||
@endphp
|
||||
<flux:button type="submit" variant="primary" icon="{{ $isBrokerOrCustomer ? 'check-circle' : 'arrow-right' }}" class="cursor-pointer" wire:target="saveStep1">
|
||||
@if ($normalized === 'retailer')
|
||||
{{ __('Weiter zu Liefergebiete') }}
|
||||
@elseif($normalized === 'manufacturer')
|
||||
{{ __('Weiter zu Marke') }}
|
||||
@else
|
||||
{{ __('Setup abschließen') }}
|
||||
@endif
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Step 2: Retailer - Liefergebiete --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Retailer')
|
||||
<form wire:submit.prevent="saveStep2Retailer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
🚚 {{ __('Ihre Liefergebiete') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Wie weit liefern und montieren Sie von Ihrer Adresse (:zip :city) aus?', ['zip' => $zip, 'city' => $city]) }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich liefere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="deliveryRadius" name="deliveryRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
@error('deliveryRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageradius') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:description>{{ __('Ich montiere im Umkreis von ... km') }}</flux:description>
|
||||
<div class="flex items-center gap-4">
|
||||
<flux:input wire:model="assemblyRadius" name="assemblyRadius" type="number" min="1" max="500"
|
||||
class="flex-1" />
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">km</span>
|
||||
</div>
|
||||
@error('assemblyRadius')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Step 2: Manufacturer - Marke anlegen --}}
|
||||
@if ($currentStep === 2 && $partnerType === 'Manufacturer')
|
||||
<form wire:submit.prevent="saveStep2Manufacturer" class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg" class="mb-2">
|
||||
™️ {{ __('Ihre Marke') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
{{ __('Unter dieser Marke werden Ihre Produkte auf B2In gelistet. (Sie können später weitere Marken hinzufügen)') }}
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Markenname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="brandName" name="brandName" icon="tag"
|
||||
placeholder="{{ __('z.B. Möbelwerke Premium') }}" />
|
||||
@error('brandName')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Logo (optional)') }}</flux:label>
|
||||
<flux:description>{{ __('Laden Sie Ihr Marken-Logo hoch (max. 2 MB, JPG/PNG)') }}</flux:description>
|
||||
|
||||
<div class="space-y-2">
|
||||
<input type="file" wire:model.live="brandLogo" name="brandLogo" accept="image/jpeg,image/png,image/jpg,image/webp"
|
||||
class="block w-full text-sm text-zinc-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent-50 file:text-accent-700 hover:file:bg-accent-100 dark:text-zinc-400 dark:file:bg-accent-900/20 dark:file:text-accent-400" />
|
||||
|
||||
@error('brandLogo')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
|
||||
<div wire:loading wire:target="brandLogo" class="text-sm text-accent-600 dark:text-accent-400">
|
||||
<flux:icon.arrow-path class="inline-block h-4 w-4 animate-spin mr-2" />
|
||||
{{ __('Wird hochgeladen...') }}
|
||||
</div>
|
||||
|
||||
@if ($brandLogo)
|
||||
<div wire:loading.remove wire:target="brandLogo">
|
||||
@try
|
||||
<div class="p-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="{{ $brandLogo->temporaryUrl() }}"
|
||||
class="h-16 w-16 object-contain rounded border" alt="Brand Logo Preview">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Logo erfolgreich hochgeladen') }}</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400">{{ $brandLogo->getClientOriginalName() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@catch(\Exception $e)
|
||||
<div class="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<p class="text-sm text-red-800 dark:text-red-200">{{ __('Fehler beim Laden der Vorschau') }}</p>
|
||||
</div>
|
||||
@endtry
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marken-Beschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="brandDescription" name="brandDescription" rows="4"
|
||||
placeholder="{{ __('Ein kurzer Text über Ihre Marke...') }}" />
|
||||
@error('brandDescription')
|
||||
<flux:error>{{ $message }}</flux:error>
|
||||
@enderror
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<x-error-alert />
|
||||
|
||||
<div class="flex justify-end">
|
||||
<flux:button type="submit" variant="primary" icon="check-circle">
|
||||
{{ __('Setup abschließen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
{{-- Final Step: Fertig! --}}
|
||||
@if ($currentStep === $totalSteps)
|
||||
<div class="text-center space-y-6 py-8" x-data x-init="
|
||||
// Konfetti-Effekt
|
||||
const duration = 3 * 1000;
|
||||
const end = Date.now() + duration;
|
||||
|
||||
(function frame() {
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 60,
|
||||
spread: 55,
|
||||
origin: { x: 0 },
|
||||
colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
});
|
||||
confetti({
|
||||
particleCount: 5,
|
||||
angle: 120,
|
||||
spread: 55,
|
||||
origin: { x: 1 },
|
||||
colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6']
|
||||
});
|
||||
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
}());
|
||||
">
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
class="flex items-center justify-center w-20 h-20 rounded-full bg-green-100 dark:bg-green-900/20 animate-bounce">
|
||||
<flux:icon.check-circle class="h-12 w-12 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<flux:heading size="xl" class="mb-2">
|
||||
✅ {{ __('Einrichtung abgeschlossen!') }}
|
||||
</flux:heading>
|
||||
<flux:subheading>
|
||||
@if ($partnerType === 'Retailer')
|
||||
{{ __('Sie sind nun ein aktiver Händler. Sie können jetzt Ihr Sortiment pflegen.') }}
|
||||
@elseif($partnerType === 'Manufacturer')
|
||||
{{ __('Sie sind nun ein aktiver Hersteller. Sie können jetzt Ihre Produkte anlegen.') }}
|
||||
@else
|
||||
{{ __('Ihr Profil ist aktiv. In Ihrem Dashboard finden Sie Ihre persönlichen Einladungslinks.') }}
|
||||
@endif
|
||||
</flux:subheading>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
|
||||
<button type="button" wire:click="goToDashboard"
|
||||
class="inline-flex items-center justify-center gap-2 px-6 py-3 text-base font-semibold text-white bg-accent-600 hover:bg-accent-700 dark:bg-accent-500 dark:hover:bg-accent-600 rounded-lg transition-colors">
|
||||
@svg('heroicon-o-home', 'w-5 h-5')
|
||||
{{ __('Zum Dashboard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<form method="POST" action="{{ route('auth.logout') }}" class="inline">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white transition-colors cursor-pointer rounded-lg">
|
||||
@svg('heroicon-o-arrow-left-start-on-rectangle', 'w-5 h-5')
|
||||
{{ __('Logout') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
794
resources/views/livewire/products/create.blade.php
Normal file
794
resources/views/livewire/products/create.blade.php
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, mount};
|
||||
|
||||
state([
|
||||
'product' => [],
|
||||
'activeTab' => 'basis'
|
||||
]);
|
||||
|
||||
mount(function () {
|
||||
// Initialisierung der Dummy-Daten
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Produkte') }} (in Entwicklung)</flux:heading>
|
||||
<flux:subheading>{{ __('Erstellen Sie ein neues Produkt') }}</flux:subheading>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@svg('heroicon-o-cube', 'w-6 h-6 text-accent-600 dark:text-accent-400')
|
||||
<span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">{{ __('Neues Produkt anlegen') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form wire:submit="save" class="space-y-6">
|
||||
|
||||
{{-- Tab Navigation --}}
|
||||
<flux:tabs wire:model.live="activeTab" variant="segmented">
|
||||
<flux:tab name="basis" icon="identification">{{ __('Basis') }}</flux:tab>
|
||||
<flux:tab name="bilder" icon="photo">{{ __('Bilder') }}</flux:tab>
|
||||
<flux:tab name="physisch" icon="cube">{{ __('Physisch') }}</flux:tab>
|
||||
<flux:tab name="material" icon="beaker">{{ __('Material & Herkunft') }}</flux:tab>
|
||||
<flux:tab name="kommerziell" icon="currency-euro">{{ __('Kommerziell') }}</flux:tab>
|
||||
<flux:tab name="verwaltung" icon="cog">{{ __('Zuordnung & Verwaltung') }}</flux:tab>
|
||||
</flux:tabs>
|
||||
|
||||
{{-- TAB 1: BASIS - Identität & Varianten --}}
|
||||
@if($activeTab === 'basis')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- 1. Identität & Katalog --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('1. Identität & Katalog') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('B2in-Artikelnummer (intern)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.b2in_article_number" placeholder="B2IN-000471" />
|
||||
<flux:description>{{ __('Fortlaufende Nummer (vom System vergeben)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferanten-Artikelnummer') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.supplier_article_number" placeholder="SOFA-ALBA-3S-ANTHR" />
|
||||
<flux:description>{{ __('Originalnummer des Herstellers') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Produktname') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.product_name" placeholder="Sofa ALBA 3-Sitzer" />
|
||||
<flux:description>{{ __('Anzeigename auf Website') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Marke / Hersteller') }}</flux:label>
|
||||
<flux:input wire:model="product.brand" placeholder="Möbelwerk Nord" />
|
||||
<flux:description>{{ __('Produzent oder Label') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kategorie') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.category" placeholder="{{ __('Bitte wählen...') }}">
|
||||
<option value="sofas">{{ __('Wohnzimmer > Sofas') }}</option>
|
||||
<option value="chairs">{{ __('Esszimmer > Stühle') }}</option>
|
||||
<option value="beds">{{ __('Schlafzimmer > Betten') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kurzbeschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="product.short_description" rows="3" placeholder="Modernes Sofa mit Holzrahmen und Stoff"></flux:textarea>
|
||||
<flux:description>{{ __('Max. 180 Zeichen für Snippets') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Langbeschreibung') }}</flux:label>
|
||||
<flux:textarea wire:model="product.long_description" rows="6" placeholder="Das Sofa ALBA verbindet zeitloses Design..."></flux:textarea>
|
||||
<flux:description>{{ __('Detaillierter Text für Produktseite') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Status') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.status">
|
||||
<option value="active">{{ __('Aktiv') }}</option>
|
||||
<option value="draft">{{ __('Entwurf') }}</option>
|
||||
<option value="inactive">{{ __('Inaktiv') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erstelldatum / Änderungsdatum') }}</flux:label>
|
||||
<flux:input type="date" wire:model="product.created_at" value="2025-11-04" />
|
||||
<flux:description>{{ __('ISO-Datum') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 2. Varianten & Attribute --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('2. Varianten & Attribute') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Variantenattribute (Stammdaten)') }}</flux:label>
|
||||
<flux:input wire:model="product.variant_attributes" placeholder="Farbe, Bezug, Gestellfarbe" />
|
||||
<flux:description>{{ __('Merkmale, die die SKUs definieren') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Varianten (Kombinationen)') }}</flux:label>
|
||||
<flux:input wire:model="product.variants" placeholder="Anthrazit / Stoff A / Eiche hell" />
|
||||
<flux:description>{{ __('Konkrete Ausprägungen') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Weitere Attribute') }}</flux:label>
|
||||
<flux:input wire:model="product.additional_attributes" placeholder="Sitzhärte: mittel" />
|
||||
<flux:description>{{ __('Zusatzinfos (z. B. Sitzhärte, Stil)') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 2: BILDER - Upload & Verwaltung --}}
|
||||
@if($activeTab === 'bilder')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- Hauptbild & Galerie --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('Produktbilder') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Hauptbild') }} <span class="text-red-500">*</span></flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.main_image" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('Hauptansicht des Produkts (min. 1200x1200px, max. 5MB)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<flux:icon.photo class="h-12 w-12 mx-auto text-zinc-400 mb-2" />
|
||||
<span class="text-sm text-zinc-500">{{ __('Hauptbild Vorschau') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Produktgalerie') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.gallery_images" accept="image/*" multiple /> --}}
|
||||
<flux:description>{{ __('Mehrere Bilder hochladen (max. 10 Bilder, je max. 5MB)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
@for($i = 1; $i <= 8; $i++)
|
||||
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-40">
|
||||
<div class="text-center">
|
||||
<flux:icon.photo class="h-8 w-8 mx-auto text-zinc-400 mb-1" />
|
||||
<span class="text-xs text-zinc-500">{{ __('Bild') }} {{ $i }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Detailbilder & Ansichten --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('Detailansichten & Perspektiven') }}</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Vorderansicht') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.view_front" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('Frontale Produktansicht') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Rückansicht') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.view_back" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('Rückseite des Produkts') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Seitenansicht (links)') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.view_left" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('Linke Seite') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Seitenansicht (rechts)') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.view_right" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('Rechte Seite') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Detailaufnahme 1') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.detail_1" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('z.B. Material-Nahaufnahme') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Detailaufnahme 2') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.detail_2" accept="image/*" /> --}}
|
||||
<flux:description>{{ __('z.B. Verarbeitung, Nähte') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Ambiente & Lifestyle --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('Ambiente & Lifestyle-Bilder') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Ambiente-Bilder') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.lifestyle_images" accept="image/*" multiple /> --}}
|
||||
<flux:description>{{ __('Produkt in Wohnsituation (max. 5 Bilder)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
@for($i = 1; $i <= 3; $i++)
|
||||
<div class="border-2 border-dashed border-zinc-300 dark:border-zinc-600 rounded-lg p-4 flex items-center justify-center h-48">
|
||||
<div class="text-center">
|
||||
<flux:icon.home class="h-10 w-10 mx-auto text-zinc-400 mb-2" />
|
||||
<span class="text-sm text-zinc-500">{{ __('Ambiente') }} {{ $i }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endfor
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- 360° Ansicht & Video --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('360° Ansicht & Produktvideo') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('360° Bilder') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.rotation_images" accept="image/*" multiple /> --}}
|
||||
<flux:description>{{ __('Bilder für 360° Rotation (min. 24 Bilder empfohlen)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:separator />
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Produktvideo') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.product_video" accept="video/*" /> --}}
|
||||
<flux:description>{{ __('Kurzes Produktvideo (max. 50MB, MP4/WebM)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Video-URL (alternativ)') }}</flux:label>
|
||||
<flux:input wire:model="product.video_url" placeholder="https://youtube.com/watch?v=..." />
|
||||
<flux:description>{{ __('YouTube, Vimeo oder andere Video-URL') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- Technische Zeichnungen & Dokumente --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('Technische Zeichnungen & Dokumente') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Maßzeichnung') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.dimension_drawing" accept="image/*,application/pdf" /> --}}
|
||||
<flux:description>{{ __('Technische Zeichnung mit Maßen (PNG, JPG oder PDF)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageanleitung') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.assembly_manual" accept="application/pdf" /> --}}
|
||||
<flux:description>{{ __('PDF mit Montageanleitung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Datenblatt / Broschüre') }}</flux:label>
|
||||
{{-- <flux:input type="file" wire:model="product.datasheet" accept="application/pdf" /> --}}
|
||||
<flux:description>{{ __('Produktdatenblatt als PDF') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- Bild-Metadaten & Alt-Texte --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('Bild-Metadaten & SEO') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Alt-Text (Hauptbild)') }}</flux:label>
|
||||
<flux:input wire:model="product.main_image_alt" placeholder="Sofa ALBA 3-Sitzer in Anthrazit" />
|
||||
<flux:description>{{ __('Beschreibung für Suchmaschinen und Barrierefreiheit') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Bildnachweis / Copyright') }}</flux:label>
|
||||
<flux:input wire:model="product.image_credits" placeholder="© Möbelwerk Nord 2024" />
|
||||
<flux:description>{{ __('Fotografen-Nennung oder Copyright-Hinweis') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Bildoptimierung') }}</flux:label>
|
||||
<flux:checkbox wire:model="product.auto_optimize_images">
|
||||
{{ __('Bilder automatisch für Web optimieren (Komprimierung & Skalierung)') }}
|
||||
</flux:checkbox>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Wasserzeichen') }}</flux:label>
|
||||
<flux:checkbox wire:model="product.add_watermark">
|
||||
{{ __('B2in-Wasserzeichen auf Bilder anwenden') }}
|
||||
</flux:checkbox>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 3: PHYSISCH - Maße & Verpackung --}}
|
||||
@if($activeTab === 'physisch')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- 3. Maße & Gewicht (Produkt) --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('3. Maße & Gewicht (Produkt)') }}</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Breite (mm)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.width" placeholder="2200" />
|
||||
<flux:description>{{ __('Gesamtbreite') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Tiefe (mm)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.depth" placeholder="950" />
|
||||
<flux:description>{{ __('Gesamttiefe') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Höhe (mm)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.height" placeholder="830" />
|
||||
<flux:description>{{ __('Gesamthöhe') }}</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gewicht netto (kg)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" step="0.1" wire:model="product.weight" placeholder="68" />
|
||||
<flux:description>{{ __('Möbel ohne Verpackung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Aufbauart') }}</flux:label>
|
||||
<flux:select wire:model="product.assembly_type">
|
||||
<option value="assembled">{{ __('montiert') }}</option>
|
||||
<option value="partially">{{ __('teilmontiert') }}</option>
|
||||
<option value="disassembled">{{ __('zerlegt') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montagezeit (min)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.assembly_time" placeholder="45" />
|
||||
<flux:description>{{ __('Aufbauzeit') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Traglast (kg)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.load_capacity" placeholder="120" />
|
||||
<flux:description>{{ __('Belastbarkeit') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 4. Verpackung & Logistik --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('4. Verpackung & Logistik') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Anzahl Packstücke') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.package_count" placeholder="2" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gesamtgewicht brutto (kg)') }}</flux:label>
|
||||
<flux:input type="number" step="0.1" wire:model="product.gross_weight" placeholder="75" />
|
||||
<flux:description>{{ __('inkl. Verpackung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verpackungsart') }}</flux:label>
|
||||
<flux:input wire:model="product.packaging_type" placeholder="Karton mit Kantenschutz" />
|
||||
<flux:description>{{ __('Karton, Holzrahmen usw.') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verpackung recyclingfähig (%)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.packaging_recyclable" placeholder="85" />
|
||||
<flux:description>{{ __('Anteil recycelbarer Materialien der Verpackung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kolli 1 Maße (mm)') }}</flux:label>
|
||||
<flux:input wire:model="product.package_1_dimensions" placeholder="L × B × H: 1500 × 950 × 600" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Kolli 1 Gewicht (kg)') }}</flux:label>
|
||||
<flux:input type="number" step="0.1" wire:model="product.package_1_weight" placeholder="45" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Palettenfähig') }}</flux:label>
|
||||
<flux:select wire:model="product.palletizable">
|
||||
<option value="yes">{{ __('Ja') }}</option>
|
||||
<option value="no">{{ __('Nein') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('HS-Code (Zolltarifnummer)') }}</flux:label>
|
||||
<flux:input wire:model="product.hs_code" placeholder="94016100" />
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 4: MATERIAL & HERKUNFT - Materialien & Holzherkunft --}}
|
||||
@if($activeTab === 'material')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- 5. Materialien & Qualität --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('5. Materialien & Qualität') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Hauptmaterial') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.main_material" placeholder="Massivholz Buche" />
|
||||
<flux:description>{{ __('Tragende Struktur') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Oberflächenmaterial') }}</flux:label>
|
||||
<flux:input wire:model="product.surface_material" placeholder="Furnier Eiche geölt" />
|
||||
<flux:description>{{ __('Sichtflächen') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Bezugsmaterial') }}</flux:label>
|
||||
<flux:input wire:model="product.upholstery_material" placeholder="Stoff (Polyester)" />
|
||||
<flux:description>{{ __('Stoff / Leder / Synthetik') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Farbton / Dekor') }}</flux:label>
|
||||
<flux:input wire:model="product.color" placeholder="Eiche natur / Anthrazit" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Herkunftsland (Produktion)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.country_of_origin">
|
||||
<option value="DE">{{ __('Deutschland') }}</option>
|
||||
<option value="PL">{{ __('Polen') }}</option>
|
||||
<option value="IT">{{ __('Italien') }}</option>
|
||||
<option value="AT">{{ __('Österreich') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('ISO-Land') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Pflegehinweise') }}</flux:label>
|
||||
<flux:textarea wire:model="product.care_instructions" rows="3" placeholder="Reinigung & Pflege"></flux:textarea>
|
||||
<flux:description>{{ __('Mit feuchtem Tuch abwischen.') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Zertifikate / Labels') }}</flux:label>
|
||||
<flux:input wire:model="product.certificates" placeholder="FSC, OEKO-TEX, Blauer Engel etc." />
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 6. Holzherkunft & EUDR --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('6. Holzherkunft & EUDR') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Holzart(en)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.wood_type" placeholder="Quercus robur (Eiche)" />
|
||||
<flux:description>{{ __('Botanische Bezeichnung (falls Holz enthalten)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Herkunftsland des Holzes') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.wood_origin_country">
|
||||
<option value="PL">{{ __('Polen') }}</option>
|
||||
<option value="DE">{{ __('Deutschland') }}</option>
|
||||
<option value="RO">{{ __('Rumänien') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('ISO-Code (falls Holz enthalten)') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Region / Provinz') }}</flux:label>
|
||||
<flux:input wire:model="product.wood_region" placeholder="Masowien" />
|
||||
<flux:description>{{ __('falls erforderlich für EUDR') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Erntejahr') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.harvest_year" placeholder="2024" />
|
||||
<flux:description>{{ __('Jahr der Holzgewinnung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Forstbetrieb / Lieferant') }}</flux:label>
|
||||
<flux:input wire:model="product.forest_operator" placeholder="ForestPol Sp. z o.o." />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nachhaltigkeitszertifikat') }}</flux:label>
|
||||
<flux:input wire:model="product.sustainability_certificate" placeholder="FSC C123456" />
|
||||
<flux:description>{{ __('FSC / PEFC') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Sorgfaltserklärung (EUDR-DD-Referenz)') }}</flux:label>
|
||||
<flux:input wire:model="product.eudr_reference" placeholder="EUDR-DD-2025-PL-03421" />
|
||||
<flux:description>{{ __('offiziell Referenz') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Nachweisdatei (Upload)') }}</flux:label>
|
||||
<flux:input type="file" wire:model="product.eudr_document" />
|
||||
<flux:description>{{ __('PDF / Link zum Statement') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 5: KOMMERZIELL - Preise, Verfügbarkeit & Lieferung --}}
|
||||
@if($activeTab === 'kommerziell')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- 7. Preise & Konditionen --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('7. Preise & Konditionen') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Einkaufspreis (net)') }}</flux:label>
|
||||
<flux:input type="number" step="0.01" wire:model="product.purchase_price" placeholder="680.00" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verkaufspreis (net)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" step="0.01" wire:model="product.selling_price" placeholder="1,250.00" />
|
||||
<flux:description>{{ __('Für B2in-Plattform') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Währung') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.currency">
|
||||
<option value="EUR">{{ __('EUR') }}</option>
|
||||
<option value="USD">{{ __('USD') }}</option>
|
||||
<option value="CHF">{{ __('CHF') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Steuersatz (%)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.tax_rate" placeholder="19" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('UVP (Brutto)') }}</flux:label>
|
||||
<flux:input type="number" step="0.01" wire:model="product.rrp" placeholder="1,499.00" />
|
||||
<flux:description>{{ __('Unverbindliche Preisempfehlung') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 8. Verfügbarkeit & Lieferzeit --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('8. Verfügbarkeit & Lieferzeit') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lagerstatus') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.stock_status">
|
||||
<option value="in_stock">{{ __('Auf Lager') }}</option>
|
||||
<option value="on_order">{{ __('Auf Bestellung') }}</option>
|
||||
<option value="out_of_stock">{{ __('Nicht verfügbar') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Auf Lager / Auf Bestellung / Nicht verfügbar') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferzeit (Wochen)') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input type="number" wire:model="product.delivery_time" placeholder="4-6" />
|
||||
<flux:description>{{ __('Min–Max-Spanne') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Produktionszeit (Tage)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.production_time" placeholder="21" />
|
||||
<flux:description>{{ __('falls relevant') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 9. Lieferung, Montage & Service --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('9. Lieferung, Montage & Service') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Lieferart') }}</flux:label>
|
||||
<flux:select wire:model="product.delivery_type">
|
||||
<option value="pickup">{{ __('Abholung') }}</option>
|
||||
<option value="delivery">{{ __('Lieferung') }}</option>
|
||||
<option value="expedition">{{ __('Spedition') }}</option>
|
||||
<option value="parcel">{{ __('Paket') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Abholung / Lieferung / Spedition / Paket') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Montageservice') }}</flux:label>
|
||||
<flux:select wire:model="product.assembly_service">
|
||||
<option value="yes">{{ __('Ja') }}</option>
|
||||
<option value="no">{{ __('Nein') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Service-Radius (km)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.service_radius" placeholder="50" />
|
||||
<flux:description>{{ __('Für Montageservice') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Garantie (Monate)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.warranty" placeholder="24" />
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- TAB 6: VERWALTUNG - Händler, Nachhaltigkeit, Scoring & Verwaltung --}}
|
||||
@if($activeTab === 'verwaltung')
|
||||
<div class="space-y-8">
|
||||
|
||||
{{-- 10. Händler- / Herstellerzuordnung --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('10. Händler- / Herstellerzuordnung') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verkäufertyp') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:select wire:model="product.seller_type">
|
||||
<option value="retailer">{{ __('Händler') }}</option>
|
||||
<option value="manufacturer">{{ __('Hersteller') }}</option>
|
||||
<option value="broker">{{ __('Makler') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Hersteller / Makler') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verkäufername') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.seller_name" placeholder="WohnDesign Bielefeld" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Verkäufer-ID') }}</flux:label>
|
||||
<flux:input wire:model="product.seller_id" placeholder="SELLER_xyz" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Region / Hub') }} <span class="text-red-500">*</span></flux:label>
|
||||
<flux:input wire:model="product.region" placeholder="OWL" />
|
||||
<flux:description>{{ __('Logistische Zuordnung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Ort / PLZ') }}</flux:label>
|
||||
<flux:input wire:model="product.location" placeholder="33602 Bielefeld" />
|
||||
<flux:description>{{ __('Standort des Verkäufers/Lagers') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 11. Nachhaltigkeit & Umwelt --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('11. Nachhaltigkeit & Umwelt') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('CO₂-Fußabdruck (kg CO₂e) pro Stück') }}</flux:label>
|
||||
<flux:input type="number" step="0.1" wire:model="product.co2_footprint" placeholder="35" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Recyclinganteil (%)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.recycling_rate" placeholder="40" />
|
||||
<flux:description>{{ __('Anteil recycelter Materialien im Produkt') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Regionale Produktion') }}</flux:label>
|
||||
<flux:select wire:model="product.regional_production">
|
||||
<option value="yes">{{ __('Ja') }}</option>
|
||||
<option value="no">{{ __('Nein') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('Ja / Nein (Umkreis z. B. < 500 km)') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 12. Scoring-System (B2in Internal) --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('12. Scoring-System (B2in Internal)') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Stauraumvolumen (L)') }}</flux:label>
|
||||
<flux:input type="number" wire:model="product.storage_volume" placeholder="280" />
|
||||
<flux:description>{{ __('Innenvolumen') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Aufbauaufwand (1–5)') }}</flux:label>
|
||||
<flux:select wire:model="product.assembly_effort">
|
||||
<option value="1">1 - {{ __('Sehr einfach') }}</option>
|
||||
<option value="2">2 - {{ __('Einfach') }}</option>
|
||||
<option value="3">3 - {{ __('Mittel') }}</option>
|
||||
<option value="4">4 - {{ __('Anspruchsvoll') }}</option>
|
||||
<option value="5">5 - {{ __('Sehr anspruchsvoll') }}</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('gering = 1') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Designpunkte (1–5)') }}</flux:label>
|
||||
<flux:select wire:model="product.design_score">
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
</flux:select>
|
||||
<flux:description>{{ __('interne Bewertung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Gesamt-Score') }}</flux:label>
|
||||
<flux:input type="number" step="0.1" wire:model="product.total_score" placeholder="4.2" disabled />
|
||||
<flux:description>{{ __('automatisch berechnet') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
{{-- 13. Verwaltung & Lebenszyklus --}}
|
||||
<flux:card class="space-y-6">
|
||||
<flux:heading size="lg" class="border-b pb-3">{{ __('13. Verwaltung & Lebenszyklus') }}</flux:heading>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Sichtbar ab / bis (Datum)') }}</flux:label>
|
||||
<flux:input type="date" wire:model="product.visible_from" placeholder="2025-01-01 / 2026-01-01" />
|
||||
<flux:description>{{ __('Steuerung der Veröffentlichung') }}</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Freigabe durch B2in erforderlich') }}</flux:label>
|
||||
<flux:select wire:model="product.requires_approval">
|
||||
<option value="yes">{{ __('Ja') }}</option>
|
||||
<option value="no">{{ __('Nein') }}</option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('Letzte Änderung') }}</flux:label>
|
||||
<flux:input type="date" wire:model="product.last_modified" value="2025-11-04" disabled />
|
||||
<flux:description>{{ __('Datum der letzten Aktualisierung') }}</flux:description>
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Submit Button (außerhalb der Tabs, immer sichtbar) --}}
|
||||
<div class="flex justify-end gap-4 pt-6 border-t">
|
||||
<flux:button variant="ghost" href="{{ route('dashboard') }}">{{ __('Abbrechen') }}</flux:button>
|
||||
<flux:button type="submit" variant="primary" icon="check">{{ __('Produkt speichern') }}</flux:button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
398
resources/views/livewire/products/index.blade.php
Normal file
398
resources/views/livewire/products/index.blade.php
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
<?php
|
||||
|
||||
use function Livewire\Volt\{state, mount, computed};
|
||||
|
||||
state([
|
||||
'search' => '',
|
||||
'statusFilter' => 'all',
|
||||
'categoryFilter' => 'all',
|
||||
'sortBy' => 'created_at',
|
||||
'sortDirection' => 'desc',
|
||||
]);
|
||||
|
||||
// Dummy-Produkte für die Anzeige
|
||||
$products = computed(function () {
|
||||
return [
|
||||
[
|
||||
'id' => 1,
|
||||
'b2in_number' => 'B2IN-000471',
|
||||
'supplier_number' => 'SOFA-ALBA-3S-ANTHR',
|
||||
'name' => 'Sofa ALBA 3-Sitzer',
|
||||
'brand' => 'Möbelwerk Nord',
|
||||
'category' => 'Wohnzimmer > Sofas',
|
||||
'status' => 'active',
|
||||
'price' => 1250.00,
|
||||
'stock_status' => 'in_stock',
|
||||
'created_at' => '2025-11-04',
|
||||
'image' => null,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'b2in_number' => 'B2IN-000472',
|
||||
'supplier_number' => 'CHAIR-NORDIC-OAK',
|
||||
'name' => 'Stuhl Nordic Eiche',
|
||||
'brand' => 'Design Studio',
|
||||
'category' => 'Esszimmer > Stühle',
|
||||
'status' => 'active',
|
||||
'price' => 189.00,
|
||||
'stock_status' => 'in_stock',
|
||||
'created_at' => '2025-11-05',
|
||||
'image' => null,
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'b2in_number' => 'B2IN-000473',
|
||||
'supplier_number' => 'BED-LUNA-180',
|
||||
'name' => 'Bett Luna 180x200',
|
||||
'brand' => 'Schlafwelt',
|
||||
'category' => 'Schlafzimmer > Betten',
|
||||
'status' => 'draft',
|
||||
'price' => 899.00,
|
||||
'stock_status' => 'on_order',
|
||||
'created_at' => '2025-11-03',
|
||||
'image' => null,
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'b2in_number' => 'B2IN-000474',
|
||||
'supplier_number' => 'TABLE-OAK-EXTEND',
|
||||
'name' => 'Esstisch Eiche ausziehbar',
|
||||
'brand' => 'Tischmanufaktur',
|
||||
'category' => 'Esszimmer > Tische',
|
||||
'status' => 'active',
|
||||
'price' => 1450.00,
|
||||
'stock_status' => 'in_stock',
|
||||
'created_at' => '2025-10-28',
|
||||
'image' => null,
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'b2in_number' => 'B2IN-000475',
|
||||
'supplier_number' => 'WARDROBE-CLASSIC',
|
||||
'name' => 'Kleiderschrank Classic',
|
||||
'brand' => 'Möbelwerk Nord',
|
||||
'category' => 'Schlafzimmer > Schränke',
|
||||
'status' => 'inactive',
|
||||
'price' => 2100.00,
|
||||
'stock_status' => 'out_of_stock',
|
||||
'created_at' => '2025-10-15',
|
||||
'image' => null,
|
||||
],
|
||||
[
|
||||
'id' => 6,
|
||||
'b2in_number' => 'B2IN-000476',
|
||||
'supplier_number' => 'SIDEBOARD-MOD-120',
|
||||
'name' => 'Sideboard Modern 120cm',
|
||||
'brand' => 'Design Studio',
|
||||
'category' => 'Wohnzimmer > Sideboards',
|
||||
'status' => 'active',
|
||||
'price' => 675.00,
|
||||
'stock_status' => 'in_stock',
|
||||
'created_at' => '2025-11-01',
|
||||
'image' => null,
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
?>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ __('Produkte') }} (in Entwicklung)</flux:heading>
|
||||
<flux:subheading>{{ __('Verwalten Sie Ihre Produktliste') }}</flux:subheading>
|
||||
</div>
|
||||
<flux:button variant="primary" icon="plus" :href="route('products.create')" wire:navigate>
|
||||
{{ __('Neues Produkt') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Filter & Suche --}}
|
||||
<flux:card class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{{-- Suchfeld --}}
|
||||
<div class="md:col-span-2">
|
||||
<flux:input
|
||||
wire:model.live.debounce.300ms="search"
|
||||
placeholder="{{ __('Suche nach Name, Artikelnummer...') }}"
|
||||
icon="magnifying-glass"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- Status Filter --}}
|
||||
<flux:select wire:model.live="statusFilter">
|
||||
<option value="all">{{ __('Alle Status') }}</option>
|
||||
<option value="active">{{ __('Aktiv') }}</option>
|
||||
<option value="draft">{{ __('Entwurf') }}</option>
|
||||
<option value="inactive">{{ __('Inaktiv') }}</option>
|
||||
</flux:select>
|
||||
|
||||
{{-- Kategorie Filter --}}
|
||||
<flux:select wire:model.live="categoryFilter">
|
||||
<option value="all">{{ __('Alle Kategorien') }}</option>
|
||||
<option value="sofas">{{ __('Sofas') }}</option>
|
||||
<option value="chairs">{{ __('Stühle') }}</option>
|
||||
<option value="tables">{{ __('Tische') }}</option>
|
||||
<option value="beds">{{ __('Betten') }}</option>
|
||||
<option value="wardrobes">{{ __('Schränke') }}</option>
|
||||
</flux:select>
|
||||
</div>
|
||||
|
||||
{{-- Aktive Filter Anzeige --}}
|
||||
@if($search || $statusFilter !== 'all' || $categoryFilter !== 'all')
|
||||
<div class="flex items-center gap-2 mt-4 pt-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Filter:') }}</span>
|
||||
@if($search)
|
||||
<flux:badge color="blue">
|
||||
{{ __('Suche: ') }}{{ $search }}
|
||||
<button wire:click="$set('search', '')" class="ml-1">×</button>
|
||||
</flux:badge>
|
||||
@endif
|
||||
@if($statusFilter !== 'all')
|
||||
<flux:badge color="green">
|
||||
{{ __('Status: ') }}{{ __($statusFilter) }}
|
||||
<button wire:click="$set('statusFilter', 'all')" class="ml-1">×</button>
|
||||
</flux:badge>
|
||||
@endif
|
||||
@if($categoryFilter !== 'all')
|
||||
<flux:badge color="purple">
|
||||
{{ __('Kategorie: ') }}{{ __($categoryFilter) }}
|
||||
<button wire:click="$set('categoryFilter', 'all')" class="ml-1">×</button>
|
||||
</flux:badge>
|
||||
@endif
|
||||
<button
|
||||
wire:click="$set('search', ''); $set('statusFilter', 'all'); $set('categoryFilter', 'all')"
|
||||
class="text-sm text-accent-600 hover:text-accent-700 dark:text-accent-400 ml-2"
|
||||
>
|
||||
{{ __('Alle Filter zurücksetzen') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Produkttabelle --}}
|
||||
<flux:card>
|
||||
<flux:table>
|
||||
<flux:table.columns>
|
||||
<flux:table.column class="w-20">{{ __('Bild') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Produkt') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Marke') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Kategorie') }}</flux:table.column>
|
||||
<flux:table.column class="text-right">{{ __('Preis') }}</flux:table.column>
|
||||
<flux:table.column class="text-center">{{ __('Status') }}</flux:table.column>
|
||||
<flux:table.column class="text-center">{{ __('Lager') }}</flux:table.column>
|
||||
<flux:table.column>{{ __('Erstellt') }}</flux:table.column>
|
||||
<flux:table.column class="text-right w-32">{{ __('Aktionen') }}</flux:table.column>
|
||||
</flux:table.columns>
|
||||
|
||||
<flux:table.rows>
|
||||
@forelse($this->products as $product)
|
||||
<flux:table.row :key="$product['id']" class="odd:bg-zinc-50 dark:odd:bg-zinc-800/30">
|
||||
{{-- Bild --}}
|
||||
<flux:table.cell>
|
||||
<div class="w-12 h-12 bg-zinc-200 dark:bg-zinc-700 rounded flex items-center justify-center">
|
||||
<flux:icon.photo class="w-6 h-6 text-zinc-400" />
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Produkt --}}
|
||||
<flux:table.cell>
|
||||
<div>
|
||||
<div class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ $product['name'] }}
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 dark:text-zinc-400 space-y-0.5 mt-1">
|
||||
<div>B2in: {{ $product['b2in_number'] }}</div>
|
||||
<div>Art.-Nr.: {{ $product['supplier_number'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Marke --}}
|
||||
<flux:table.cell>
|
||||
<span class="text-sm text-zinc-700 dark:text-zinc-300">
|
||||
{{ $product['brand'] }}
|
||||
</span>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Kategorie --}}
|
||||
<flux:table.cell>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ $product['category'] }}
|
||||
</span>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Preis --}}
|
||||
<flux:table.cell class="text-right">
|
||||
<span class="font-semibold text-zinc-900 dark:text-zinc-100">
|
||||
{{ number_format($product['price'], 2, ',', '.') }} €
|
||||
</span>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Status --}}
|
||||
<flux:table.cell class="text-center">
|
||||
@php
|
||||
$statusColors = [
|
||||
'active' => 'green',
|
||||
'draft' => 'yellow',
|
||||
'inactive' => 'zinc',
|
||||
];
|
||||
$statusLabels = [
|
||||
'active' => __('Aktiv'),
|
||||
'draft' => __('Entwurf'),
|
||||
'inactive' => __('Inaktiv'),
|
||||
];
|
||||
@endphp
|
||||
<flux:badge :color="$statusColors[$product['status']]" size="sm">
|
||||
{{ $statusLabels[$product['status']] }}
|
||||
</flux:badge>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Lagerstatus --}}
|
||||
<flux:table.cell class="text-center">
|
||||
@php
|
||||
$stockColors = [
|
||||
'in_stock' => 'green',
|
||||
'on_order' => 'yellow',
|
||||
'out_of_stock' => 'red',
|
||||
];
|
||||
$stockLabels = [
|
||||
'in_stock' => __('Auf Lager'),
|
||||
'on_order' => __('Bestellung'),
|
||||
'out_of_stock' => __('Nicht verfügbar'),
|
||||
];
|
||||
@endphp
|
||||
<flux:badge :color="$stockColors[$product['stock_status']]" size="sm" variant="outline">
|
||||
{{ $stockLabels[$product['stock_status']] }}
|
||||
</flux:badge>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Erstellt am --}}
|
||||
<flux:table.cell>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ \Carbon\Carbon::parse($product['created_at'])->format('d.m.Y') }}
|
||||
</span>
|
||||
</flux:table.cell>
|
||||
|
||||
{{-- Aktionen --}}
|
||||
<flux:table.cell class="text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<flux:button variant="ghost" size="sm" icon="eye">
|
||||
{{ __('Ansehen') }}
|
||||
</flux:button>
|
||||
|
||||
<flux:dropdown position="bottom" align="end">
|
||||
<flux:button variant="ghost" size="sm" icon="ellipsis-horizontal" icon-trailing />
|
||||
|
||||
<flux:menu class="w-48">
|
||||
<flux:menu.item icon="pencil">
|
||||
{{ __('Bearbeiten') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item icon="document-duplicate">
|
||||
{{ __('Duplizieren') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.separator />
|
||||
<flux:menu.item icon="archive-box">
|
||||
{{ __('Archivieren') }}
|
||||
</flux:menu.item>
|
||||
<flux:menu.item icon="trash" variant="danger">
|
||||
{{ __('Löschen') }}
|
||||
</flux:menu.item>
|
||||
</flux:menu>
|
||||
</flux:dropdown>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@empty
|
||||
<flux:table.row>
|
||||
<flux:table.cell colspan="9" class="text-center py-12">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<flux:icon.cube class="w-16 h-16 text-zinc-400 mb-4" />
|
||||
<flux:heading size="lg" class="mb-2">{{ __('Keine Produkte gefunden') }}</flux:heading>
|
||||
<flux:subheading class="mb-4">
|
||||
{{ __('Erstellen Sie Ihr erstes Produkt oder passen Sie Ihre Filter an.') }}
|
||||
</flux:subheading>
|
||||
<flux:button variant="primary" icon="plus" :href="route('products.create')" wire:navigate>
|
||||
{{ __('Neues Produkt erstellen') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:table.cell>
|
||||
</flux:table.row>
|
||||
@endforelse
|
||||
</flux:table.rows>
|
||||
</flux:table>
|
||||
|
||||
{{-- Pagination / Stats --}}
|
||||
<div class="px-6 py-4 border-t border-zinc-200 dark:border-zinc-700">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{{ __('Zeige') }} <span class="font-semibold">{{ count($this->products) }}</span> {{ __('von') }} <span class="font-semibold">{{ count($this->products) }}</span> {{ __('Produkten') }}
|
||||
</div>
|
||||
|
||||
{{-- Hier würde normalerweise die Pagination kommen --}}
|
||||
<div class="flex items-center gap-2">
|
||||
<flux:button variant="ghost" size="sm" icon="chevron-left" disabled>
|
||||
{{ __('Zurück') }}
|
||||
</flux:button>
|
||||
<span class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Seite 1 von 1') }}</span>
|
||||
<flux:button variant="ghost" size="sm" icon="chevron-right" icon-trailing disabled>
|
||||
{{ __('Weiter') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Statistiken (Optional) --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-green-100 dark:bg-green-900/20 rounded-lg">
|
||||
<flux:icon.check-circle class="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">4</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Aktive Produkte') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-yellow-100 dark:bg-yellow-900/20 rounded-lg">
|
||||
<flux:icon.document class="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">1</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Entwürfe') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
|
||||
<flux:icon.cube class="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">5</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Auf Lager') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 bg-zinc-100 dark:bg-zinc-700 rounded-lg">
|
||||
<flux:icon.currency-euro class="w-6 h-6 text-zinc-600 dark:text-zinc-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">6.563 €</div>
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">{{ __('Ø Preis') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
291
resources/views/livewire/reg/landing.blade.php
Normal file
291
resources/views/livewire/reg/landing.blade.php
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
<?php
|
||||
|
||||
use App\Models\RegistrationCode;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
new #[Layout('web.layouts.web-master-slot'), Title('Registrierung')] class extends Component {
|
||||
public string $roleSlug;
|
||||
public string $roleKey;
|
||||
public string $codePrefix = '';
|
||||
public string $codePart1 = '';
|
||||
public string $codePart2 = '';
|
||||
public string $codePart3 = '';
|
||||
public string $codePart4 = '';
|
||||
public string $roleLabel = '';
|
||||
public string $roleDescription = '';
|
||||
|
||||
protected array $roleMap = [];
|
||||
|
||||
|
||||
public function mount(string $role): void
|
||||
{
|
||||
$this->loadRoleMap();
|
||||
|
||||
$slug = strtolower($role);
|
||||
if (!isset($this->roleMap[$slug])) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->roleSlug = $slug;
|
||||
$this->roleKey = $this->roleMap[$slug]['key'];
|
||||
$this->roleLabel = $this->roleMap[$slug]['label'];
|
||||
$this->roleDescription = $this->roleMap[$slug]['description'];
|
||||
$this->codePrefix = $this->roleMap[$slug]['prefix'] ?? '';
|
||||
}
|
||||
|
||||
protected function loadRoleMap(): void
|
||||
{
|
||||
if (!empty($this->roleMap)) {
|
||||
return;
|
||||
}
|
||||
$roles = Role::whereNotNull('reg_prefix')->where('can_be_invited', true)->get();
|
||||
foreach ($roles as $role) {
|
||||
$slug = strtolower(str_replace('-', '', $role->reg_prefix));
|
||||
$key = strtolower(str_replace('-', '', $role->name));
|
||||
$this->roleMap[$slug] = [
|
||||
'key' => $key,
|
||||
'label' => __('registration.roles.' . $key . '.label'),
|
||||
'description' => __('registration.roles.' . $key . '.description'),
|
||||
'prefix' => $role->reg_prefix,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function submitCode(): void
|
||||
{
|
||||
$this->validate([
|
||||
'codePrefix' => ['required', 'string', 'size:1'],
|
||||
'codePart1' => ['required', 'digits:2'],
|
||||
'codePart2' => ['required', 'digits:2'],
|
||||
'codePart3' => ['required', 'digits:2'],
|
||||
'codePart4' => ['required', 'digits:2'],
|
||||
]);
|
||||
|
||||
|
||||
$normalized = $this->normalizeCode(
|
||||
$this->codePrefix,
|
||||
$this->codePart1,
|
||||
$this->codePart2,
|
||||
$this->codePart3,
|
||||
$this->codePart4
|
||||
);
|
||||
/** @var RegistrationCode|null $registrationCode */
|
||||
$registrationCode = RegistrationCode::where('code', $normalized)
|
||||
->where('role', $this->roleKey)
|
||||
->first();
|
||||
if (!$registrationCode || !$registrationCode->isAvailable()) {
|
||||
$this->addError('code', __('registration.messages.code_invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Merke Code in Session; Verbrauch/Markierung erfolgt nach erfolgreicher Registrierung.
|
||||
session([
|
||||
'registration_code_id' => $registrationCode->id,
|
||||
'registration_role' => $this->roleKey,
|
||||
'registration_slug' => $this->roleSlug,
|
||||
]);
|
||||
|
||||
session()->flash('message', __('registration.messages.code_accepted'));
|
||||
$this->redirect(route('partner.create.account'), navigate: true);
|
||||
}
|
||||
|
||||
protected function normalizeCode(string $prefix, string ...$parts): string
|
||||
{
|
||||
$prefix = strtoupper(trim($prefix));
|
||||
$segments = array_map(fn ($p) => str_pad(preg_replace('/\D+/', '', $p), 2, '0', STR_PAD_LEFT), $parts);
|
||||
return $prefix . implode('', $segments);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<div class="max-w-xl mx-auto px-6 space-y-8 pt-20 pb-40" id="code-form">
|
||||
{{-- Hero --}}
|
||||
<div class="text-center space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-accent-500">{{ __('registration.titles.registration') }}</p>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-foreground">
|
||||
{{ __('registration.titles.access_for_role', ['role' => $roleLabel]) }}
|
||||
</h1>
|
||||
<p class="text-base text-muted-foreground">
|
||||
{{ $roleDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-card/80 backdrop-blur shadow-2xl border border-border rounded-3xl p-6 md:p-8 space-y-6">
|
||||
|
||||
|
||||
<form wire:submit="submitCode" class="space-y-4 mt-10">
|
||||
<flux:field>
|
||||
<div class="text-center text-lg font-medium text-foreground mb-4">{{ __('registration.titles.enter_code') }}</div>
|
||||
<div class="flex justify-center items-center gap-3 flex-nowrap mx-auto w-fit">
|
||||
<input
|
||||
wire:model.defer="codePrefix"
|
||||
placeholder="M"
|
||||
maxlength="1"
|
||||
autofocus
|
||||
class="w-12 text-center uppercase text-xl px-3 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600 text-foreground"
|
||||
/>
|
||||
<span class="text-muted-foreground">|</span>
|
||||
<input
|
||||
wire:model.defer="codePart1"
|
||||
placeholder="00"
|
||||
maxlength="2"
|
||||
class="w-16 text-center text-xl px-3 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600"
|
||||
/>
|
||||
<input
|
||||
wire:model.defer="codePart2"
|
||||
placeholder="00"
|
||||
maxlength="2"
|
||||
class="w-16 text-center text-xl px-3 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600"
|
||||
/>
|
||||
<input
|
||||
wire:model.defer="codePart3"
|
||||
placeholder="00"
|
||||
maxlength="2"
|
||||
class="w-16 text-center text-xl px-3 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600"
|
||||
/>
|
||||
<input
|
||||
wire:model.defer="codePart4"
|
||||
placeholder="00"
|
||||
maxlength="2"
|
||||
class="w-16 text-center text-xl px-3 py-3 bg-gray-50 border border-border rounded-lg outline-1 -outline-offset-1 outline-gray-300 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-sky-600"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div class="flex flex-col gap-3 p-4 mt-10 rounded-2xl bg-accent-50 dark:bg-accent-900/15 border border-accent-100 dark:border-accent-900/30">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-10 w-10 rounded-xl bg-accent-100 dark:bg-accent-900/40 flex items-center justify-center">
|
||||
<flux:icon.key class="h-5 w-5 text-accent-600 dark:text-accent-300" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-base text-foreground">{{ __('registration.messages.code_unique') }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ __('registration.messages.code_format') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-center text-muted-foreground mt-2 mb-2">
|
||||
{{ __('') }}
|
||||
</div>
|
||||
<br />
|
||||
|
||||
</flux:field>
|
||||
|
||||
<x-error-alert-static light />
|
||||
|
||||
<div class="flex justify-between">
|
||||
<flux:button type="button" variant="primary" icon="arrow-down" href="#how-it-works" class="cursor-pointer">
|
||||
{{ __('registration.actions.how_it_works') }}
|
||||
</flux:button>
|
||||
<button type="submit" class="btn-secondary-accent small cursor-pointer" wire:target="submitCode">
|
||||
<span class="flex items-center gap-2"><flux:icon.arrow-right class="h-5 w-5" /> {{ __('registration.actions.check_code') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@if (session()->has('message'))
|
||||
<div class="p-3 rounded-lg border border-red-200 bg-red-50 text-red-800">
|
||||
{{ session('message') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
{{ __('registration.messages.code_problems') }}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<section class="section-padding bg-accent" id="how-it-works">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16 slide-up delay-300">
|
||||
<h2 class="text-section-title text-foreground mb-6">
|
||||
{{ __('registration.titles.how_it_works') }}
|
||||
</h2>
|
||||
<p class="text-large text-muted-foreground max-w-3xl mx-auto">
|
||||
{{ __('registration.titles.how_it_works_description', ['role' => $roleLabel]) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-8 mb-16">
|
||||
<div class="card-elevated p-0 overflow-hidden group hover:shadow-elevated transition-all duration-300 slide-up delay-200">
|
||||
<div class="relative pt-12 pb-8">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@svg('heroicon-o-key', 'w-10 h-10 text-secondary-foreground')
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center">
|
||||
<span class="text-lg font-bold text-secondary">1</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-medium text-foreground">
|
||||
{{ __('registration.steps.code_entry.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-muted-foreground leading-relaxed mb-0">
|
||||
{{ __('registration.steps.code_entry.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-elevated p-0 overflow-hidden group hover:shadow-elevated transition-all duration-300 slide-up delay-400">
|
||||
<div class="relative pt-12 pb-8">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@svg('heroicon-o-user-plus', 'w-10 h-10 text-secondary-foreground')
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center">
|
||||
<span class="text-lg font-bold text-secondary">2</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-medium text-foreground">
|
||||
{{ __('registration.steps.create_account.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-muted-foreground leading-relaxed mb-0">
|
||||
{{ __('registration.steps.create_account.description', ['role' => $roleLabel]) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-elevated p-0 overflow-hidden group hover:shadow-elevated transition-all duration-300 slide-up delay-600">
|
||||
<div class="relative pt-12 pb-8">
|
||||
<div class="mx-auto w-20 h-20 icon-secondary-linear glow-soft group-hover:glow-medium rounded-2xl flex items-center justify-center transition-colors duration-300">
|
||||
@svg('heroicon-o-sparkles', 'w-10 h-10 text-secondary-foreground')
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-8">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center">
|
||||
<span class="text-lg font-bold text-secondary">3</span>
|
||||
</div>
|
||||
<h3 class="text-2xl font-medium text-foreground">
|
||||
{{ __('registration.steps.complete_onboarding.title') }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-muted-foreground leading-relaxed mb-6">
|
||||
{{ __('registration.steps.complete_onboarding.description') }}
|
||||
</p>
|
||||
<a href="#code-form" class="block">
|
||||
<button class="btn-secondary-accent small w-full cursor-pointer">
|
||||
{{ __('registration.actions.start_now') }}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<livewire:web.components.ui.footer />
|
||||
</div>
|
||||
129
resources/views/livewire/reg/thank-you.blade.php
Normal file
129
resources/views/livewire/reg/thank-you.blade.php
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new #[Layout('web.layouts.web-master-slot'), Title('Registrierung erfolgreich')] class extends Component {
|
||||
public ?string $email = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->email = request()->query('email');
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="variante-glass-flow">
|
||||
<div class="max-w-3xl mx-auto px-6 space-y-8 pt-20 pb-40">
|
||||
{{-- Header --}}
|
||||
<div class="text-center space-y-4">
|
||||
<div class="mx-auto w-20 h-20 rounded-full bg-green-100 flex items-center justify-center mb-6">
|
||||
@svg('heroicon-o-check-circle', 'h-12 w-12 text-green-600')
|
||||
</div>
|
||||
<p class="text-xs uppercase tracking-[0.2em] text-accent-500">{{ __('registration.thank_you.subtitle') }}</p>
|
||||
<h1 class="text-3xl md:text-4xl font-bold text-foreground">
|
||||
{{ __('registration.thank_you.title') }}
|
||||
</h1>
|
||||
<p class="text-lg text-muted-foreground">
|
||||
{{ __('registration.thank_you.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Info Card --}}
|
||||
<div class="bg-card/80 backdrop-blur shadow-2xl border border-border rounded-3xl p-6 md:p-8 space-y-6">
|
||||
|
||||
{{-- Email Verification Info --}}
|
||||
<div class="flex flex-col gap-3 p-4 rounded-2xl bg-lime-100 border border-lime-200 ">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="h-10 w-10 rounded-xl bg-lime-200 flex items-center justify-center flex-shrink-0">
|
||||
@svg('heroicon-o-envelope', 'h-5 w-5 text-lime-600')
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('registration.thank_you.email_sent_title') }}</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ __('registration.thank_you.email_sent_description', ['email' => $email]) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Next Steps --}}
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold text-foreground">{{ __('registration.thank_you.next_steps_title') }}</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
{{-- Step 1 --}}
|
||||
<div class="flex gap-4 p-4 rounded-xl bg-accent-50 border border-accent-100">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-lg bg-secondary text-secondary-foreground flex items-center justify-center font-bold text-sm">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('registration.thank_you.step1_title') }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ __('registration.thank_you.step1_description') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Step 2 --}}
|
||||
<div class="flex gap-4 p-4 rounded-xl bg-accent-50 border border-accent-100">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-lg bg-secondary text-secondary-foreground flex items-center justify-center font-bold text-sm">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('registration.thank_you.step2_title') }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ __('registration.thank_you.step2_description') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Step 3 --}}
|
||||
<div class="flex gap-4 p-4 rounded-xl bg-accent-50 border border-accent-100">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-lg bg-secondary text-secondary-foreground flex items-center justify-center font-bold text-sm">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('registration.thank_you.step3_title') }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ __('registration.thank_you.step3_description') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Important Note --}}
|
||||
<div class="flex flex-col gap-3 p-4 rounded-2xl bg-amber-50 border border-amber-100">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="h-10 w-10 rounded-xl bg-amber-100 flex items-center justify-center flex-shrink-0">
|
||||
@svg('heroicon-o-exclamation-triangle', 'h-5 w-5 text-amber-600')
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-foreground">{{ __('registration.thank_you.spam_check_title') }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ __('registration.thank_you.spam_check_description') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Login Link --}}
|
||||
<div class="text-center pt-6">
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
{{ __('registration.thank_you.already_verified') }}
|
||||
</p>
|
||||
<a href="{{ route('login') }}" class="btn-secondary-accent small cursor-pointer inline-flex items-center gap-2">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
@svg('heroicon-o-arrow-right', 'h-5 w-5')
|
||||
{{ __('registration.thank_you.login_button') }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Support Contact --}}
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
{{ __('registration.messages.code_problems') }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<livewire:web.components.ui.footer />
|
||||
</div>
|
||||
|
||||
|
|
@ -7,21 +7,23 @@
|
|||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center space-x-8">
|
||||
@foreach ($content['navigation'] as $navItem)
|
||||
<a href="{{ $navItem['url'] }}"
|
||||
class="text-sm font-medium transition-colors relative
|
||||
@if ($this->isActiveRoute($navItem['url'])) text-secondary after:absolute after:bottom-[-4px] after:left-0 after:w-full after:h-0.5 after:bg-secondary after:rounded-full
|
||||
@else
|
||||
text-primary hover:text-secondary @endif">
|
||||
{{ $navItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@if(isset($content['navigation']) && is_array($content['navigation']))
|
||||
@foreach ($content['navigation'] as $navItem)
|
||||
<a href="{{ $navItem['url'] }}"
|
||||
class="text-sm font-medium transition-colors relative
|
||||
@if ($this->isActiveRoute($navItem['url'])) text-secondary after:absolute after:bottom-[-4px] after:left-0 after:w-full after:h-0.5 after:bg-secondary after:rounded-full
|
||||
@else
|
||||
text-primary hover:text-secondary @endif">
|
||||
{{ $navItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ config('domains.domain_portal_url') }}"
|
||||
class=" md:block rounded-md px-3 py-2 text-sm font-medium bg-secondary hover-bg-primary glow-medium text-white transition-all duration-200">
|
||||
{{ $content['portal_login'] }}
|
||||
{{ $content['portal_login'] ?? 'Portal Login' }}
|
||||
</a>
|
||||
<button wire:click="toggleMobileMenu"
|
||||
class="md:hidden w-5 h-5 text-muted-foreground hover:text-foreground transition-colors">
|
||||
|
|
@ -44,20 +46,22 @@
|
|||
@if ($this->isMobileMenuOpen)
|
||||
<div class="md:hidden border-t border-border bg-background/95 backdrop-blur-sm">
|
||||
<nav class="px-4 py-6 space-y-4">
|
||||
@foreach ($content['navigation'] as $navItem)
|
||||
<a href="{{ $navItem['url'] }}"
|
||||
class="block text-sm font-medium transition-colors py-2 px-3 rounded-md
|
||||
@if ($this->isActiveRoute($navItem['url'])) text-secondary bg-secondary/10 border-l-2 border-secondary
|
||||
@else
|
||||
text-foreground hover:text-secondary hover:bg-muted/50 @endif"
|
||||
wire:click="closeMobileMenu">
|
||||
{{ $navItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@if(isset($content['navigation']) && is_array($content['navigation']))
|
||||
@foreach ($content['navigation'] as $navItem)
|
||||
<a href="{{ $navItem['url'] }}"
|
||||
class="block text-sm font-medium transition-colors py-2 px-3 rounded-md
|
||||
@if ($this->isActiveRoute($navItem['url'])) text-secondary bg-secondary/10 border-l-2 border-secondary
|
||||
@else
|
||||
text-foreground hover:text-secondary hover:bg-muted/50 @endif"
|
||||
wire:click="closeMobileMenu">
|
||||
{{ $navItem['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
@endif
|
||||
<div class="pt-4 border-t border-border">
|
||||
<a href="{{ config('domains.domain_portal_url') }}"
|
||||
class="block w-full btn-secondary-accent text-center">
|
||||
{{ $content['portal_login'] }}
|
||||
{{ $content['portal_login'] ?? 'Portal Login' }}
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
|
||||
|
||||
@vite(['resources/css/portal.css', 'resources/js/app.js'], 'build/portal')
|
||||
|
||||
@livewireStyles
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
@php
|
||||
// Brand aus Variable oder Auth-User's Partner, Fallback: aktuelles Theme
|
||||
$brand = $brand ?? (auth()->check() && auth()->user()->partner ? auth()->user()->partner->brand : null) ?? config('app.theme', 'b2in');
|
||||
@endphp
|
||||
<div class="mb-6 mx-auto flex justify-center">
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('positive')) }}"
|
||||
alt="B2IN Logo"
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPathForBrand($brand, 'positive')) }}"
|
||||
alt="Logo"
|
||||
class="h-14 w-auto dark:hidden" />
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPath('negative')) }}"
|
||||
alt="B2IN Logo"
|
||||
<img src="{{ asset(\App\Helpers\ThemeHelper::getLogoPathForBrand($brand, 'negative')) }}"
|
||||
alt="Logo"
|
||||
class="h-14 w-auto hidden dark:block" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,38 +1,48 @@
|
|||
<script>
|
||||
// Theme Toggle Funktionalität
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleText = document.getElementById('theme-toggle-text');
|
||||
const html = document.documentElement;
|
||||
(function() {
|
||||
// Theme Toggle Funktionalität
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleText = document.getElementById('theme-toggle-text');
|
||||
const html = document.documentElement;
|
||||
|
||||
// Theme aus localStorage laden oder Standard verwenden
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
|
||||
function updateThemeUI(theme) {
|
||||
if (theme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
themeToggleDarkIcon.classList.add('hidden');
|
||||
themeToggleText.textContent = 'Hell';
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.add('hidden');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
themeToggleText.textContent = 'Dunkel';
|
||||
// Abbrechen wenn Elemente nicht gefunden wurden
|
||||
if (!themeToggleBtn || !themeToggleLightIcon || !themeToggleDarkIcon || !themeToggleText) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initiales Theme setzen
|
||||
updateThemeUI(savedTheme);
|
||||
// Theme aus localStorage laden oder Standard verwenden
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
|
||||
// Toggle Button Event
|
||||
themeToggleBtn.addEventListener('click', () => {
|
||||
const currentTheme = html.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
function updateThemeUI(theme) {
|
||||
if (theme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
themeToggleDarkIcon.classList.add('hidden');
|
||||
themeToggleText.textContent = 'Hell';
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.add('hidden');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
themeToggleText.textContent = 'Dunkel';
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeUI(newTheme);
|
||||
});
|
||||
// Initiales Theme setzen
|
||||
updateThemeUI(savedTheme);
|
||||
|
||||
// Vorherigen Event-Listener entfernen (falls vorhanden) und neuen hinzufügen
|
||||
const newBtn = themeToggleBtn.cloneNode(true);
|
||||
themeToggleBtn.parentNode.replaceChild(newBtn, themeToggleBtn);
|
||||
|
||||
newBtn.addEventListener('click', () => {
|
||||
const currentTheme = html.classList.contains('dark') ? 'dark' : 'light';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
localStorage.setItem('theme', newTheme);
|
||||
updateThemeUI(newTheme);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
102
resources/views/vendor/livewire/bootstrap.blade.php
vendored
Normal file
102
resources/views/vendor/livewire/bootstrap.blade.php
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav class="d-flex justify-items-center justify-content-between">
|
||||
<div class="d-flex justify-content-between flex-fill d-sm-none">
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.previous')</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link" aria-hidden="true">@lang('pagination.next')</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-none flex-sm-fill d-sm-flex align-items-sm-center justify-content-sm-between">
|
||||
<div>
|
||||
<p class="small text-muted">
|
||||
{!! __('Showing') !!}
|
||||
<span class="fw-semibold">{{ $paginator->firstItem() }}</span>
|
||||
{!! __('to') !!}
|
||||
<span class="fw-semibold">{{ $paginator->lastItem() }}</span>
|
||||
{!! __('of') !!}
|
||||
<span class="fw-semibold">{{ $paginator->total() }}</span>
|
||||
{!! __('results') !!}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
|
||||
<span class="page-link" aria-hidden="true">‹</span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.previous')">‹</button>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Pagination Elements --}}
|
||||
@foreach ($elements as $element)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
|
||||
@endif
|
||||
|
||||
{{-- Array Of Links --}}
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<li class="page-item active" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}" aria-current="page"><span class="page-link">{{ $page }}</span></li>
|
||||
@else
|
||||
<li class="page-item" wire:key="paginator-{{ $paginator->getPageName() }}-page-{{ $page }}"><button type="button" class="page-link" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}">{{ $page }}</button></li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" aria-label="@lang('pagination.next')">›</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
|
||||
<span class="page-link" aria-hidden="true">›</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
53
resources/views/vendor/livewire/simple-bootstrap.blade.php
vendored
Normal file
53
resources/views/vendor/livewire/simple-bootstrap.blade.php
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.previous')</span>
|
||||
</li>
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="previousPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.previous')</button>
|
||||
</li>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<li class="page-item">
|
||||
<button dusk="nextPage" type="button" class="page-link" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<button type="button" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="page-link" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled">@lang('pagination.next')</button>
|
||||
</li>
|
||||
@endif
|
||||
@else
|
||||
<li class="page-item disabled" aria-disabled="true">
|
||||
<span class="page-link">@lang('pagination.next')</span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
56
resources/views/vendor/livewire/simple-tailwind.blade.php
vendored
Normal file
56
resources/views/vendor/livewire/simple-tailwind.blade.php
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="Pagination Navigation" class="flex justify-between">
|
||||
<span>
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.previous') !!}
|
||||
</span>
|
||||
@else
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="previousPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->previousCursor()->encode() }}" wire:click="setPage('{{$paginator->previousCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@else
|
||||
<button
|
||||
type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
@if(method_exists($paginator,'getCursorName'))
|
||||
<button type="button" dusk="nextPage" wire:key="cursor-{{ $paginator->getCursorName() }}-{{ $paginator->nextCursor()->encode() }}" wire:click="setPage('{{$paginator->nextCursor()->encode()}}','{{ $paginator->getCursorName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@else
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.next') !!}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
126
resources/views/vendor/livewire/tailwind.blade.php
vendored
Normal file
126
resources/views/vendor/livewire/tailwind.blade.php
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
@php
|
||||
if (! isset($scrollTo)) {
|
||||
$scrollTo = 'body';
|
||||
}
|
||||
|
||||
$scrollIntoViewJsSnippet = ($scrollTo !== false)
|
||||
? <<<JS
|
||||
(\$el.closest('{$scrollTo}') || document.querySelector('{$scrollTo}')).scrollIntoView()
|
||||
JS
|
||||
: '';
|
||||
@endphp
|
||||
|
||||
<div>
|
||||
@if ($paginator->hasPages())
|
||||
<nav role="navigation" aria-label="Pagination Navigation" class="flex items-center justify-between">
|
||||
<div class="flex justify-between flex-1 sm:hidden">
|
||||
<span>
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.previous') !!}
|
||||
</button>
|
||||
@endif
|
||||
</span>
|
||||
|
||||
<span>
|
||||
@if ($paginator->hasMorePages())
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" wire:loading.attr="disabled" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.before" class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-blue-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
|
||||
{!! __('pagination.next') !!}
|
||||
</button>
|
||||
@else
|
||||
<span class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
|
||||
{!! __('pagination.next') !!}
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
|
||||
<span>{!! __('Showing') !!}</span>
|
||||
<span class="font-medium">{{ $paginator->firstItem() }}</span>
|
||||
<span>{!! __('to') !!}</span>
|
||||
<span class="font-medium">{{ $paginator->lastItem() }}</span>
|
||||
<span>{!! __('of') !!}</span>
|
||||
<span class="font-medium">{{ $paginator->total() }}</span>
|
||||
<span>{!! __('results') !!}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="relative z-0 inline-flex rtl:flex-row-reverse rounded-md shadow-sm">
|
||||
<span>
|
||||
{{-- Previous Page Link --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span aria-disabled="true" aria-label="{{ __('pagination.previous') }}">
|
||||
<span class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="previousPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="previousPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" class="relative inline-flex items-center px-2 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.previous') }}">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</span>
|
||||
|
||||
{{-- Pagination Elements --}}
|
||||
@foreach ($elements as $element)
|
||||
{{-- "Three Dots" Separator --}}
|
||||
@if (is_string($element))
|
||||
<span aria-disabled="true">
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">{{ $element }}</span>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
{{-- Array Of Links --}}
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
<span wire:key="paginator-{{ $paginator->getPageName() }}-page{{ $page }}">
|
||||
@if ($page == $paginator->currentPage())
|
||||
<span aria-current="page">
|
||||
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600">{{ $page }}</span>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" wire:click="gotoPage({{ $page }}, '{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-300 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
|
||||
{{ $page }}
|
||||
</button>
|
||||
@endif
|
||||
</span>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<span>
|
||||
{{-- Next Page Link --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<button type="button" wire:click="nextPage('{{ $paginator->getPageName() }}')" x-on:click="{{ $scrollIntoViewJsSnippet }}" dusk="nextPage{{ $paginator->getPageName() == 'page' ? '' : '.' . $paginator->getPageName() }}.after" class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:ring ring-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.next') }}">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@else
|
||||
<span aria-disabled="true" aria-label="{{ __('pagination.next') }}">
|
||||
<span class="relative inline-flex items-center px-2 py-2 -ml-px text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
@endif
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
39
resources/views/vendor/mail/html/button.blade.php
vendored
Normal file
39
resources/views/vendor/mail/html/button.blade.php
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
@props([
|
||||
'url',
|
||||
'color' => 'primary',
|
||||
'align' => 'center',
|
||||
])
|
||||
@php
|
||||
$colorMap = [
|
||||
'primary' => '#20a0da',
|
||||
'blue' => '#20a0da',
|
||||
'success' => '#22c55e',
|
||||
'green' => '#22c55e',
|
||||
'error' => '#dc2626',
|
||||
'red' => '#dc2626',
|
||||
];
|
||||
$bgColor = $colorMap[$color] ?? '#20a0da';
|
||||
@endphp
|
||||
<table class="action" role="presentation" align="{{ $align }}" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
|
||||
<tr>
|
||||
<td align="{{ $align }}" style="padding: 0;">
|
||||
<table role="presentation" width="auto" border="0" cellpadding="0" cellspacing="0" align="{{ $align }}">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{ $url }}" style="height:48px;v-text-anchor:middle;width:200px;" arcsize="10%" stroke="f" fillcolor="{{ $bgColor }}">
|
||||
<w:anchorlock/>
|
||||
<center style="color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;">
|
||||
{!! $slot !!}
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<a href="{{ $url }}" class="button button-{{ $color }}" target="_blank" rel="noopener" style="background-color: {{ $bgColor }}; border-top: 14px solid {{ $bgColor }}; border-bottom: 14px solid {{ $bgColor }}; border-left: 32px solid {{ $bgColor }}; border-right: 32px solid {{ $bgColor }}; color: #ffffff; display: inline-block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 16px; font-weight: 600; line-height: 1; text-align: center; text-decoration: none; -webkit-text-size-adjust: none; mso-hide: all;">{!! $slot !!}</a>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
21
resources/views/vendor/mail/html/footer.blade.php
vendored
Normal file
21
resources/views/vendor/mail/html/footer.blade.php
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="600" align="center">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<![endif]-->
|
||||
<table class="footer" role="presentation" align="center" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; margin: 0 auto;">
|
||||
<tr>
|
||||
<td class="content-cell" align="center" style="padding: 32px 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.6; color: #999999; text-align: center;">
|
||||
{{ Illuminate\Mail\Markdown::parse($slot) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
26
resources/views/vendor/mail/html/header.blade.php
vendored
Normal file
26
resources/views/vendor/mail/html/header.blade.php
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
@props(['url'])
|
||||
@php
|
||||
// Logo-URL für E-Mails (muss absolute URL sein, PNG für volle Kompatibilität)
|
||||
$logoUrl = config('app.url') . '/img/logos/b2in-logo-negative.png';
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="header" style="background-color: #2b3f51; padding: 30px 20px; text-align: center;" bgcolor="#2b3f51" align="center">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding: 0;">
|
||||
<![endif]-->
|
||||
<a href="{{ $url }}" style="display: inline-block; text-decoration: none;" target="_blank" rel="noopener">
|
||||
@if (trim($slot) === 'Laravel')
|
||||
<img src="https://laravel.com/img/notification-logo-v2.1.png" class="logo" alt="Laravel Logo" width="50" height="50" style="display: block; height: 50px; max-height: 50px; width: auto; border: 0; outline: none;" />
|
||||
@else
|
||||
<img src="{{ $logoUrl }}" alt="{{ config('app.name') }}" width="140" height="40" style="display: block; height: 40px; max-height: 40px; width: auto; border: 0; outline: none; -ms-interpolation-mode: bicubic;" />
|
||||
@endif
|
||||
</a>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
178
resources/views/vendor/mail/html/layout.blade.php
vendored
Normal file
178
resources/views/vendor/mail/html/layout.blade.php
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" lang="de">
|
||||
<head>
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<title>{{ config('app.name') }}</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="supported-color-schemes" content="light dark" />
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td, th, p, a, h1, h2, h3, h4, h5, h6 {
|
||||
font-family: Arial, Helvetica, sans-serif !important;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
/* Reset & Base */
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* iOS Blue Links */
|
||||
a[x-apple-data-detectors] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
/* Gmail Blue Links */
|
||||
u + #body a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Samsung Mail */
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Dark Mode Styles */
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark-bg { background-color: #1a1a1a !important; }
|
||||
.dark-bg-card { background-color: #2a2a2a !important; }
|
||||
.dark-text { color: #ffffff !important; }
|
||||
.dark-text-secondary { color: #e0e0e0 !important; }
|
||||
.dark-border { border-color: #404040 !important; }
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media only screen and (max-width: 620px) {
|
||||
.wrapper {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.inner-body {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.footer {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.content-cell {
|
||||
padding: 24px 16px !important;
|
||||
}
|
||||
.button {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.mobile-full-width {
|
||||
width: 100% !important;
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{!! $head ?? '' !!}
|
||||
</head>
|
||||
<body id="body" style="margin: 0; padding:0; width: 100%; height: 100%; background-color: #f5f4f2; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%;" class="dark-bg">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" border="0" cellspacing="0" cellpadding="0" width="100%" bgcolor="#f5f4f2">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<![endif]-->
|
||||
|
||||
<table class="wrapper" role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f5f4f2; margin: 0; padding: 0;" bgcolor="#f5f4f2">
|
||||
<tr>
|
||||
<td align="center" valign="top" style="padding: 0;">
|
||||
<table class="content" role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; margin: 0 auto;">
|
||||
{!! $header ?? '' !!}
|
||||
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="body" style="background-color: #f5f4f2; padding: 30px 0 0 0;" bgcolor="#f5f4f2">
|
||||
<!--[if mso]>
|
||||
<table role="presentation" border="0" cellspacing="0" cellpadding="0" width="600" align="center">
|
||||
<tr>
|
||||
<td>
|
||||
<![endif]-->
|
||||
<table class="inner-body" role="presentation" align="center" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width: 600px; background-color: #ffffff; border-radius: 8px; margin: 0 auto;" bgcolor="#ffffff">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell dark-bg-card" style="padding: 40px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #2a2a2a;">
|
||||
{!! Illuminate\Mail\Markdown::parse($slot) !!}
|
||||
|
||||
{!! $subcopy ?? '' !!}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{!! $footer ?? '' !!}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</body>
|
||||
</html>
|
||||
36
resources/views/vendor/mail/html/message.blade.php
vendored
Normal file
36
resources/views/vendor/mail/html/message.blade.php
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
@component('mail::layout')
|
||||
{{-- Header --}}
|
||||
@slot('header')
|
||||
@component('mail::header', ['url' => config('app.url')])
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
||||
@endslot
|
||||
|
||||
{{-- Body --}}
|
||||
{!! $slot !!}
|
||||
|
||||
{{-- Subcopy --}}
|
||||
@isset($subcopy)
|
||||
@slot('subcopy')
|
||||
@component('mail::subcopy')
|
||||
{!! $subcopy !!}
|
||||
@endcomponent
|
||||
@endslot
|
||||
@endisset
|
||||
|
||||
{{-- Footer --}}
|
||||
@slot('footer')
|
||||
@component('mail::footer')
|
||||
© {{ date('Y') }} {{ config('app.name') }}. {{ __('Alle Rechte vorbehalten.') }}
|
||||
|
||||
[{{ parse_url(config('app.url'), PHP_URL_HOST) }}]({{ config('app.url') }})
|
||||
@endcomponent
|
||||
@endslot
|
||||
|
||||
{{-- Theme CSS --}}
|
||||
@slot('head')
|
||||
<style type="text/css">
|
||||
{!! file_get_contents(resource_path('views/vendor/mail/html/themes/b2in.css')) !!}
|
||||
</style>
|
||||
@endslot
|
||||
@endcomponent
|
||||
13
resources/views/vendor/mail/html/panel.blade.php
vendored
Normal file
13
resources/views/vendor/mail/html/panel.blade.php
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<table class="panel" role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-left: 4px solid #20a0da; margin: 24px 0;">
|
||||
<tr>
|
||||
<td class="panel-content" style="background-color: #f0f9ff; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.6; color: #2a2a2a;" bgcolor="#f0f9ff">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td class="panel-item" style="padding: 0;">
|
||||
{{ Illuminate\Mail\Markdown::parse($slot) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
7
resources/views/vendor/mail/html/subcopy.blade.php
vendored
Normal file
7
resources/views/vendor/mail/html/subcopy.blade.php
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<table class="subcopy" role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="border-top: 1px solid #e0e0e0; margin-top: 30px; padding-top: 25px;">
|
||||
<tr>
|
||||
<td style="padding-top: 25px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.6; color: #606060;">
|
||||
{{ Illuminate\Mail\Markdown::parse($slot) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
9
resources/views/vendor/mail/html/table.blade.php
vendored
Normal file
9
resources/views/vendor/mail/html/table.blade.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<div class="table" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.6; color: #2a2a2a;">
|
||||
{{ Illuminate\Mail\Markdown::parse($slot) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
468
resources/views/vendor/mail/html/themes/b2in.css
vendored
Normal file
468
resources/views/vendor/mail/html/themes/b2in.css
vendored
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
/* B2IN Mail Theme - E-Mail Client Optimized */
|
||||
/* Primary: #2b3f51 (Anthracite) | Secondary: #20a0da (Dynamic Blue) */
|
||||
/* Kompatibel mit: Outlook, Gmail, Apple Mail, Yahoo, Thunderbird */
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
============================================ */
|
||||
|
||||
body,
|
||||
body *:not(html):not(style):not(br):not(tr):not(code) {
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
background-color: #f5f4f2;
|
||||
color: #2a2a2a;
|
||||
height: 100%;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
line-height: 1.6;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #20a0da;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
h1 {
|
||||
color: #2b3f51;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
line-height: 1.3;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2b3f51;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
line-height: 1.3;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #2b3f51;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
text-align: left;
|
||||
color: #2a2a2a;
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #2b3f51;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border: 0;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LAYOUT
|
||||
============================================ */
|
||||
|
||||
.wrapper {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
background-color: #f5f4f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HEADER
|
||||
============================================ */
|
||||
|
||||
.header {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
background-color: #2b3f51;
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOGO
|
||||
============================================ */
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
max-height: 50px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BODY
|
||||
============================================ */
|
||||
|
||||
.body {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
background-color: #f5f4f2;
|
||||
border-bottom: 1px solid #f5f4f2;
|
||||
border-top: 1px solid #f5f4f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner-body {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 600px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 600px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.inner-body a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SUBCOPY
|
||||
============================================ */
|
||||
|
||||
.subcopy {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: 30px;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.subcopy p {
|
||||
font-size: 13px;
|
||||
color: #606060;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FOOTER
|
||||
============================================ */
|
||||
|
||||
.footer {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
color: #999999;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #20a0da;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TABLES
|
||||
============================================ */
|
||||
|
||||
.table table {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 30px auto;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
color: #2b3f51;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table td {
|
||||
color: #2a2a2a;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
max-width: 100vw;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BUTTONS
|
||||
============================================ */
|
||||
|
||||
.action {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
-webkit-text-size-adjust: none;
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.button-blue,
|
||||
.button-primary {
|
||||
background-color: #20a0da;
|
||||
border-top: 14px solid #20a0da;
|
||||
border-bottom: 14px solid #20a0da;
|
||||
border-left: 32px solid #20a0da;
|
||||
border-right: 32px solid #20a0da;
|
||||
}
|
||||
|
||||
.button-green,
|
||||
.button-success {
|
||||
background-color: #22c55e;
|
||||
border-top: 14px solid #22c55e;
|
||||
border-bottom: 14px solid #22c55e;
|
||||
border-left: 32px solid #22c55e;
|
||||
border-right: 32px solid #22c55e;
|
||||
}
|
||||
|
||||
.button-red,
|
||||
.button-error {
|
||||
background-color: #dc2626;
|
||||
border-top: 14px solid #dc2626;
|
||||
border-bottom: 14px solid #dc2626;
|
||||
border-left: 32px solid #dc2626;
|
||||
border-right: 32px solid #dc2626;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PANELS
|
||||
============================================ */
|
||||
|
||||
.panel {
|
||||
border-left: 4px solid #20a0da;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background-color: #f0f9ff;
|
||||
color: #2a2a2a;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-content p {
|
||||
color: #2a2a2a;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel-item p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Special Panel Variants */
|
||||
|
||||
.panel-warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.panel-warning .panel-content {
|
||||
background-color: #fffbeb;
|
||||
}
|
||||
|
||||
.panel-success {
|
||||
border-left-color: #22c55e;
|
||||
}
|
||||
|
||||
.panel-success .panel-content {
|
||||
background-color: #f0fdf4;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
PROMOTION
|
||||
============================================ */
|
||||
|
||||
.promotion {
|
||||
background-color: #2b3f51;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.promotion h1 {
|
||||
color: #ffffff;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.promotion p {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@media only screen and (max-width: 620px) {
|
||||
.inner-body {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 24px 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
DARK MODE SUPPORT
|
||||
(Für E-Mail-Clients die es unterstützen)
|
||||
============================================ */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.wrapper,
|
||||
.body {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
.inner-body {
|
||||
background-color: #2a2a2a !important;
|
||||
border-color: #404040 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, strong {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
p, .table td {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background-color: #1e3a5f !important;
|
||||
}
|
||||
|
||||
.panel-content p {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.subcopy {
|
||||
border-top-color: #404040 !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
color: #ffffff !important;
|
||||
border-bottom-color: #404040 !important;
|
||||
}
|
||||
|
||||
.table td {
|
||||
border-bottom-color: #333333 !important;
|
||||
}
|
||||
}
|
||||
297
resources/views/vendor/mail/html/themes/default.css
vendored
Normal file
297
resources/views/vendor/mail/html/themes/default.css
vendored
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
/* Base */
|
||||
|
||||
body,
|
||||
body *:not(html):not(style):not(br):not(tr):not(code) {
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-text-size-adjust: none;
|
||||
background-color: #ffffff;
|
||||
color: #52525b;
|
||||
height: 100%;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #18181b;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
|
||||
h1 {
|
||||
color: #18181b;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.5em;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
|
||||
.wrapper {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
background-color: #fafafa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
.header {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: #18181b;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
|
||||
.logo {
|
||||
height: 75px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
max-height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
|
||||
.body {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #fafafa;
|
||||
border-top: 1px solid #fafafa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner-body {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 570px;
|
||||
background-color: #ffffff;
|
||||
border-color: #e4e4e7;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
width: 570px;
|
||||
}
|
||||
|
||||
.inner-body a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Subcopy */
|
||||
|
||||
.subcopy {
|
||||
border-top: 1px solid #e4e4e7;
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.subcopy p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
|
||||
.footer {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 570px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
color: #a1a1aa;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #a1a1aa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
|
||||
.table table {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 30px auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table th {
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
margin: 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
color: #52525b;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
max-width: 100vw;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.action {
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
-premailer-width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
float: unset;
|
||||
}
|
||||
|
||||
.button {
|
||||
-webkit-text-size-adjust: none;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button-blue,
|
||||
.button-primary {
|
||||
background-color: #18181b;
|
||||
border-bottom: 8px solid #18181b;
|
||||
border-left: 18px solid #18181b;
|
||||
border-right: 18px solid #18181b;
|
||||
border-top: 8px solid #18181b;
|
||||
}
|
||||
|
||||
.button-green,
|
||||
.button-success {
|
||||
background-color: #16a34a;
|
||||
border-bottom: 8px solid #16a34a;
|
||||
border-left: 18px solid #16a34a;
|
||||
border-right: 18px solid #16a34a;
|
||||
border-top: 8px solid #16a34a;
|
||||
}
|
||||
|
||||
.button-red,
|
||||
.button-error {
|
||||
background-color: #dc2626;
|
||||
border-bottom: 8px solid #dc2626;
|
||||
border-left: 18px solid #dc2626;
|
||||
border-right: 18px solid #dc2626;
|
||||
border-top: 8px solid #dc2626;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
|
||||
.panel {
|
||||
border-left: #18181b solid 4px;
|
||||
margin: 21px 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background-color: #fafafa;
|
||||
color: #52525b;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel-content p {
|
||||
color: #52525b;
|
||||
}
|
||||
|
||||
.panel-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel-item p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
1
resources/views/vendor/mail/text/button.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/button.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}: {{ $url }}
|
||||
1
resources/views/vendor/mail/text/footer.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/footer.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}
|
||||
1
resources/views/vendor/mail/text/header.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/header.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}: {{ $url }}
|
||||
9
resources/views/vendor/mail/text/layout.blade.php
vendored
Normal file
9
resources/views/vendor/mail/text/layout.blade.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{!! strip_tags($header ?? '') !!}
|
||||
|
||||
{!! strip_tags($slot) !!}
|
||||
@isset($subcopy)
|
||||
|
||||
{!! strip_tags($subcopy) !!}
|
||||
@endisset
|
||||
|
||||
{!! strip_tags($footer ?? '') !!}
|
||||
27
resources/views/vendor/mail/text/message.blade.php
vendored
Normal file
27
resources/views/vendor/mail/text/message.blade.php
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<x-mail::layout>
|
||||
{{-- Header --}}
|
||||
<x-slot:header>
|
||||
<x-mail::header :url="config('app.url')">
|
||||
{{ config('app.name') }}
|
||||
</x-mail::header>
|
||||
</x-slot:header>
|
||||
|
||||
{{-- Body --}}
|
||||
{{ $slot }}
|
||||
|
||||
{{-- Subcopy --}}
|
||||
@isset($subcopy)
|
||||
<x-slot:subcopy>
|
||||
<x-mail::subcopy>
|
||||
{{ $subcopy }}
|
||||
</x-mail::subcopy>
|
||||
</x-slot:subcopy>
|
||||
@endisset
|
||||
|
||||
{{-- Footer --}}
|
||||
<x-slot:footer>
|
||||
<x-mail::footer>
|
||||
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
|
||||
</x-mail::footer>
|
||||
</x-slot:footer>
|
||||
</x-mail::layout>
|
||||
1
resources/views/vendor/mail/text/panel.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/panel.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}
|
||||
1
resources/views/vendor/mail/text/subcopy.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/subcopy.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}
|
||||
1
resources/views/vendor/mail/text/table.blade.php
vendored
Normal file
1
resources/views/vendor/mail/text/table.blade.php
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{{ $slot }}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.hero-slider />
|
||||
<livewire:web.components.sections.vision-section bg="" />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent" section="about_philosophie" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.f-a-q bg="bg-background" section="faq" />
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
<div class="hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white dark:bg-gray-700 ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" id="user-menu">
|
||||
<a href="{{ route('admin.settings.profile') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Profil</a>
|
||||
<a href="{{ route('admin.settings.appearance') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Einstellungen</a>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
<form method="POST" action="{{ route('auth.logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600" role="menuitem">Abmelden</button>
|
||||
</form>
|
||||
|
|
|
|||
85
resources/views/web/layouts/web-master-slot.blade.php
Normal file
85
resources/views/web/layouts/web-master-slot.blade.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ $title ?? ($domainName ?? config('app.name', 'Laravel')) }}</title>
|
||||
|
||||
<!-- Domain-spezifisches Favicon -->
|
||||
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
|
||||
@php
|
||||
$primaryFont = \App\Helpers\ThemeHelper::getPrimaryFont();
|
||||
$secondaryFont = \App\Helpers\ThemeHelper::getSecondaryFont();
|
||||
$theme = config('app.theme', 'b2in');
|
||||
@endphp
|
||||
|
||||
@vite([
|
||||
\App\Helpers\ThemeHelper::getThemeCssPath(),
|
||||
'resources/js/app.js'
|
||||
], 'build/' . $theme)
|
||||
|
||||
<!-- Sticky Header Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const topbar = document.getElementById('topbar');
|
||||
const header = document.getElementById('header');
|
||||
|
||||
if (!topbar || !header) return;
|
||||
|
||||
let topbarHeight = topbar.offsetHeight;
|
||||
let isHeaderSticky = false;
|
||||
|
||||
function updateHeaderPosition() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
if (scrollTop >= topbarHeight && !isHeaderSticky) {
|
||||
header.classList.remove('header-normal');
|
||||
header.classList.add('header-sticky');
|
||||
isHeaderSticky = true;
|
||||
} else if (scrollTop < topbarHeight && isHeaderSticky) {
|
||||
header.classList.remove('header-sticky');
|
||||
header.classList.add('header-normal');
|
||||
isHeaderSticky = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateHeaderPosition();
|
||||
window.addEventListener('scroll', updateHeaderPosition);
|
||||
window.addEventListener('resize', function() {
|
||||
topbarHeight = topbar.offsetHeight;
|
||||
updateHeaderPosition();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@stack('styles')
|
||||
|
||||
@if ($primaryFont === 'Inter' && $secondaryFont === 'IBM Plex Sans')
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Merriweather')
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|merriweather:400,700" rel="stylesheet" />
|
||||
@elseif($primaryFont === 'Inter' && $secondaryFont === 'Ephesis')
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ephesis:400" rel="stylesheet" />
|
||||
@elseif($primaryFont === 'Inter' && $secondaryFont === 'EB Garamond')
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|eb-garamond:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
@else
|
||||
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700|ibm-plex-sans:400,500,600,700"
|
||||
rel="stylesheet" />
|
||||
@endif
|
||||
</head>
|
||||
<body class="antialiased bg-background text-foreground">
|
||||
<livewire:web.components.ui.top-bar />
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -91,6 +91,7 @@
|
|||
<!-- TopBar - statisch oben -->
|
||||
<livewire:web.components.ui.top-bar />
|
||||
@yield('content')
|
||||
|
||||
<!-- Additional Scripts -->
|
||||
@stack('scripts')
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.portfolio />
|
||||
<livewire:web.components.sections.c-t-a-section section="cta_section_portfolio" />
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.hero-image />
|
||||
<livewire:web.components.sections.content-section layout="left" bg="bg-white" section="content_section_left" />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent" section="content_section_right" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.hero-slider />
|
||||
<livewire:web.components.sections.vision-section bg="" />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent" section="about_philosophie" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<livewire:web.components.sections.hero-slider />
|
||||
<livewire:web.components.sections.vision-section bg="" />
|
||||
<livewire:web.components.sections.content-section layout="right" bg="bg-accent" section="about_philosophie" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main class="section-padding">
|
||||
<main class="section-padding variante-glass-flow">
|
||||
<div class="container-padding">
|
||||
<!-- Theme Info Section -->
|
||||
<livewire:web.components.demo.theme-info />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div class="min-h-screen bg-background">
|
||||
<livewire:web.components.ui.header />
|
||||
|
||||
<main>
|
||||
<main class="variante-glass-flow">
|
||||
<section class="section-padding">
|
||||
<div class="container-padding">
|
||||
<div class="text-center mb-16">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue