358 lines
16 KiB
Svelte
358 lines
16 KiB
Svelte
<script lang="ts">
|
|
import {onMount} from 'svelte';
|
|
import {
|
|
serviceService,
|
|
accountService,
|
|
type Service,
|
|
type Account
|
|
} from '$lib/api.js';
|
|
import {profile, isAuthenticated} from '$lib/auth.js';
|
|
import {goto} from '$app/navigation';
|
|
import {page} from '$app/state';
|
|
import {format, parseISO} from 'date-fns';
|
|
|
|
// STATES
|
|
let service: Service | null = $state(null);
|
|
let account: Account | null = $state(null);
|
|
let loading = $state(true);
|
|
let serviceId = $state('');
|
|
let isAdmin = $state(false);
|
|
let isTeamLeader = $state(false);
|
|
|
|
// LOAD SERVICE DATA
|
|
onMount(async () => {
|
|
if (!$isAuthenticated) {
|
|
await goto('/login');
|
|
return;
|
|
}
|
|
|
|
isAdmin = $profile?.role === 'admin';
|
|
isTeamLeader = $profile?.role === 'team_leader';
|
|
serviceId = page.params.id;
|
|
|
|
if (!serviceId) {
|
|
await goto('/services');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading = true;
|
|
|
|
// Fetch service details
|
|
service = await serviceService.getById(serviceId);
|
|
|
|
// Fetch related account
|
|
if (service) {
|
|
const accountId = typeof service.account === 'object'
|
|
? service.account.id
|
|
: service.account;
|
|
|
|
account = await accountService.getById(accountId);
|
|
}
|
|
|
|
loading = false;
|
|
} catch (error) {
|
|
console.error('Error loading service:', error);
|
|
loading = false;
|
|
}
|
|
});
|
|
|
|
// UPDATE SERVICE STATUS
|
|
async function updateStatus(newStatus: string) {
|
|
if (!service) return;
|
|
|
|
if (confirm(`Are you sure you want to mark this service as ${newStatus}?`)) {
|
|
try {
|
|
await serviceService.patch(service.id, {
|
|
status: newStatus as 'scheduled' | 'in_progress' | 'completed' | 'cancelled'
|
|
});
|
|
|
|
// Refresh service data
|
|
service = await serviceService.getById(service.id);
|
|
} catch (error) {
|
|
console.error(`Error updating service to ${newStatus}:`, error);
|
|
alert('Failed to update service. Please try again.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// FORMAT DATE USING DATE-FNS
|
|
function formatDate(dateStr: string | undefined): string {
|
|
if (!dateStr) return 'N/A';
|
|
try {
|
|
const date = parseISO(dateStr);
|
|
return format(date, 'MMMM d, yyyy');
|
|
} catch (error) {
|
|
console.error('Error formatting date:', error);
|
|
return 'Invalid Date';
|
|
}
|
|
}
|
|
|
|
// FORMAT TIME USING DATE-FNS
|
|
function formatTime(dateTimeStr: string | undefined): string {
|
|
if (!dateTimeStr) return 'N/A';
|
|
try {
|
|
const date = parseISO(dateTimeStr);
|
|
return format(date, 'h:mm aa');
|
|
} catch (error) {
|
|
console.error('Error formatting time:', error);
|
|
return 'Invalid Time';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="container-fluid p-4">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<a href="/services" class="btn btn-outline-secondary mb-2">
|
|
<i class="bi bi-arrow-left"></i> Back to Services
|
|
</a>
|
|
<h1 class="mb-0">Service Details</h1>
|
|
{#if service && account}
|
|
<p class="text-muted">
|
|
Account: <a href={`/accounts/${account.id}`}>{account.name}</a>
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if (isAdmin || isTeamLeader) && service && service.status !== 'completed' && service.status !== 'cancelled'}
|
|
<div>
|
|
<a href={`/services/${serviceId}/edit`} class="btn btn-primary">
|
|
Edit Service
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if loading}
|
|
<div class="text-center p-5">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
{:else if !service}
|
|
<div class="alert alert-danger" role="alert">
|
|
Service not found
|
|
</div>
|
|
{:else}
|
|
<!-- Service Details -->
|
|
<div class="row">
|
|
<!-- Main Info -->
|
|
<div class="col-lg-8">
|
|
<div class="card mb-4">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Service Information</h5>
|
|
<span class={`badge ${
|
|
service.status === 'completed' ? 'bg-success' :
|
|
service.status === 'cancelled' ? 'bg-danger' :
|
|
service.status === 'in_progress' ? 'bg-primary' : 'bg-secondary'
|
|
}`}>
|
|
{service.status.replace('_', ' ')}
|
|
</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<p class="mb-1 text-muted">Service Date</p>
|
|
<p class="mb-3">{formatDate(service.date)}</p>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<p class="mb-1 text-muted">Time Window</p>
|
|
<p class="mb-3">{formatTime(service.deadline_start)}
|
|
- {formatTime(service.deadline_end)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<p class="mb-1 text-muted">Account Address</p>
|
|
{#if account}
|
|
<p class="mb-3">
|
|
{account.street_address}<br>
|
|
{account.city}, {account.state} {account.zip_code}
|
|
</p>
|
|
{:else}
|
|
<p class="text-muted">Account details not available</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<p class="mb-1 text-muted">Team Members</p>
|
|
{#if service.team_members && service.team_members.length > 0}
|
|
<ul class="list-group">
|
|
{#each service.team_members as member (typeof member === 'object' ? member.id : member)}
|
|
<li class="list-group-item">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>{typeof member === 'object' ? `${member.first_name} ${member.last_name}` : 'Unknown Member'}</strong>
|
|
<div class="text-muted small">{typeof member === 'object' ? member.role.replace('_', ' ') : ''}</div>
|
|
</div>
|
|
{#if typeof member === 'object'}
|
|
<div>
|
|
<a href={`tel:${member.primary_phone}`}
|
|
class="btn btn-sm btn-outline-secondary me-1">
|
|
<i class="bi bi-telephone"></i>
|
|
Call
|
|
</a>
|
|
<a href={`mailto:${member.email}`}
|
|
class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-envelope"></i>
|
|
Email
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{:else}
|
|
<p class="text-muted">No team members assigned</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
{#if service.notes}
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<p class="mb-1 text-muted">Notes</p>
|
|
<div class="p-3 bg-light rounded">
|
|
<p class="mb-0">{service.notes}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
{#if isAdmin || isTeamLeader}
|
|
<!-- Status Management -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Status Management</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{#if service.status === 'scheduled'}
|
|
<p>This service is currently scheduled.</p>
|
|
<div class="d-grid gap-2">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={() => updateStatus('in_progress')}
|
|
>
|
|
Mark as In Progress
|
|
</button>
|
|
<button
|
|
class="btn btn-success"
|
|
onclick={() => updateStatus('completed')}
|
|
>
|
|
Mark as Completed
|
|
</button>
|
|
<button
|
|
class="btn btn-danger"
|
|
onclick={() => updateStatus('cancelled')}
|
|
>
|
|
Cancel Service
|
|
</button>
|
|
</div>
|
|
{:else if service.status === 'in_progress'}
|
|
<p>This service is currently in progress.</p>
|
|
<div class="d-grid gap-2">
|
|
<button
|
|
class="btn btn-success"
|
|
onclick={() => updateStatus('completed')}
|
|
>
|
|
Mark as Completed
|
|
</button>
|
|
<button
|
|
class="btn btn-danger"
|
|
onclick={() => updateStatus('cancelled')}
|
|
>
|
|
Cancel Service
|
|
</button>
|
|
</div>
|
|
{:else if service.status === 'completed'}
|
|
<p class="text-success">
|
|
<i class="bi bi-check-circle"></i>
|
|
This service has been completed.
|
|
</p>
|
|
<div class="d-grid">
|
|
<button
|
|
class="btn btn-outline-secondary"
|
|
onclick={() => updateStatus('in_progress')}
|
|
>
|
|
Reopen Service
|
|
</button>
|
|
</div>
|
|
{:else if service.status === 'cancelled'}
|
|
<p class="text-danger">
|
|
<i class="bi bi-x-circle"></i>
|
|
This service has been cancelled.
|
|
</p>
|
|
<div class="d-grid">
|
|
<button
|
|
class="btn btn-outline-secondary"
|
|
onclick={() => updateStatus('scheduled')}
|
|
>
|
|
Reschedule Service
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Contact Information -->
|
|
{#if account}
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Account Contact</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="mb-1"><strong>{account.contact_first_name} {account.contact_last_name}</strong>
|
|
</p>
|
|
<p class="mb-3">
|
|
<a href={`tel:${account.contact_phone}`}>{account.contact_phone}</a><br>
|
|
<a href={`mailto:${account.contact_email}`}>{account.contact_email}</a>
|
|
</p>
|
|
<div class="d-grid gap-2">
|
|
<a href={`tel:${account.contact_phone}`} class="btn btn-outline-primary">
|
|
<i class="bi bi-telephone"></i> Call Contact
|
|
</a>
|
|
<a href={`mailto:${account.contact_email}`} class="btn btn-outline-primary">
|
|
<i class="bi bi-envelope"></i> Email Contact
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Quick Actions</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
{#if account}
|
|
<a href={`/services/new?account_id=${account.id}`} class="btn btn-outline-primary">
|
|
<i class="bi bi-calendar-plus"></i> Schedule Another Service
|
|
</a>
|
|
<a href={`/accounts/${account.id}`} class="btn btn-outline-secondary">
|
|
<i class="bi bi-building"></i> View Account
|
|
</a>
|
|
{/if}
|
|
<a href="/services" class="btn btn-outline-secondary">
|
|
<i class="bi bi-list-check"></i> View All Services
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div> |