e5623d2f84
- Add Infrastructure Health section with CPU/Memory/Disk panels - Add Service Status section with PM2/Docker panels - Create GrafanaPanel component for iframe embedding - Build passes successfully
676 lines
23 KiB
Go
676 lines
23 KiB
Go
package employee
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Handler hanterar Employee Lifecycle API
|
|
type Handler struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewHandler(db *sql.DB) *Handler {
|
|
return &Handler{DB: db}
|
|
}
|
|
|
|
// ==========================================
|
|
// EMPLOYEE CRUD
|
|
// ==========================================
|
|
|
|
// Employee representerar en medarbetare
|
|
type Employee struct {
|
|
ID string `json:"id"`
|
|
TenantID string `json:"tenant_id"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Email string `json:"email"`
|
|
Phone *string `json:"phone,omitempty"`
|
|
PersonalNumber *string `json:"personal_number,omitempty"`
|
|
BirthDate *time.Time `json:"birth_date,omitempty"`
|
|
AvatarURL *string `json:"avatar_url,omitempty"`
|
|
Bio *string `json:"bio,omitempty"`
|
|
EmploymentType string `json:"employment_type"`
|
|
EmploymentStatus string `json:"employment_status"`
|
|
StartDate *time.Time `json:"start_date,omitempty"`
|
|
EndDate *time.Time `json:"end_date,omitempty"`
|
|
ProbationEndDate *time.Time `json:"probation_end_date,omitempty"`
|
|
NoticePeriodDays int `json:"notice_period_days"`
|
|
DepartmentID *string `json:"department_id,omitempty"`
|
|
TeamID *string `json:"team_id,omitempty"`
|
|
ManagerID *string `json:"manager_id,omitempty"`
|
|
RoleID *string `json:"role_id,omitempty"`
|
|
Salary *float64 `json:"salary,omitempty"`
|
|
SalaryCurrency string `json:"salary_currency"`
|
|
BankAccount *string `json:"bank_account,omitempty"`
|
|
BankClearing *string `json:"bank_clearing,omitempty"`
|
|
Address *string `json:"address,omitempty"`
|
|
PostalCode *string `json:"postal_code,omitempty"`
|
|
City *string `json:"city,omitempty"`
|
|
Country string `json:"country"`
|
|
EmergencyContactName *string `json:"emergency_contact_name,omitempty"`
|
|
EmergencyContactPhone *string `json:"emergency_contact_phone,omitempty"`
|
|
EmergencyContactRelation *string `json:"emergency_contact_relation,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ListEmployees returnerar alla medarbetare
|
|
func (h *Handler) ListEmployees(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, tenant_id, first_name, last_name, email, phone, employment_type, employment_status,
|
|
start_date, department_id, team_id, manager_id, role_id, created_at
|
|
FROM employees
|
|
WHERE employment_status != 'terminated'
|
|
ORDER BY created_at DESC
|
|
`)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var employees []Employee
|
|
for rows.Next() {
|
|
var e Employee
|
|
var phone sql.NullString
|
|
var startDate sql.NullTime
|
|
var deptID, teamID, managerID, roleID sql.NullString
|
|
|
|
rows.Scan(&e.ID, &e.TenantID, &e.FirstName, &e.LastName, &e.Email, &phone,
|
|
&e.EmploymentType, &e.EmploymentStatus, &startDate,
|
|
&deptID, &teamID, &managerID, &roleID, &e.CreatedAt)
|
|
|
|
if phone.Valid { e.Phone = &phone.String }
|
|
if startDate.Valid { e.StartDate = &startDate.Time }
|
|
if deptID.Valid { e.DepartmentID = &deptID.String }
|
|
if teamID.Valid { e.TeamID = &teamID.String }
|
|
if managerID.Valid { e.ManagerID = &managerID.String }
|
|
if roleID.Valid { e.RoleID = &roleID.String }
|
|
|
|
employees = append(employees, e)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"employees": employees,
|
|
"total": len(employees),
|
|
})
|
|
}
|
|
|
|
// GetEmployee returnerar en specifik medarbetare
|
|
func (h *Handler) GetEmployee(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
var e Employee
|
|
var phone, personalNumber, avatarURL, bio, bankAccount, bankClearing sql.NullString
|
|
var birthDate, startDate, endDate, probationEnd sql.NullTime
|
|
var deptID, teamID, managerID, roleID sql.NullString
|
|
var salary sql.NullFloat64
|
|
var address, postalCode, city, emergencyName, emergencyPhone, emergencyRelation sql.NullString
|
|
|
|
err := h.DB.QueryRow(`
|
|
SELECT id, tenant_id, first_name, last_name, email, phone, personal_number, birth_date,
|
|
avatar_url, bio, employment_type, employment_status, start_date, end_date, probation_end_date,
|
|
notice_period_days, department_id, team_id, manager_id, role_id,
|
|
salary, salary_currency, bank_account, bank_clearing,
|
|
address, postal_code, city, country,
|
|
emergency_contact_name, emergency_contact_phone, emergency_contact_relation,
|
|
created_at, updated_at
|
|
FROM employees WHERE id = $1
|
|
`, id).Scan(
|
|
&e.ID, &e.TenantID, &e.FirstName, &e.LastName, &e.Email, &phone, &personalNumber, &birthDate,
|
|
&avatarURL, &bio, &e.EmploymentType, &e.EmploymentStatus, &startDate, &endDate, &probationEnd,
|
|
&e.NoticePeriodDays, &deptID, &teamID, &managerID, &roleID,
|
|
&salary, &e.SalaryCurrency, &bankAccount, &bankClearing,
|
|
&address, &postalCode, &city, &e.Country,
|
|
&emergencyName, &emergencyPhone, &emergencyRelation,
|
|
&e.CreatedAt, &e.UpdatedAt,
|
|
)
|
|
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, `{"error":"employee not found"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Sätt nullable fält
|
|
if phone.Valid { e.Phone = &phone.String }
|
|
if personalNumber.Valid { e.PersonalNumber = &personalNumber.String }
|
|
if birthDate.Valid { e.BirthDate = &birthDate.Time }
|
|
if avatarURL.Valid { e.AvatarURL = &avatarURL.String }
|
|
if bio.Valid { e.Bio = &bio.String }
|
|
if startDate.Valid { e.StartDate = &startDate.Time }
|
|
if endDate.Valid { e.EndDate = &endDate.Time }
|
|
if probationEnd.Valid { e.ProbationEndDate = &probationEnd.Time }
|
|
if deptID.Valid { e.DepartmentID = &deptID.String }
|
|
if teamID.Valid { e.TeamID = &teamID.String }
|
|
if managerID.Valid { e.ManagerID = &managerID.String }
|
|
if roleID.Valid { e.RoleID = &roleID.String }
|
|
if salary.Valid { e.Salary = &salary.Float64 }
|
|
if bankAccount.Valid { e.BankAccount = &bankAccount.String }
|
|
if bankClearing.Valid { e.BankClearing = &bankClearing.String }
|
|
if address.Valid { e.Address = &address.String }
|
|
if postalCode.Valid { e.PostalCode = &postalCode.String }
|
|
if city.Valid { e.City = &city.String }
|
|
if emergencyName.Valid { e.EmergencyContactName = &emergencyName.String }
|
|
if emergencyPhone.Valid { e.EmergencyContactPhone = &emergencyPhone.String }
|
|
if emergencyRelation.Valid { e.EmergencyContactRelation = &emergencyRelation.String }
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(e)
|
|
}
|
|
|
|
// CreateEmployee skapar en ny medarbetare
|
|
func (h *Handler) CreateEmployee(w http.ResponseWriter, r *http.Request) {
|
|
var req Employee
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var id string
|
|
err := h.DB.QueryRow(`
|
|
INSERT INTO employees (tenant_id, first_name, last_name, email, phone, personal_number,
|
|
employment_type, employment_status, start_date, department_id, team_id, manager_id, role_id,
|
|
salary, salary_currency, bank_account, bank_clearing,
|
|
address, postal_code, city, country)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
|
|
RETURNING id
|
|
`, req.TenantID, req.FirstName, req.LastName, req.Email, req.Phone, req.PersonalNumber,
|
|
req.EmploymentType, req.EmploymentStatus, req.StartDate, req.DepartmentID, req.TeamID, req.ManagerID, req.RoleID,
|
|
req.Salary, req.SalaryCurrency, req.BankAccount, req.BankClearing,
|
|
req.Address, req.PostalCode, req.City, req.Country).Scan(&id)
|
|
|
|
if err != nil {
|
|
http.Error(w, `{"error":"failed to create employee"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Skapa timeline event
|
|
h.DB.Exec(`
|
|
INSERT INTO employee_timeline_events (employee_id, event_type, event_date, title, description)
|
|
VALUES ($1, 'hired', $2, 'Anställd', 'Anställning påbörjad')
|
|
`, id, time.Now())
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": id,
|
|
"message": "Employee created",
|
|
})
|
|
}
|
|
|
|
// UpdateEmployee uppdaterar en medarbetare
|
|
func (h *Handler) UpdateEmployee(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
var req Employee
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, err := h.DB.Exec(`
|
|
UPDATE employees
|
|
SET first_name = COALESCE($1, first_name),
|
|
last_name = COALESCE($2, last_name),
|
|
email = COALESCE($3, email),
|
|
phone = COALESCE($4, phone),
|
|
employment_type = COALESCE($5, employment_type),
|
|
employment_status = COALESCE($6, employment_status),
|
|
department_id = COALESCE($7, department_id),
|
|
team_id = COALESCE($8, team_id),
|
|
manager_id = COALESCE($9, manager_id),
|
|
role_id = COALESCE($10, role_id),
|
|
salary = COALESCE($11, salary),
|
|
updated_at = NOW()
|
|
WHERE id = $12
|
|
`, req.FirstName, req.LastName, req.Email, req.Phone,
|
|
req.EmploymentType, req.EmploymentStatus,
|
|
req.DepartmentID, req.TeamID, req.ManagerID, req.RoleID,
|
|
req.Salary, id)
|
|
|
|
if err != nil {
|
|
http.Error(w, `{"error":"failed to update employee"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"message": "Employee updated",
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// TIMELINE
|
|
// ==========================================
|
|
|
|
// TimelineEvent representerar en händelse i tidslinjen
|
|
type TimelineEvent struct {
|
|
ID string `json:"id"`
|
|
EmployeeID string `json:"employee_id"`
|
|
EventType string `json:"event_type"`
|
|
EventDate time.Time `json:"event_date"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// GetTimeline returnerar tidslinje för en medarbetare
|
|
func (h *Handler) GetTimeline(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, event_type, event_date, title, description, metadata, created_at
|
|
FROM employee_timeline_events
|
|
WHERE employee_id = $1
|
|
ORDER BY event_date DESC, created_at DESC
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var events []TimelineEvent
|
|
for rows.Next() {
|
|
var e TimelineEvent
|
|
var desc sql.NullString
|
|
var meta []byte
|
|
rows.Scan(&e.ID, &e.EventType, &e.EventDate, &e.Title, &desc, &meta, &e.CreatedAt)
|
|
if desc.Valid { e.Description = &desc.String }
|
|
if len(meta) > 0 {
|
|
json.Unmarshal(meta, &e.Metadata)
|
|
}
|
|
e.EmployeeID = employeeID
|
|
events = append(events, e)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"events": events,
|
|
"total": len(events),
|
|
})
|
|
}
|
|
|
|
// AddTimelineEvent lägger till en händelse
|
|
func (h *Handler) AddTimelineEvent(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
var req struct {
|
|
EventType string `json:"event_type"`
|
|
EventDate time.Time `json:"event_date"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
metaJSON, _ := json.Marshal(req.Metadata)
|
|
|
|
var id string
|
|
err := h.DB.QueryRow(`
|
|
INSERT INTO employee_timeline_events (employee_id, event_type, event_date, title, description, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id
|
|
`, employeeID, req.EventType, req.EventDate, req.Title, req.Description, metaJSON).Scan(&id)
|
|
|
|
if err != nil {
|
|
http.Error(w, `{"error":"failed to add event"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": id,
|
|
"message": "Timeline event added",
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// COMPETENCES
|
|
// ==========================================
|
|
|
|
// Competence representerar en kompetens
|
|
type Competence struct {
|
|
ID string `json:"id"`
|
|
TenantID string `json:"tenant_id"`
|
|
Name string `json:"name"`
|
|
Category string `json:"category"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// EmployeeCompetence representerar en medarbetares kompetens
|
|
type EmployeeCompetence struct {
|
|
ID string `json:"id"`
|
|
EmployeeID string `json:"employee_id"`
|
|
CompetenceID string `json:"competence_id"`
|
|
Competence Competence `json:"competence"`
|
|
Level int `json:"level"`
|
|
AcquiredDate *time.Time `json:"acquired_date,omitempty"`
|
|
ValidatedBy *string `json:"validated_by,omitempty"`
|
|
ValidationDate *time.Time `json:"validation_date,omitempty"`
|
|
Notes string `json:"notes"`
|
|
}
|
|
|
|
// GetCompetences returnerar kompetenser för en medarbetare
|
|
func (h *Handler) GetCompetences(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT ec.id, ec.competence_id, c.name, c.category, c.description,
|
|
ec.level, ec.acquired_date, ec.validated_by, ec.validation_date, ec.notes
|
|
FROM employee_competences ec
|
|
JOIN competences c ON ec.competence_id = c.id
|
|
WHERE ec.employee_id = $1
|
|
ORDER BY c.category, c.name
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var competences []EmployeeCompetence
|
|
for rows.Next() {
|
|
var ec EmployeeCompetence
|
|
var acquired, validation sql.NullTime
|
|
var validatedBy sql.NullString
|
|
|
|
rows.Scan(&ec.ID, &ec.CompetenceID, &ec.Competence.Name, &ec.Competence.Category, &ec.Competence.Description,
|
|
&ec.Level, &acquired, &validatedBy, &validation, &ec.Notes)
|
|
|
|
if acquired.Valid { ec.AcquiredDate = &acquired.Time }
|
|
if validatedBy.Valid { ec.ValidatedBy = &validatedBy.String }
|
|
if validation.Valid { ec.ValidationDate = &validation.Time }
|
|
|
|
competences = append(competences, ec)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"competences": competences,
|
|
"total": len(competences),
|
|
})
|
|
}
|
|
|
|
// AddCompetence lägger till en kompetens
|
|
func (h *Handler) AddCompetence(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
var req struct {
|
|
CompetenceID string `json:"competence_id"`
|
|
Level int `json:"level"`
|
|
AcquiredDate *time.Time `json:"acquired_date"`
|
|
Notes string `json:"notes"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var id string
|
|
err := h.DB.QueryRow(`
|
|
INSERT INTO employee_competences (employee_id, competence_id, level, acquired_date, notes)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id
|
|
`, employeeID, req.CompetenceID, req.Level, req.AcquiredDate, req.Notes).Scan(&id)
|
|
|
|
if err != nil {
|
|
http.Error(w, `{"error":"failed to add competence"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Lägg till timeline event
|
|
h.DB.Exec(`
|
|
INSERT INTO employee_timeline_events (employee_id, event_type, event_date, title, description)
|
|
VALUES ($1, 'competence_added', NOW(), 'Kompetens tillagd', $2)
|
|
`, employeeID, req.Notes)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": id,
|
|
"message": "Competence added",
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// DOCUMENTS
|
|
// ==========================================
|
|
|
|
// EmployeeDocument representerar ett dokument
|
|
type EmployeeDocument struct {
|
|
ID string `json:"id"`
|
|
EmployeeID string `json:"employee_id"`
|
|
TemplateID *string `json:"template_id,omitempty"`
|
|
DocumentType string `json:"document_type"`
|
|
Title string `json:"title"`
|
|
SignatureStatus string `json:"signature_status"`
|
|
SignedByEmployeeAt *time.Time `json:"signed_by_employee_at,omitempty"`
|
|
SignedByCompanyAt *time.Time `json:"signed_by_company_at,omitempty"`
|
|
Version int `json:"version"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// GetDocuments returnerar dokument för en medarbetare
|
|
func (h *Handler) GetDocuments(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, template_id, document_type, title, signature_status,
|
|
signed_by_employee_at, signed_by_company_at, version, created_at
|
|
FROM employee_documents
|
|
WHERE employee_id = $1
|
|
ORDER BY created_at DESC
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var documents []EmployeeDocument
|
|
for rows.Next() {
|
|
var d EmployeeDocument
|
|
var templateID sql.NullString
|
|
var signedEmp, signedComp sql.NullTime
|
|
|
|
rows.Scan(&d.ID, &templateID, &d.DocumentType, &d.Title, &d.SignatureStatus,
|
|
&signedEmp, &signedComp, &d.Version, &d.CreatedAt)
|
|
|
|
if templateID.Valid { d.TemplateID = &templateID.String }
|
|
if signedEmp.Valid { d.SignedByEmployeeAt = &signedEmp.Time }
|
|
if signedComp.Valid { d.SignedByCompanyAt = &signedComp.Time }
|
|
|
|
documents = append(documents, d)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"documents": documents,
|
|
"total": len(documents),
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// TRAININGS
|
|
// ==========================================
|
|
|
|
// EmployeeTraining representerar en medarbetares utbildning
|
|
type EmployeeTraining struct {
|
|
ID string `json:"id"`
|
|
TrainingID string `json:"training_id"`
|
|
TrainingTitle string `json:"training_title"`
|
|
Status string `json:"status"`
|
|
ProgressPercent int `json:"progress_percent"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
Score *int `json:"score,omitempty"`
|
|
CertificateURL *string `json:"certificate_url,omitempty"`
|
|
}
|
|
|
|
// GetTrainings returnerar utbildningar för en medarbetare
|
|
func (h *Handler) GetTrainings(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT et.id, et.training_id, t.title, et.status, et.progress_percent,
|
|
et.started_at, et.completed_at, et.expires_at, et.score, et.certificate_url
|
|
FROM employee_trainings et
|
|
JOIN trainings t ON et.training_id = t.id
|
|
WHERE et.employee_id = $1
|
|
ORDER BY et.created_at DESC
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var trainings []EmployeeTraining
|
|
for rows.Next() {
|
|
var et EmployeeTraining
|
|
var started, completed, expires sql.NullTime
|
|
var score sql.NullInt64
|
|
var certURL sql.NullString
|
|
|
|
rows.Scan(&et.ID, &et.TrainingID, &et.TrainingTitle, &et.Status, &et.ProgressPercent,
|
|
&started, &completed, &expires, &score, &certURL)
|
|
|
|
if started.Valid { et.StartedAt = &started.Time }
|
|
if completed.Valid { et.CompletedAt = &completed.Time }
|
|
if expires.Valid { et.ExpiresAt = &expires.Time }
|
|
if score.Valid { s := int(score.Int64); et.Score = &s }
|
|
if certURL.Valid { et.CertificateURL = &certURL.String }
|
|
|
|
trainings = append(trainings, et)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"trainings": trainings,
|
|
"total": len(trainings),
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// TASKS
|
|
// ==========================================
|
|
|
|
// EmployeeTask representerar en uppgift
|
|
type EmployeeTask struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Status string `json:"status"`
|
|
Priority string `json:"priority"`
|
|
DueDate *time.Time `json:"due_date,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Source string `json:"source"`
|
|
AssignedAt time.Time `json:"assigned_at"`
|
|
}
|
|
|
|
// GetTasks returnerar uppgifter för en medarbetare
|
|
func (h *Handler) GetTasks(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, title, description, status, priority, due_date, completed_at, source, assigned_at
|
|
FROM employee_tasks
|
|
WHERE employee_id = $1
|
|
ORDER BY
|
|
CASE status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 ELSE 3 END,
|
|
CASE priority WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END,
|
|
due_date ASC
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var tasks []EmployeeTask
|
|
for rows.Next() {
|
|
var t EmployeeTask
|
|
var dueDate, completed sql.NullTime
|
|
|
|
rows.Scan(&t.ID, &t.Title, &t.Description, &t.Status, &t.Priority, &dueDate, &completed, &t.Source, &t.AssignedAt)
|
|
if dueDate.Valid { t.DueDate = &dueDate.Time }
|
|
if completed.Valid { t.CompletedAt = &completed.Time }
|
|
|
|
tasks = append(tasks, t)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"tasks": tasks,
|
|
"total": len(tasks),
|
|
})
|
|
}
|
|
|
|
// ==========================================
|
|
// PERFORMANCE
|
|
// ==========================================
|
|
|
|
// PerformanceReview representerar en prestationsbedömning
|
|
type PerformanceReview struct {
|
|
ID string `json:"id"`
|
|
ReviewerID string `json:"reviewer_id"`
|
|
ReviewPeriodStart time.Time `json:"review_period_start"`
|
|
ReviewPeriodEnd time.Time `json:"review_period_end"`
|
|
ReviewType string `json:"review_type"`
|
|
OverallRating *int `json:"overall_rating,omitempty"`
|
|
Status string `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// GetPerformance returnerar prestationsbedömningar
|
|
func (h *Handler) GetPerformance(w http.ResponseWriter, r *http.Request) {
|
|
employeeID := chi.URLParam(r, "id")
|
|
|
|
rows, err := h.DB.Query(`
|
|
SELECT id, reviewer_id, review_period_start, review_period_end, review_type, overall_rating, status, created_at
|
|
FROM performance_reviews
|
|
WHERE employee_id = $1
|
|
ORDER BY review_period_end DESC
|
|
`, employeeID)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var reviews []PerformanceReview
|
|
for rows.Next() {
|
|
var pr PerformanceReview
|
|
var rating sql.NullInt64
|
|
|
|
rows.Scan(&pr.ID, &pr.ReviewerID, &pr.ReviewPeriodStart, &pr.ReviewPeriodEnd,
|
|
&pr.ReviewType, &rating, &pr.Status, &pr.CreatedAt)
|
|
if rating.Valid { r := int(rating.Int64); pr.OverallRating = &r }
|
|
|
|
reviews = append(reviews, pr)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"reviews": reviews,
|
|
"total": len(reviews),
|
|
})
|
|
}
|