refactor: Remove deprecated, bad examples
This commit is contained in:
parent
ce39b7ba34
commit
aa5c7e77fc
@ -1,101 +0,0 @@
|
|||||||
// interfaces/http/handlers/time_entry_handler.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/email/timetracker/internal/application/timetracking"
|
|
||||||
"github.com/email/timetracker/internal/interfaces/http/dto"
|
|
||||||
"github.com/email/timetracker/internal/interfaces/http/middleware"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeEntryHandler behandelt HTTP-Anfragen für Zeitbuchungen
|
|
||||||
type TimeEntryHandler struct {
|
|
||||||
createTimeEntryUseCase *timetracking.CreateTimeEntryUseCase
|
|
||||||
updateTimeEntryUseCase *timetracking.UpdateTimeEntryUseCase
|
|
||||||
listTimeEntriesUseCase *timetracking.ListTimeEntriesUseCase
|
|
||||||
deleteTimeEntryUseCase *timetracking.DeleteTimeEntryUseCase
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimeEntryHandler erstellt einen neuen TimeEntryHandler
|
|
||||||
func NewTimeEntryHandler(
|
|
||||||
createTimeEntryUseCase *timetracking.CreateTimeEntryUseCase,
|
|
||||||
updateTimeEntryUseCase *timetracking.UpdateTimeEntryUseCase,
|
|
||||||
listTimeEntriesUseCase *timetracking.ListTimeEntriesUseCase,
|
|
||||||
deleteTimeEntryUseCase *timetracking.DeleteTimeEntryUseCase,
|
|
||||||
) *TimeEntryHandler {
|
|
||||||
return &TimeEntryHandler{
|
|
||||||
createTimeEntryUseCase: createTimeEntryUseCase,
|
|
||||||
updateTimeEntryUseCase: updateTimeEntryUseCase,
|
|
||||||
listTimeEntriesUseCase: listTimeEntriesUseCase,
|
|
||||||
deleteTimeEntryUseCase: deleteTimeEntryUseCase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterRoutes registriert die Routen am Router
|
|
||||||
func (h *TimeEntryHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|
||||||
timeEntries := router.Group("/time-entries")
|
|
||||||
{
|
|
||||||
timeEntries.GET("", h.ListTimeEntries)
|
|
||||||
timeEntries.POST("", h.CreateTimeEntry)
|
|
||||||
timeEntries.GET("/:id", h.GetTimeEntry)
|
|
||||||
timeEntries.PUT("/:id", h.UpdateTimeEntry)
|
|
||||||
timeEntries.DELETE("/:id", h.DeleteTimeEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTimeEntry behandelt die Erstellung einer neuen Zeitbuchung
|
|
||||||
func (h *TimeEntryHandler) CreateTimeEntry(c *gin.Context) {
|
|
||||||
var req dto.CreateTimeEntryRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tenant-ID aus dem Kontext extrahieren
|
|
||||||
companyID, exists := middleware.GetCompanyID(c)
|
|
||||||
if !exists {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Company ID not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benutzer-ID aus dem Kontext oder Request
|
|
||||||
var userID uuid.UUID
|
|
||||||
if req.UserID != nil {
|
|
||||||
userID = *req.UserID
|
|
||||||
} else {
|
|
||||||
currentUserID, exists := middleware.GetUserID(c)
|
|
||||||
if !exists {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "User ID not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
userID = currentUserID
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command erstellen
|
|
||||||
cmd := timetracking.CreateTimeEntryCommand{
|
|
||||||
UserID: userID,
|
|
||||||
ProjectID: req.ProjectID,
|
|
||||||
ActivityID: req.ActivityID,
|
|
||||||
TaskID: req.TaskID,
|
|
||||||
StartTime: req.StartTime,
|
|
||||||
EndTime: req.EndTime,
|
|
||||||
Description: req.Description,
|
|
||||||
BillablePercentage: req.BillablePercentage,
|
|
||||||
}
|
|
||||||
|
|
||||||
// UseCase ausführen
|
|
||||||
result := h.createTimeEntryUseCase.Execute(c.Request.Context(), companyID, cmd)
|
|
||||||
if result.IsFailure() {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error().Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeEntry in Response-DTO umwandeln
|
|
||||||
timeEntry := result.Value()
|
|
||||||
response := dto.MapTimeEntryToDTO(*timeEntry)
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, response)
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
// domain/repositories/time_entry_repository.go
|
|
||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/email/timetracker/internal/domain/entities"
|
|
||||||
"github.com/email/timetracker/pkg/functional"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeEntryFilter enthält Filter für die Suche nach Zeitbuchungen
|
|
||||||
type TimeEntryFilter struct {
|
|
||||||
UserID *uuid.UUID
|
|
||||||
ProjectID *uuid.UUID
|
|
||||||
CustomerID *uuid.UUID
|
|
||||||
StartDate *time.Time
|
|
||||||
EndDate *time.Time
|
|
||||||
ActivityID *uuid.UUID
|
|
||||||
TaskID *uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeEntryRepository Interface für den Zugriff auf Zeitbuchungen
|
|
||||||
type TimeEntryRepository interface {
|
|
||||||
// FindByID sucht eine Zeitbuchung anhand ihrer ID
|
|
||||||
FindByID(ctx context.Context, companyID, id uuid.UUID) functional.Result[*entities.TimeEntry]
|
|
||||||
|
|
||||||
// FindAll sucht alle Zeitbuchungen mit optionalen Filtern
|
|
||||||
FindAll(ctx context.Context, companyID uuid.UUID, filter TimeEntryFilter) functional.Result[[]entities.TimeEntry]
|
|
||||||
|
|
||||||
// Create erstellt eine neue Zeitbuchung
|
|
||||||
Create(ctx context.Context, entry *entities.TimeEntry) functional.Result[*entities.TimeEntry]
|
|
||||||
|
|
||||||
// Update aktualisiert eine bestehende Zeitbuchung
|
|
||||||
Update(ctx context.Context, entry *entities.TimeEntry) functional.Result[*entities.TimeEntry]
|
|
||||||
|
|
||||||
// Delete löscht eine Zeitbuchung
|
|
||||||
Delete(ctx context.Context, companyID, id uuid.UUID) functional.Result[bool]
|
|
||||||
|
|
||||||
// GetSummary berechnet eine Zusammenfassung der Zeitbuchungen
|
|
||||||
GetSummary(ctx context.Context, companyID uuid.UUID, filter TimeEntryFilter) functional.Result[TimeEntrySummary]
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeEntrySummary enthält zusammengefasste Informationen über Zeitbuchungen
|
|
||||||
type TimeEntrySummary struct {
|
|
||||||
TotalDuration int
|
|
||||||
TotalBillableDuration int
|
|
||||||
TotalAmount float64
|
|
||||||
TotalBillableAmount float64
|
|
||||||
EntriesCount int
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
// application/timetracking/create_time_entry.go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/email/timetracker/internal/domain/entities"
|
|
||||||
"github.com/email/timetracker/internal/domain/repositories"
|
|
||||||
"github.com/email/timetracker/pkg/functional"
|
|
||||||
"github.com/email/timetracker/pkg/validator"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateTimeEntryCommand enthält die Daten zum Erstellen einer Zeitbuchung
|
|
||||||
type CreateTimeEntryCommand struct {
|
|
||||||
UserID uuid.UUID
|
|
||||||
ProjectID uuid.UUID
|
|
||||||
ActivityID uuid.UUID
|
|
||||||
TaskID *uuid.UUID
|
|
||||||
StartTime time.Time
|
|
||||||
EndTime time.Time
|
|
||||||
Description string
|
|
||||||
BillablePercentage int
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTimeEntryUseCase repräsentiert den Anwendungsfall zum Erstellen einer Zeitbuchung
|
|
||||||
type CreateTimeEntryUseCase struct {
|
|
||||||
timeEntryRepo repositories.TimeEntryRepository
|
|
||||||
projectRepo repositories.ProjectRepository
|
|
||||||
activityRepo repositories.ActivityRepository
|
|
||||||
userRepo repositories.UserRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCreateTimeEntryUseCase erstellt eine neue Instanz des UseCase
|
|
||||||
func NewCreateTimeEntryUseCase(
|
|
||||||
timeEntryRepo repositories.TimeEntryRepository,
|
|
||||||
projectRepo repositories.ProjectRepository,
|
|
||||||
activityRepo repositories.ActivityRepository,
|
|
||||||
userRepo repositories.UserRepository,
|
|
||||||
) *CreateTimeEntryUseCase {
|
|
||||||
return &CreateTimeEntryUseCase{
|
|
||||||
timeEntryRepo: timeEntryRepo,
|
|
||||||
projectRepo: projectRepo,
|
|
||||||
activityRepo: activityRepo,
|
|
||||||
userRepo: userRepo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute führt den Anwendungsfall aus
|
|
||||||
func (uc *CreateTimeEntryUseCase) Execute(ctx context.Context, companyID uuid.UUID, cmd CreateTimeEntryCommand) functional.Result[*entities.TimeEntry] {
|
|
||||||
// Validierung
|
|
||||||
if err := validator.ValidateStruct(cmd); err != nil {
|
|
||||||
return functional.Failure[*entities.TimeEntry](err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Überprüfen, ob Projekt existiert und zum gleichen Tenant gehört
|
|
||||||
projectResult := uc.projectRepo.FindByID(ctx, companyID, cmd.ProjectID)
|
|
||||||
if projectResult.IsFailure() {
|
|
||||||
return functional.Failure[*entities.TimeEntry](projectResult.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Überprüfen, ob Activity existiert und zum gleichen Tenant gehört
|
|
||||||
activityResult := uc.activityRepo.FindByID(ctx, companyID, cmd.ActivityID)
|
|
||||||
if activityResult.IsFailure() {
|
|
||||||
return functional.Failure[*entities.TimeEntry](activityResult.Error())
|
|
||||||
}
|
|
||||||
activity := activityResult.Value()
|
|
||||||
|
|
||||||
// Benutzer abrufen für den Stundensatz
|
|
||||||
userResult := uc.userRepo.FindByID(ctx, companyID, cmd.UserID)
|
|
||||||
if userResult.IsFailure() {
|
|
||||||
return functional.Failure[*entities.TimeEntry](userResult.Error())
|
|
||||||
}
|
|
||||||
user := userResult.Value()
|
|
||||||
|
|
||||||
// Berechnung der Dauer in Minuten
|
|
||||||
durationMinutes := int(cmd.EndTime.Sub(cmd.StartTime).Minutes())
|
|
||||||
|
|
||||||
// TimeEntry erstellen
|
|
||||||
timeEntry := &entities.TimeEntry{
|
|
||||||
TenantEntity: entities.TenantEntity{
|
|
||||||
CompanyID: companyID,
|
|
||||||
},
|
|
||||||
UserID: cmd.UserID,
|
|
||||||
ProjectID: cmd.ProjectID,
|
|
||||||
ActivityID: cmd.ActivityID,
|
|
||||||
TaskID: cmd.TaskID,
|
|
||||||
StartTime: cmd.StartTime,
|
|
||||||
EndTime: cmd.EndTime,
|
|
||||||
DurationMinutes: durationMinutes,
|
|
||||||
Description: cmd.Description,
|
|
||||||
BillablePercentage: cmd.BillablePercentage,
|
|
||||||
BillingRate: activity.BillingRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichern der TimeEntry
|
|
||||||
return uc.timeEntryRepo.Create(ctx, timeEntry)
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BaseEntity enthält gemeinsame Felder für alle Entitäten
|
|
||||||
type BaseEntity struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeCreate setzt eine neue UUID vor dem Erstellen
|
|
||||||
func (base *BaseEntity) BeforeCreate(tx *gorm.DB) error {
|
|
||||||
if base.ID == uuid.Nil {
|
|
||||||
base.ID = uuid.New()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TenantEntity erweitert BaseEntity um Company-ID für Multi-Tenancy
|
|
||||||
type TenantEntity struct {
|
|
||||||
BaseEntity
|
|
||||||
CompanyID uuid.UUID `gorm:"type:uuid;index:idx_tenant"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role repräsentiert eine Benutzerrolle
|
|
||||||
type Role struct {
|
|
||||||
BaseEntity
|
|
||||||
Name string `gorm:"unique;not null"`
|
|
||||||
Description string
|
|
||||||
Permissions []Permission `gorm:"many2many:role_permissions;"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permission repräsentiert eine einzelne Berechtigung
|
|
||||||
type Permission struct {
|
|
||||||
BaseEntity
|
|
||||||
Resource string `gorm:"not null"`
|
|
||||||
Action string `gorm:"not null"`
|
|
||||||
UniqueID string `gorm:"uniqueIndex"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// User repräsentiert einen Benutzer im System
|
|
||||||
type User struct {
|
|
||||||
TenantEntity
|
|
||||||
Email string `gorm:"uniqueIndex;not null"`
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
PasswordHash string `gorm:"not null"`
|
|
||||||
RoleID uuid.UUID `gorm:"type:uuid"`
|
|
||||||
Role Role `gorm:"foreignKey:RoleID"`
|
|
||||||
HourlyRate float64
|
|
||||||
IsActive bool `gorm:"default:true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FullName gibt den vollständigen Namen des Benutzers zurück
|
|
||||||
func (u User) FullName() string {
|
|
||||||
return u.FirstName + " " + u.LastName
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeEntry repräsentiert eine Zeitbuchung
|
|
||||||
type TimeEntry struct {
|
|
||||||
TenantEntity
|
|
||||||
UserID uuid.UUID `gorm:"type:uuid;index:idx_user"`
|
|
||||||
User User `gorm:"foreignKey:UserID"`
|
|
||||||
ProjectID uuid.UUID `gorm:"type:uuid;index:idx_project"`
|
|
||||||
Project Project `gorm:"foreignKey:ProjectID"`
|
|
||||||
ActivityID uuid.UUID `gorm:"type:uuid"`
|
|
||||||
Activity Activity `gorm:"foreignKey:ActivityID"`
|
|
||||||
TaskID *uuid.UUID `gorm:"type:uuid;null"`
|
|
||||||
Task *Task `gorm:"foreignKey:TaskID"`
|
|
||||||
StartTime time.Time `gorm:"not null;index:idx_time_range"`
|
|
||||||
EndTime time.Time `gorm:"not null;index:idx_time_range"`
|
|
||||||
DurationMinutes int `gorm:"not null"`
|
|
||||||
Description string
|
|
||||||
BillablePercentage int `gorm:"default:100"`
|
|
||||||
BillingRate float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Project struct {
|
|
||||||
TenantEntity
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Activity struct {
|
|
||||||
TenantEntity
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Task struct {
|
|
||||||
TenantEntity
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CalculateBillableAmount berechnet den abrechenbaren Betrag
|
|
||||||
func (t TimeEntry) CalculateBillableAmount() float64 {
|
|
||||||
hours := float64(t.DurationMinutes) / 60.0
|
|
||||||
return hours * t.BillingRate * (float64(t.BillablePercentage) / 100.0)
|
|
||||||
}
|
|
@ -1,136 +0,0 @@
|
|||||||
// presentation/components/timeTracker/Timer/Timer.tsx
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useTimeTracking } from '../../../hooks/useTimeTracking';
|
|
||||||
import { pipe, Option, fromNullable } from '../../../../utils/fp/option';
|
|
||||||
import { Button } from '../../common/Button';
|
|
||||||
import { formatDuration } from '../../../../utils/date/dateUtils';
|
|
||||||
|
|
||||||
interface TimerProps {
|
|
||||||
onComplete?: (duration: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Timer: React.FC<TimerProps> = ({ onComplete }) => {
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
|
||||||
const [startTime, setStartTime] = useState<Option<Date>>(Option.none());
|
|
||||||
const [elapsedTime, setElapsedTime] = useState(0);
|
|
||||||
const [selectedProject, setSelectedProject] = useState<Option<string>>(Option.none());
|
|
||||||
const [selectedActivity, setSelectedActivity] = useState<Option<string>>(Option.none());
|
|
||||||
|
|
||||||
const { lastTimeEntry, projects, activities } = useTimeTracking();
|
|
||||||
|
|
||||||
// Beim ersten Rendering die letzte Zeitbuchung laden
|
|
||||||
useEffect(() => {
|
|
||||||
pipe(
|
|
||||||
fromNullable(lastTimeEntry),
|
|
||||||
Option.map(entry => {
|
|
||||||
setSelectedProject(Option.some(entry.projectId));
|
|
||||||
setSelectedActivity(Option.some(entry.activityId));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [lastTimeEntry]);
|
|
||||||
|
|
||||||
// Timer-Logik
|
|
||||||
useEffect(() => {
|
|
||||||
let interval: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
if (isRunning) {
|
|
||||||
interval = setInterval(() => {
|
|
||||||
const now = new Date();
|
|
||||||
pipe(
|
|
||||||
startTime,
|
|
||||||
Option.map(start => {
|
|
||||||
const diff = now.getTime() - start.getTime();
|
|
||||||
setElapsedTime(Math.floor(diff / 1000));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, 1000);
|
|
||||||
} else if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (interval) clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, [isRunning, startTime]);
|
|
||||||
|
|
||||||
// Timer starten
|
|
||||||
const handleStart = () => {
|
|
||||||
setStartTime(Option.some(new Date()));
|
|
||||||
setIsRunning(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Timer stoppen
|
|
||||||
const handleStop = () => {
|
|
||||||
setIsRunning(false);
|
|
||||||
|
|
||||||
// Prüfen, ob Projekt und Aktivität ausgewählt wurden
|
|
||||||
const projectId = pipe(
|
|
||||||
selectedProject,
|
|
||||||
Option.getOrElse(() => '')
|
|
||||||
);
|
|
||||||
|
|
||||||
const activityId = pipe(
|
|
||||||
selectedActivity,
|
|
||||||
Option.getOrElse(() => '')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (projectId && activityId && onComplete) {
|
|
||||||
onComplete(elapsedTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer zurücksetzen
|
|
||||||
setElapsedTime(0);
|
|
||||||
setStartTime(Option.none());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-4">
|
|
||||||
<div className="text-4xl text-center font-mono mb-4">
|
|
||||||
{formatDuration(elapsedTime)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Projekt
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
|
||||||
value={pipe(selectedProject, Option.getOrElse(() => ''))}
|
|
||||||
onChange={(e) => setSelectedProject(Option.some(e.target.value))}
|
|
||||||
disabled={isRunning}
|
|
||||||
>
|
|
||||||
<option value="">Projekt auswählen</option>
|
|
||||||
{projects.map((project) => (
|
|
||||||
<option key={project.id} value={project.id}>
|
|
||||||
{project.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
Tätigkeit
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className="w-full border border-gray-300 rounded-md px-3 py-2"
|
|
||||||
value={pipe(selectedActivity, Option.getOrElse(() => ''))}
|
|
||||||
onChange={(e) => setSelectedActivity(Option.some(e.target.value))}
|
|
||||||
disabled={isRunning}
|
|
||||||
>
|
|
||||||
<option value="">Tätigkeit auswählen</option>
|
|
||||||
{activities.map((activity) => (
|
|
||||||
<option key={activity.id} value={activity.id}>
|
|
||||||
{activity.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-center">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user