big refactor

This commit is contained in:
2026-04-29 16:25:11 -05:00
parent 6c411ce2b6
commit 8265fb689a
156 changed files with 15845 additions and 50373 deletions

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import KanbanTaskCard from './KanbanTaskCard.vue'
const props = defineProps<{
title: string
status: string
tasks: any[]
loading: boolean
}>()
const router = useRouter()
function navigateToTask(task: any) {
router.push(`/back-office/workload/${encodeURIComponent(task.id)}`)
}
// Stage configuration for colors
const stageConfig: Record<string, {
color: string;
dot: string;
headerBg: string;
}> = {
created: {
color: 'text-[var(--text-muted)]',
dot: 'bg-[var(--text-muted)]',
headerBg: 'bg-[var(--surface)] border-[var(--card-border)]'
},
draft: {
color: 'text-[var(--brand)]',
dot: 'bg-[var(--brand)]',
headerBg: 'bg-[var(--brand-faint)] border-[var(--brand-soft)]'
},
approved: {
color: 'text-emerald-700',
dot: 'bg-emerald-500',
headerBg: 'bg-emerald-50 border-emerald-200'
},
completed: {
color: 'text-gray-500',
dot: 'bg-gray-400',
headerBg: 'bg-gray-50 border-gray-200'
}
}
function daysInStage(createdAt: string): string {
const days = Math.floor((Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24))
return days === 0 ? 'Today' : `${days}d`
}
function taskUrgent(task: any): boolean {
// Placeholder - will be based on priority/due_date from backend
return false
}
</script>
<template>
<div class="kanban-column">
<div class="column-header" :class="stageConfig[status].headerBg">
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full" :class="stageConfig[status].dot" />
<span class="text-[13px] font-semibold" :class="stageConfig[status].color">
{{ title }}
</span>
</div>
<div class="flex items-center gap-1.5">
<span class="flex h-5 min-w-[20px] items-center justify-center rounded-full bg-[var(--surface)]/70 px-1.5 text-[11px] font-semibold text-[var(--text-muted)] ring-1 ring-[var(--card-border)]/60">
{{ tasks.length }}
</span>
</div>
</div>
<div class="task-list">
<KanbanTaskCard
v-for="task in tasks"
:key="task.id"
:task="task"
:urgent="taskUrgent(task)"
@click="navigateToTask(task)"
/>
<div v-if="tasks.length === 0 && !loading" class="empty-state">
<UIcon name="i-heroicons-inbox" class="w-8 h-8" />
</div>
<div v-if="loading" class="loading-state">
<div v-for="n in 3" :key="n" class="skeleton-card" />
</div>
</div>
</div>
</template>
<style scoped>
.kanban-column {
flex: 0 0 240px;
display: flex;
flex-direction: column;
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--card-border);
overflow: hidden;
height: 100%;
}
.column-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--card-border);
background: var(--surface);
border-radius: 8px 8px 0 0;
flex-shrink: 0;
}
.task-list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px;
color: var(--text-muted);
min-height: 100%;
}
.loading-state {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
}
.skeleton-card {
height: 100px;
background: var(--card-border);
border-radius: 6px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
</style>

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
const props = defineProps<{
task: any
urgent?: boolean
}>()
const emit = defineEmits<{
click: [task: any]
}>()
const policyTypeColors: Record<string, string> = {
car: 'bg-blue-100 text-blue-700',
life: 'bg-violet-100 text-violet-700',
fire: 'bg-amber-100 text-amber-700'
}
function onClick() {
emit('click', props.task)
}
function daysInStage(createdAt: string): string {
const days = Math.floor((Date.now() - new Date(createdAt).getTime()) / (1000 * 60 * 60 * 24))
return days === 0 ? 'Today' : `${days}d`
}
function formatDate(date: string, format: 'short' = 'short'): string {
if (!date) return '—'
const d = new Date(date)
if (format === 'short') {
return d.toLocaleDateString('es-PA', { month: 'short', day: 'numeric' })
}
return d.toLocaleDateString('es-PA', {
day: '2-digit',
month: 'short',
year: 'numeric'
})
}
</script>
<template>
<div
class="kanban-task-card rounded-xl border px-3.5 py-3 shadow-sm ring-1 transition hover:shadow-md"
:class="urgent
? 'border-rose-300 bg-rose-50/50 ring-rose-100 hover:border-rose-400'
: 'border-[var(--card-border)] bg-[var(--surface)] ring-[var(--surface)] hover:border-[var(--brand)]/30'"
@click="onClick"
>
<!-- Header row: Application ID + Policy type badge -->
<div class="flex items-start justify-between gap-1 mb-1">
<p class="text-[13px] font-mono font-semibold leading-tight text-[var(--text-primary)] truncate">
{{ task.application_id }}
</p>
<span
class="ml-1 shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-semibold"
:class="policyTypeColors[task.policy_type] || 'bg-gray-100 text-gray-700'"
>
{{ task.policy_type?.toUpperCase() }}
</span>
</div>
<!-- Provider name -->
<p class="text-[12px] text-[var(--text-muted)] truncate">
{{ task.provider_name || 'Unknown Provider' }}
</p>
<!-- Urgent flag -->
<span v-if="urgent" class="mt-1.5 inline-flex items-center gap-1 rounded-full bg-rose-100 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-rose-600">
<span class="h-1.5 w-1.5 rounded-full bg-rose-500" />
Urgent
</span>
<!-- Meta row -->
<div class="mt-2.5 flex items-center justify-between border-t border-[var(--divider)] pt-2">
<div class="flex items-center gap-1">
<!-- Assignee -->
<div v-if="task.assignee" class="flex items-center gap-1 text-[11px] text-[var(--text-muted)]">
<UIcon name="i-heroicons-user" class="w-3 h-3" />
<span class="truncate max-w-[60px]">{{ task.assignee.name }}</span>
</div>
<div v-else class="flex items-center gap-1 text-[11px] text-[var(--text-muted)] opacity-50">
<UIcon name="i-heroicons-user-minus" class="w-3 h-3" />
<span>Unassigned</span>
</div>
</div>
<div class="flex items-center gap-1.5 text-[11px] text-[var(--text-muted)]">
<!-- Due date if present -->
<span v-if="task.due_date" class="truncate">
Due: {{ formatDate(task.due_date, 'short') }}
</span>
<!-- Days in stage -->
<span class="flex items-center gap-0.5">
<UIcon name="i-heroicons-clock" class="w-3 h-3" />
{{ daysInStage(task.created_at) }}
</span>
</div>
</div>
</div>
</template>
<style scoped>
.kanban-task-card {
cursor: pointer;
transition: all 0.15s ease;
}
.kanban-task-card:hover {
transform: translateY(-1px);
}
</style>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
defineOptions({ name: 'NestedJsonViewer' })
interface Props {
data: any
depth?: number
}
const props = defineProps<Props>()
const depth = computed(() => props.depth || 0)
// Font size decreases with depth: 12px base, -1px per level
const fontSize = computed(() => Math.max(10, 12 - depth.value * 1))
// Padding decreases with depth
const padding = computed(() => Math.max(1, 4 - depth.value))
function isObject(value: any): boolean {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function isArray(value: any): boolean {
return Array.isArray(value)
}
function isDateString(value: string): boolean {
if (typeof value !== 'string') return false
return /^\d{4}-\d{2}-\d{2}/.test(value) && !isNaN(Date.parse(value))
}
function formatValue(value: any): string {
if (value === null || value === undefined) return '—'
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
if (typeof value === 'number') return value.toLocaleString()
if (isDateString(value)) {
try {
return new Date(value).toLocaleDateString('es-PA', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
} catch {
return value
}
}
return String(value)
}
function formatKey(key: string): string {
return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
}
</script>
<template>
<div class="nested-json-viewer" :style="{ fontSize: fontSize + 'px' }">
<!-- Object: render as table -->
<table v-if="isObject(data) && Object.keys(data).length > 0" class="json-table border rounded overflow-hidden mb-2">
<tbody>
<tr v-for="(value, key) in data" :key="key" class="border-t border-gray-200 hover:bg-gray-50">
<td class="field-name px-3 py-2 font-medium text-gray-600 align-top whitespace-nowrap" :style="{ padding: padding + 'px', fontSize: (fontSize - 1) + 'px' }">
{{ formatKey(key) }}
</td>
<td class="field-value px-3 py-2 text-gray-900 align-top" :style="{ padding: padding + 'px' }">
<NestedJsonViewer :data="value" :depth="depth + 1" />
</td>
</tr>
</tbody>
</table>
<div v-else-if="isObject(data) && Object.keys(data).length === 0" class="text-gray-400 italic" :style="{ fontSize: fontSize + 'px' }">
Empty object
</div>
<!-- Array -->
<div v-else-if="isArray(data)" class="array-container">
<div v-for="(item, index) in data" :key="index" class="array-item mb-2 border rounded p-2">
<div class="text-xs font-semibold text-gray-500 mb-1">[{{ index }}]</div>
<NestedJsonViewer :data="item" :depth="depth + 1" />
</div>
<div v-if="data.length === 0" class="text-gray-400 italic" :style="{ fontSize: fontSize + 'px' }">
Empty array
</div>
</div>
<!-- Primitive -->
<span v-else class="primitive-value" :class="{ 'text-gray-400': data === '—' }">
{{ formatValue(data) }}
</span>
</div>
</template>
<style scoped>
.nested-json-viewer {
width: 100%;
}
.json-table {
width: 100%;
border-collapse: collapse;
background: white;
}
.field-name {
min-width: 120px;
background: #f9fafb;
}
.field-value {
word-break: break-word;
}
.array-item {
background: white;
border-color: var(--card-border, #e5e7eb);
}
</style>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
interface Props {
task: any // TODO: Type properly
}
const props = defineProps<Props>()
const formatDate = (d: string) => d
? new Date(d).toLocaleDateString('es-PA', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })
: '—'
</script>
<template>
<div class="space-y-4 text-sm">
<!-- Basic Info -->
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<div class="text-gray-500">Task ID</div>
<div class="font-mono text-xs text-right">{{ task.id }}</div>
<div class="text-gray-500">Application ID</div>
<div class="font-mono text-xs text-right">{{ task.application_id }}</div>
<div class="text-gray-500">Provider ID</div>
<div class="font-mono text-xs text-right">{{ task.task_info?.provider_id }}</div>
<div v-if="task.task_info?.provider_name" class="text-gray-500">Provider</div>
<div v-if="task.task_info?.provider_name" class="text-right">{{ task.task_info.provider_name }}</div>
<div class="text-gray-500">Org</div>
<div class="font-mono text-xs text-right">{{ task.org_id }}</div>
<div class="text-gray-500">Created</div>
<div class="text-right">{{ formatDate(task.created_at) }}</div>
<div class="text-gray-500">Updated</div>
<div class="text-right">{{ formatDate(task.updated_at) }}</div>
<div class="text-gray-500">Policy Type</div>
<div class="text-right uppercase text-xs">{{ task.task_info?.policy_type || '—' }}</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
interface Props {
task: any // TODO: Type properly
}
const props = defineProps<Props>()
</script>
<template>
<div class="space-y-6">
<!-- Applicant Info -->
<div v-if="task.task_info?.applicant_info">
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2">Applicant Information</h3>
<div class="border rounded-lg overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Field</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(value, key) in task.task_info.applicant_info" :key="key">
<td class="px-3 py-2 text-gray-600 font-medium capitalize">{{ key.replace(/_/g, ' ') }}</td>
<td class="px-3 py-2 text-gray-900">{{ value || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Vehicle Info -->
<div v-if="task.task_info?.vehicle_info">
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2">Vehicle Information</h3>
<div class="border rounded-lg overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Field</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(value, key) in task.task_info.vehicle_info" :key="key">
<td class="px-3 py-2 text-gray-600 font-medium capitalize">{{ key.replace(/_/g, ' ') }}</td>
<td class="px-3 py-2 text-gray-900">{{ value || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Policy Details -->
<div v-if="task.task_info?.policy_details">
<h3 class="text-xs font-semibold text-gray-500 uppercase mb-2">Policy Details</h3>
<div class="border rounded-lg overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Field</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="(value, key) in task.task_info.policy_details" :key="key">
<td class="px-3 py-2 text-gray-600 font-medium capitalize">{{ key.replace(/_/g, ' ') }}</td>
<td class="px-3 py-2 text-gray-900">{{ value || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>