2026-01-26 11:30:40 -05:00

329 lines
9.8 KiB
Svelte

<script lang="ts">
import AssignColumnHeader from './AssignColumnHeader.svelte';
import AssignServiceGroup from './AssignServiceGroup.svelte';
import IconSpinner from '$lib/components/icons/IconSpinner.svelte';
import type { SvelteSet } from 'svelte/reactivity';
type EnrichedService = {
id: string;
date: string;
status: string;
notes: string | null;
teamMembers: { pk: string }[];
accountAddressId: string;
accountName: string;
addressName: string | null;
address: string | null;
};
type ColumnType = 'unassigned' | 'readyToAssign' | 'assigned';
interface Props {
column: ColumnType;
services: EnrichedService[];
groupedServices: Map<string, EnrichedService[]>;
expandedGroups: SvelteSet<string>;
updatingServices: Set<string>;
openTeamMemberDropdown: string | null;
editingDateServiceId?: string | null;
// Selection props
selectedServices: SvelteSet<string>;
isBulkOperating?: boolean;
// For Ready to Assign bulk operations
nonAdminTeamMembers?: { id: string; fullName: string }[];
bulkSelectedTeamMember?: string | null;
showBulkTeamMemberDropdown?: boolean;
// Callbacks
onToggleGroup: (groupKey: string) => void;
getTeamMemberNames: (teamMembers: { pk: string }[]) => string[];
getNonDispatchTeamMemberNames: (teamMembers: { pk: string }[]) => string[];
getAvailableTeamMembers: (service: EnrichedService) => { pk: string; name: string }[];
getStagedTeamMemberDetails: (serviceId: string) => { pk: string; name: string }[];
hasStagedMembers: (serviceId: string) => boolean;
onAddDispatch: (service: EnrichedService) => void;
onRemoveDispatch: (service: EnrichedService) => void;
onSubmitStaged: (service: EnrichedService) => void;
onRemoveNonDispatch: (service: EnrichedService) => void;
onStageTeamMember: (serviceId: string, memberPk: string) => void;
onUnstageTeamMember: (serviceId: string, memberPk: string) => void;
onToggleDropdown: (serviceId: string | null) => void;
// Selection callbacks
onToggleSelection: (serviceId: string) => void;
onSelectAll: () => void;
onClearSelection: () => void;
onBulkAction: () => void;
// For Ready to Assign
onBulkTeamMemberSelect?: (memberPk: string) => void;
onToggleBulkTeamMemberDropdown?: () => void;
// Unassigned column specific
onUpdateDate?: (service: EnrichedService, newDate: string) => void;
onDeleteService?: (service: EnrichedService) => void;
onStartEditDate?: (serviceId: string) => void;
onCancelEditDate?: () => void;
}
let {
column,
services,
groupedServices,
expandedGroups,
updatingServices,
openTeamMemberDropdown,
editingDateServiceId = null,
selectedServices,
isBulkOperating = false,
nonAdminTeamMembers = [],
bulkSelectedTeamMember = null,
showBulkTeamMemberDropdown = false,
onToggleGroup,
getTeamMemberNames,
getNonDispatchTeamMemberNames,
getAvailableTeamMembers,
getStagedTeamMemberDetails,
hasStagedMembers,
onAddDispatch,
onRemoveDispatch,
onSubmitStaged,
onRemoveNonDispatch,
onStageTeamMember,
onUnstageTeamMember,
onToggleDropdown,
onToggleSelection,
onSelectAll,
onClearSelection,
onBulkAction,
onBulkTeamMemberSelect,
onToggleBulkTeamMemberDropdown,
onUpdateDate,
onDeleteService,
onStartEditDate,
onCancelEditDate
}: Props = $props();
let hasSelection = $derived(selectedServices.size > 0);
let allSelected = $derived(services.length > 0 && selectedServices.size === services.length);
function getEmptyIcon(): string {
switch (column) {
case 'unassigned':
case 'assigned':
return 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z';
case 'readyToAssign':
return 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z';
}
}
function getEmptyMessage(): string {
switch (column) {
case 'unassigned':
return 'No unassigned services';
case 'readyToAssign':
return 'No services ready to assign';
case 'assigned':
return 'No assigned services';
}
}
function getBulkActionLabel(): string {
switch (column) {
case 'unassigned':
return 'Add Dispatch';
case 'readyToAssign':
return 'Assign';
case 'assigned':
return 'Remove Team';
}
}
function getBulkActionDisabled(): boolean {
if (isBulkOperating) return true;
if (selectedServices.size === 0) return true;
return column === 'readyToAssign' && !bulkSelectedTeamMember;
}
function getSelectedTeamMemberName(): string {
if (!bulkSelectedTeamMember) return 'Select team member...';
const member = nonAdminTeamMembers.find((m) => {
// Extract pk from global id
const parts = atob(m.id).split(':');
return parts[1] === bulkSelectedTeamMember;
});
return member?.fullName ?? 'Unknown';
}
</script>
<div class="flex w-1/3 flex-col overflow-hidden rounded-xl bg-theme-card shadow-md">
<AssignColumnHeader {column} count={services.length} />
<!-- Bulk Action Bar -->
{#if services.length > 0}
<div class="border-b border-theme bg-theme-card px-3 py-2">
<div class="flex items-center justify-between gap-2">
<!-- Select All / Clear -->
<div class="flex items-center gap-2">
<button
onclick={() => (allSelected ? onClearSelection() : onSelectAll())}
class="flex items-center gap-1.5 rounded interactive px-2 py-1 text-xs font-medium {hasSelection
? 'text-primary-600 dark:text-primary-400'
: 'text-theme-muted'}"
>
<svg
class="h-4 w-4"
style="fill: none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
{#if allSelected}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
{:else if hasSelection}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
{:else}
<circle cx="12" cy="12" r="9" />
{/if}
</svg>
{#if hasSelection}
<span>{selectedServices.size} selected</span>
{:else}
<span>Select all</span>
{/if}
</button>
{#if hasSelection}
<button onclick={onClearSelection} class="text-xs text-theme-muted hover:text-theme">
Clear
</button>
{/if}
</div>
<!-- Bulk Action -->
{#if hasSelection}
<div class="flex items-center gap-2">
{#if column === 'readyToAssign'}
<!-- Team member dropdown for bulk assignment -->
<div class="relative">
<button
onclick={(e) => {
e.stopPropagation();
onToggleBulkTeamMemberDropdown?.();
}}
class="flex items-center gap-1 rounded border border-theme interactive bg-theme px-2 py-1 text-xs font-medium text-theme"
>
<span class="max-w-[100px] truncate">{getSelectedTeamMemberName()}</span>
<svg class="h-3 w-3" style="fill: none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{#if showBulkTeamMemberDropdown}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="menu"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
class="absolute top-full right-0 z-30 mt-1 max-h-48 w-48 overflow-y-auto rounded-lg border border-theme bg-theme-card py-1 shadow-lg"
>
{#each nonAdminTeamMembers as member (member.id)}
{@const memberPk = atob(member.id).split(':')[1]}
<button
onclick={() => onBulkTeamMemberSelect?.(memberPk)}
class="block w-full px-3 py-2 text-left text-sm text-theme hover:bg-black/5 dark:hover:bg-white/10 {bulkSelectedTeamMember ===
memberPk
? 'bg-primary-50 dark:bg-primary-900/20'
: ''}"
>
{member.fullName}
</button>
{/each}
</div>
{/if}
</div>
{/if}
<button
onclick={onBulkAction}
disabled={getBulkActionDisabled()}
class="flex items-center gap-1 rounded bg-primary-600 px-2 py-1 text-xs font-medium text-white transition-colors hover:bg-primary-700 disabled:cursor-not-allowed disabled:opacity-50"
>
{#if isBulkOperating}
<IconSpinner class="h-3 w-3" />
{:else}
<svg class="h-3 w-3" style="fill: none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
{/if}
{getBulkActionLabel()}
</button>
</div>
{/if}
</div>
</div>
{/if}
<div class="flex-1 space-y-2 overflow-y-auto bg-theme p-4">
{#each [...groupedServices.entries()] as [groupKey, groupServices] (groupKey)}
<AssignServiceGroup
{groupKey}
services={groupServices}
{column}
isExpanded={expandedGroups.has(groupKey)}
{updatingServices}
{openTeamMemberDropdown}
{editingDateServiceId}
{selectedServices}
onToggle={() => onToggleGroup(groupKey)}
{getTeamMemberNames}
{getNonDispatchTeamMemberNames}
{getAvailableTeamMembers}
{getStagedTeamMemberDetails}
{hasStagedMembers}
{onAddDispatch}
{onRemoveDispatch}
{onSubmitStaged}
{onRemoveNonDispatch}
{onStageTeamMember}
{onUnstageTeamMember}
{onToggleDropdown}
{onToggleSelection}
{onUpdateDate}
{onDeleteService}
{onStartEditDate}
{onCancelEditDate}
/>
{/each}
{#if services.length === 0}
<div class="py-12 text-center text-theme-muted">
<svg
class="mx-auto mb-3 h-12 w-12"
style="fill: none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d={getEmptyIcon()}
/>
</svg>
{getEmptyMessage()}
</div>
{/if}
</div>
</div>