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