- Video Interview system (backend + frontend) - Date Posted filter (24h, 7d, 30d) - Company filter in jobs listing - Recent searches persistence (LocalStorage) - Job Alerts with email confirmation - Favorite jobs with API - Company followers system - Careerjet URL compatibility (s/l aliases)
302 lines
8.9 KiB
Go
302 lines
8.9 KiB
Go
package services
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
|
)
|
|
|
|
type VideoInterviewService struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func NewVideoInterviewService(db *sql.DB) *VideoInterviewService {
|
|
return &VideoInterviewService{DB: db}
|
|
}
|
|
|
|
func (s *VideoInterviewService) CreateInterview(req dto.CreateVideoInterviewRequest, createdBy string) (*models.VideoInterview, error) {
|
|
scheduledAt, err := time.Parse(time.RFC3339, req.ScheduledAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid scheduled_at format: %w", err)
|
|
}
|
|
|
|
if req.Timezone == "" {
|
|
req.Timezone = "UTC"
|
|
}
|
|
|
|
if req.MeetingProvider == nil {
|
|
provider := "custom"
|
|
req.MeetingProvider = &provider
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO video_interviews (
|
|
application_id, scheduled_at, duration_minutes, timezone,
|
|
meeting_provider, notes, status, created_by, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
interview := &models.VideoInterview{
|
|
ApplicationID: req.ApplicationID,
|
|
ScheduledAt: scheduledAt,
|
|
DurationMinutes: req.DurationMinutes,
|
|
Timezone: req.Timezone,
|
|
MeetingProvider: req.MeetingProvider,
|
|
Notes: req.Notes,
|
|
Status: "scheduled",
|
|
CreatedBy: &createdBy,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
err = s.DB.QueryRow(
|
|
query,
|
|
interview.ApplicationID, interview.ScheduledAt, interview.DurationMinutes, interview.Timezone,
|
|
interview.MeetingProvider, interview.Notes, interview.Status, interview.CreatedBy,
|
|
interview.CreatedAt, interview.UpdatedAt,
|
|
).Scan(&interview.ID, &interview.CreatedAt, &interview.UpdatedAt)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create interview: %w", err)
|
|
}
|
|
|
|
return interview, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) GetInterviewByID(id string) (*models.VideoInterview, error) {
|
|
query := `
|
|
SELECT id, application_id, scheduled_at, duration_minutes, timezone,
|
|
meeting_link, meeting_provider, meeting_id, meeting_password,
|
|
status, started_at, ended_at, notes, interviewer_feedback,
|
|
candidate_feedback, rating, created_by, created_at, updated_at
|
|
FROM video_interviews WHERE id = $1
|
|
`
|
|
|
|
interview := &models.VideoInterview{}
|
|
err := s.DB.QueryRow(query, id).Scan(
|
|
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
|
|
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
|
|
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
|
|
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return interview, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) GetInterviewsByApplication(applicationID string) ([]models.VideoInterview, error) {
|
|
query := `
|
|
SELECT id, application_id, scheduled_at, duration_minutes, timezone,
|
|
meeting_link, meeting_provider, meeting_id, meeting_password,
|
|
status, started_at, ended_at, notes, interviewer_feedback,
|
|
candidate_feedback, rating, created_by, created_at, updated_at
|
|
FROM video_interviews
|
|
WHERE application_id = $1
|
|
ORDER BY scheduled_at DESC
|
|
`
|
|
|
|
rows, err := s.DB.Query(query, applicationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var interviews []models.VideoInterview
|
|
for rows.Next() {
|
|
var interview models.VideoInterview
|
|
err := rows.Scan(
|
|
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
|
|
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
|
|
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
|
|
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interviews = append(interviews, interview)
|
|
}
|
|
|
|
return interviews, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) GetInterviewsByCompany(companyID string) ([]models.VideoInterviewWithDetails, error) {
|
|
query := `
|
|
SELECT
|
|
vi.id, vi.application_id, vi.scheduled_at, vi.duration_minutes, vi.timezone,
|
|
vi.meeting_link, vi.meeting_provider, vi.meeting_id, vi.meeting_password,
|
|
vi.status, vi.started_at, vi.ended_at, vi.notes, vi.interviewer_feedback,
|
|
vi.candidate_feedback, vi.rating, vi.created_by, vi.created_at, vi.updated_at,
|
|
j.title as job_title, c.id as company_id, c.name as company_name,
|
|
COALESCE(u.name, a.name) as candidate_name,
|
|
COALESCE(u.email, a.email) as candidate_email
|
|
FROM video_interviews vi
|
|
JOIN applications a ON vi.application_id = a.id
|
|
JOIN jobs j ON a.job_id = j.id
|
|
JOIN companies c ON j.company_id = c.id
|
|
LEFT JOIN users u ON a.user_id = u.id
|
|
WHERE c.id = $1
|
|
ORDER BY vi.scheduled_at DESC
|
|
`
|
|
|
|
rows, err := s.DB.Query(query, companyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var interviews []models.VideoInterviewWithDetails
|
|
for rows.Next() {
|
|
var interview models.VideoInterviewWithDetails
|
|
err := rows.Scan(
|
|
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
|
|
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
|
|
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
|
|
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
|
|
&interview.JobTitle, &interview.CompanyID, &interview.CompanyName,
|
|
&interview.CandidateName, &interview.CandidateEmail,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
interviews = append(interviews, interview)
|
|
}
|
|
|
|
return interviews, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) UpdateInterview(id string, req dto.UpdateVideoInterviewRequest) (*models.VideoInterview, error) {
|
|
interview, err := s.GetInterviewByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.ScheduledAt != nil {
|
|
scheduledAt, err := time.Parse(time.RFC3339, *req.ScheduledAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid scheduled_at format: %w", err)
|
|
}
|
|
interview.ScheduledAt = scheduledAt
|
|
}
|
|
|
|
if req.DurationMinutes != nil {
|
|
interview.DurationMinutes = *req.DurationMinutes
|
|
}
|
|
|
|
if req.Timezone != nil {
|
|
interview.Timezone = *req.Timezone
|
|
}
|
|
|
|
if req.MeetingLink != nil {
|
|
interview.MeetingLink = req.MeetingLink
|
|
}
|
|
|
|
if req.MeetingProvider != nil {
|
|
interview.MeetingProvider = req.MeetingProvider
|
|
}
|
|
|
|
if req.MeetingID != nil {
|
|
interview.MeetingID = req.MeetingID
|
|
}
|
|
|
|
if req.MeetingPassword != nil {
|
|
interview.MeetingPassword = req.MeetingPassword
|
|
}
|
|
|
|
if req.Status != nil {
|
|
interview.Status = *req.Status
|
|
|
|
if *req.Status == "in_progress" {
|
|
now := time.Now()
|
|
interview.StartedAt = &now
|
|
} else if *req.Status == "completed" {
|
|
now := time.Now()
|
|
interview.EndedAt = &now
|
|
}
|
|
}
|
|
|
|
if req.Notes != nil {
|
|
interview.Notes = req.Notes
|
|
}
|
|
|
|
interview.UpdatedAt = time.Now()
|
|
|
|
query := `
|
|
UPDATE video_interviews SET
|
|
scheduled_at = $1, duration_minutes = $2, timezone = $3,
|
|
meeting_link = $4, meeting_provider = $5, meeting_id = $6, meeting_password = $7,
|
|
status = $8, started_at = $9, ended_at = $10, notes = $11, updated_at = $12
|
|
WHERE id = $13
|
|
`
|
|
|
|
_, err = s.DB.Exec(query,
|
|
interview.ScheduledAt, interview.DurationMinutes, interview.Timezone,
|
|
interview.MeetingLink, interview.MeetingProvider, interview.MeetingID, interview.MeetingPassword,
|
|
interview.Status, interview.StartedAt, interview.EndedAt, interview.Notes, interview.UpdatedAt,
|
|
id,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update interview: %w", err)
|
|
}
|
|
|
|
return interview, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) SubmitFeedback(id string, req dto.VideoInterviewFeedbackRequest) (*models.VideoInterview, error) {
|
|
interview, err := s.GetInterviewByID(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.InterviewerFeedback != nil {
|
|
interview.InterviewerFeedback = req.InterviewerFeedback
|
|
}
|
|
|
|
if req.CandidateFeedback != nil {
|
|
interview.CandidateFeedback = req.CandidateFeedback
|
|
}
|
|
|
|
if req.Rating != nil {
|
|
interview.Rating = req.Rating
|
|
}
|
|
|
|
interview.UpdatedAt = time.Now()
|
|
|
|
query := `
|
|
UPDATE video_interviews SET
|
|
interviewer_feedback = $1, candidate_feedback = $2, rating = $3, updated_at = $4
|
|
WHERE id = $5
|
|
`
|
|
|
|
_, err = s.DB.Exec(query,
|
|
interview.InterviewerFeedback, interview.CandidateFeedback, interview.Rating,
|
|
interview.UpdatedAt, id,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to submit feedback: %w", err)
|
|
}
|
|
|
|
return interview, nil
|
|
}
|
|
|
|
func (s *VideoInterviewService) DeleteInterview(id string) error {
|
|
result, err := s.DB.Exec("DELETE FROM video_interviews WHERE id = $1", id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
if rowsAffected == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
|
|
return nil
|
|
}
|