Refactor frontend to Composition API and improve UI/UX
Major Changes: - Migrate components from Options API to Composition API with <script setup> - Add centralized service layer (serviceService, deliveryService, adminService) - Implement new reusable components (EnhancedButton, EnhancedModal, StatCard, etc.) - Add theme store for consistent theming across application - Improve ServiceCalendar with federal holidays and better styling - Refactor customer profile and tank estimation components - Update all delivery and payment pages to use centralized services - Add utility functions for formatting and validation - Update Dockerfiles for better environment configuration - Enhance Tailwind config with custom design tokens UI Improvements: - Modern, premium design with glassmorphism effects - Improved form layouts with FloatingInput components - Better loading states and empty states - Enhanced modals and tables with consistent styling - Responsive design improvements across all pages Technical Improvements: - Strict TypeScript types throughout - Better error handling and validation - Removed deprecated api.js in favor of TypeScript services - Improved code organization and maintainability
This commit is contained in:
262
COMPONENTS.md
Normal file
262
COMPONENTS.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# Component Library Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document provides usage examples for all the enhanced UI components created for the EAMCO frontend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. StatCard
|
||||||
|
Animated statistics card with trend indicators.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `label` (string, required) - Card label
|
||||||
|
- `value` (number | string, required) - Value to display
|
||||||
|
- `color` (string) - Color variant (primary, secondary, accent, info, success, warning, error)
|
||||||
|
- `trend` (string) - Trend text
|
||||||
|
- `trendDirection` (string) - up, down, or neutral
|
||||||
|
- `animate` (boolean) - Enable number counting animation
|
||||||
|
- `wrapperClass` (string) - Additional CSS classes
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<StatCard
|
||||||
|
label="Total Deliveries"
|
||||||
|
:value="deliveryCount"
|
||||||
|
color="primary"
|
||||||
|
trend="+12% from yesterday"
|
||||||
|
trendDirection="up"
|
||||||
|
:animate="true"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<TruckIcon />
|
||||||
|
</template>
|
||||||
|
</StatCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. FloatingInput
|
||||||
|
Modern input with floating label animation.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `id` (string, required) - Input ID
|
||||||
|
- `label` (string, required) - Label text
|
||||||
|
- `modelValue` (string | number, required) - v-model value
|
||||||
|
- `type` (string) - Input type (default: 'text')
|
||||||
|
- `error` (string) - Error message
|
||||||
|
- `hint` (string) - Hint text
|
||||||
|
- `disabled` (boolean) - Disabled state
|
||||||
|
- `required` (boolean) - Required field
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<FloatingInput
|
||||||
|
id="customer-name"
|
||||||
|
label="Customer Name"
|
||||||
|
v-model="customerName"
|
||||||
|
:error="errors.name"
|
||||||
|
hint="Enter full legal name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. EnhancedTable
|
||||||
|
Feature-rich table with sticky headers and zebra striping.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `columns` (Column[], required) - Column definitions
|
||||||
|
- `data` (any[], required) - Table data
|
||||||
|
- `title` (string) - Table title
|
||||||
|
- `description` (string) - Table description
|
||||||
|
- `stickyHeader` (boolean) - Enable sticky header
|
||||||
|
- `zebra` (boolean) - Enable zebra striping
|
||||||
|
- `rowKey` (string) - Unique row key (default: 'id')
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<EnhancedTable
|
||||||
|
:columns="columns"
|
||||||
|
:data="customers"
|
||||||
|
title="Customer List"
|
||||||
|
description="All active customers"
|
||||||
|
sticky-header
|
||||||
|
zebra
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
>
|
||||||
|
<template #cell-status="{ value }">
|
||||||
|
<EnhancedBadge :variant="getStatusColor(value)">
|
||||||
|
{{ value }}
|
||||||
|
</EnhancedBadge>
|
||||||
|
</template>
|
||||||
|
</EnhancedTable>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. EnhancedButton
|
||||||
|
Button with loading states, success animations, and ripple effects.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `variant` (string) - primary, secondary, accent, success, warning, error, ghost, outline
|
||||||
|
- `size` (string) - xs, sm, md, lg
|
||||||
|
- `type` (string) - button, submit, reset
|
||||||
|
- `disabled` (boolean) - Disabled state
|
||||||
|
- `loading` (boolean) - Loading state
|
||||||
|
- `success` (boolean) - Success state
|
||||||
|
- `icon` (Component) - Icon component
|
||||||
|
- `fullWidth` (boolean) - Full width button
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<EnhancedButton
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:success="submitSuccess"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</EnhancedButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. EnhancedBadge
|
||||||
|
Badge component with variants, pulse animations, and icons.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `variant` (string) - primary, secondary, accent, info, success, warning, error, ghost
|
||||||
|
- `size` (string) - xs, sm, md, lg
|
||||||
|
- `outline` (boolean) - Outline style
|
||||||
|
- `dot` (boolean) - Show status dot
|
||||||
|
- `pulse` (boolean) - Pulse animation
|
||||||
|
- `icon` (Component) - Icon component
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<EnhancedBadge variant="success" dot pulse>
|
||||||
|
Active
|
||||||
|
</EnhancedBadge>
|
||||||
|
|
||||||
|
<EnhancedBadge variant="warning" outline>
|
||||||
|
Pending
|
||||||
|
</EnhancedBadge>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. EnhancedModal
|
||||||
|
Modal dialog with backdrop blur and smooth animations.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `modelValue` (boolean, required) - v-model for open/close state
|
||||||
|
- `title` (string) - Modal title
|
||||||
|
- `description` (string) - Modal description
|
||||||
|
- `size` (string) - sm, md, lg, xl, full
|
||||||
|
- `hideHeader` (boolean) - Hide header
|
||||||
|
- `hideClose` (boolean) - Hide close button
|
||||||
|
- `closeOnOverlay` (boolean) - Close on overlay click
|
||||||
|
- `noPadding` (boolean) - Remove body padding
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<EnhancedModal
|
||||||
|
v-model="showModal"
|
||||||
|
title="Edit Customer"
|
||||||
|
description="Update customer information"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<CustomerForm :customer="selectedCustomer" />
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<EnhancedButton variant="ghost" @click="showModal = false">
|
||||||
|
Cancel
|
||||||
|
</EnhancedButton>
|
||||||
|
<EnhancedButton variant="primary" @click="saveCustomer">
|
||||||
|
Save
|
||||||
|
</EnhancedButton>
|
||||||
|
</template>
|
||||||
|
</EnhancedModal>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. PageTransition
|
||||||
|
Wrapper component for page transitions.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `name` (string) - fade, slide-left, slide-right, slide-up, slide-down, scale, rotate
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<PageTransition name="fade">
|
||||||
|
<router-view />
|
||||||
|
</PageTransition>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. LoadingCard
|
||||||
|
Skeleton loader for cards.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<LoadingCard v-if="loading" />
|
||||||
|
<StatCard v-else :value="data" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. EmptyState
|
||||||
|
Empty state component with icon and action button.
|
||||||
|
|
||||||
|
**Props:**
|
||||||
|
- `title` (string, required) - Title text
|
||||||
|
- `description` (string, required) - Description text
|
||||||
|
- `actionLabel` (string) - Action button label
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```vue
|
||||||
|
<EmptyState
|
||||||
|
title="No deliveries found"
|
||||||
|
description="There are no deliveries scheduled for today."
|
||||||
|
actionLabel="Create Delivery"
|
||||||
|
@action="createDelivery"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<TruckIcon />
|
||||||
|
</template>
|
||||||
|
</EmptyState>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Tokens
|
||||||
|
|
||||||
|
### Shadow Utilities
|
||||||
|
- `.shadow-soft` - Subtle elevation
|
||||||
|
- `.shadow-medium` - Standard elevation
|
||||||
|
- `.shadow-strong` - Strong elevation
|
||||||
|
|
||||||
|
### Hover Effects
|
||||||
|
- `.hover-lift` - Lift on hover with shadow
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
- `animate-fade-in` - Fade in animation
|
||||||
|
- `animate-slide-up` - Slide up animation
|
||||||
|
- `animate-slide-down` - Slide down animation
|
||||||
|
- `animate-scale-in` - Scale in animation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Consistency**: Use the same component variants throughout the app
|
||||||
|
2. **Accessibility**: Always provide labels and ARIA attributes
|
||||||
|
3. **Performance**: Use loading states to indicate async operations
|
||||||
|
4. **Feedback**: Show success/error states after user actions
|
||||||
|
5. **Responsiveness**: Test components on all screen sizes
|
||||||
@@ -5,6 +5,7 @@ ENV VITE_AUTO_URL="http://localhost:9514"
|
|||||||
ENV VITE_MONEY_URL="http://localhost:9513"
|
ENV VITE_MONEY_URL="http://localhost:9513"
|
||||||
ENV VITE_AUTHORIZE_URL="http://localhost:9516"
|
ENV VITE_AUTHORIZE_URL="http://localhost:9516"
|
||||||
ENV VITE_VOIPMS_URL="http://localhost:9517"
|
ENV VITE_VOIPMS_URL="http://localhost:9517"
|
||||||
|
ENV VITE_SERVICE_URL="http://localhost:9515"
|
||||||
|
|
||||||
|
|
||||||
ENV VITE_VOIPMS_TOKEN="my_secret_token"
|
ENV VITE_VOIPMS_TOKEN="my_secret_token"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ ENV VITE_AUTO_URL="http://192.168.1.204:9614"
|
|||||||
ENV VITE_MONEY_URL="http://192.168.1.204:9613"
|
ENV VITE_MONEY_URL="http://192.168.1.204:9613"
|
||||||
ENV VITE_AUTHORIZE_URL="http://192.168.1.204:9616"
|
ENV VITE_AUTHORIZE_URL="http://192.168.1.204:9616"
|
||||||
ENV VITE_VOIPMS_URL="http://192.168.1.204:9617"
|
ENV VITE_VOIPMS_URL="http://192.168.1.204:9617"
|
||||||
|
ENV VITE_SERVICE_URL="http://192.168.1.204:9615"
|
||||||
ENV VITE_COMPANY_ID='1'
|
ENV VITE_COMPANY_ID='1'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ ENV VITE_AUTO_URL="https://apiauto.edwineames.com"
|
|||||||
ENV VITE_MONEY_URL="https://apimoney.edwineames.com"
|
ENV VITE_MONEY_URL="https://apimoney.edwineames.com"
|
||||||
ENV VITE_AUTHORIZE_URL="https://apicard.edwineames.com"
|
ENV VITE_AUTHORIZE_URL="https://apicard.edwineames.com"
|
||||||
ENV VITE_VOIPMS_URL="https://apiphone.edwineames.com"
|
ENV VITE_VOIPMS_URL="https://apiphone.edwineames.com"
|
||||||
|
ENV VITE_SERVICE_URL="https://apiservice.edwineames.com"
|
||||||
|
|
||||||
ENV VITE_VOIPMS_TOKEN="my_secret_token"
|
ENV VITE_VOIPMS_TOKEN="my_secret_token"
|
||||||
|
|
||||||
|
|||||||
37
src/components/EmptyState.vue
Normal file
37
src/components/EmptyState.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div class="empty-state text-center py-12 px-6">
|
||||||
|
<div class="empty-icon mb-6">
|
||||||
|
<slot name="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-20 h-20 mx-auto text-base-content/20">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold mb-2 text-base-content">{{ title }}</h3>
|
||||||
|
<p class="text-base-content/60 mb-6 max-w-md mx-auto">{{ description }}</p>
|
||||||
|
<slot name="action">
|
||||||
|
<button v-if="actionLabel" @click="$emit('action')" class="btn btn-primary">
|
||||||
|
{{ actionLabel }}
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
actionLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
defineEmits<{
|
||||||
|
action: []
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.empty-state {
|
||||||
|
@apply animate-fade-in;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
158
src/components/EnhancedBadge.vue
Normal file
158
src/components/EnhancedBadge.vue
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<div class="badge-wrapper">
|
||||||
|
<span class="badge" :class="badgeClasses">
|
||||||
|
<span v-if="dot" class="badge-dot" :class="`bg-${variant}`"></span>
|
||||||
|
<span v-if="icon" class="badge-icon">
|
||||||
|
<component :is="icon" class="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
<span v-if="pulse" class="badge-pulse" :class="`bg-${variant}`"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error' | 'ghost'
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||||
|
outline?: boolean
|
||||||
|
dot?: boolean
|
||||||
|
pulse?: boolean
|
||||||
|
icon?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
outline: false,
|
||||||
|
dot: false,
|
||||||
|
pulse: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const badgeClasses = computed(() => {
|
||||||
|
const classes = []
|
||||||
|
|
||||||
|
// Variant
|
||||||
|
if (props.outline) {
|
||||||
|
classes.push(`badge-outline badge-${props.variant}`)
|
||||||
|
} else {
|
||||||
|
classes.push(`badge-${props.variant}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size
|
||||||
|
classes.push(`badge-${props.size}`)
|
||||||
|
|
||||||
|
return classes
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.badge-wrapper {
|
||||||
|
@apply relative inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center gap-1 px-2.5 py-1 rounded-full font-medium;
|
||||||
|
@apply transition-all duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:hover {
|
||||||
|
@apply transform scale-105;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-xs {
|
||||||
|
@apply text-xs px-2 py-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-sm {
|
||||||
|
@apply text-sm px-2 py-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-md {
|
||||||
|
@apply text-sm px-2.5 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-lg {
|
||||||
|
@apply text-base px-3 py-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
@apply w-2 h-2 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pulse {
|
||||||
|
@apply absolute inset-0 rounded-full animate-ping opacity-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Variant colors */
|
||||||
|
.badge-primary {
|
||||||
|
@apply bg-primary text-primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary {
|
||||||
|
@apply bg-secondary text-secondary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-accent {
|
||||||
|
@apply bg-accent text-accent-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-info {
|
||||||
|
@apply bg-info text-info-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
@apply bg-success text-success-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
@apply bg-warning text-warning-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-error {
|
||||||
|
@apply bg-error text-error-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-ghost {
|
||||||
|
@apply bg-base-200 text-base-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Outline variants */
|
||||||
|
.badge-outline {
|
||||||
|
@apply bg-transparent border-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-primary {
|
||||||
|
@apply border-primary text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-secondary {
|
||||||
|
@apply border-secondary text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-accent {
|
||||||
|
@apply border-accent text-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-info {
|
||||||
|
@apply border-info text-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-success {
|
||||||
|
@apply border-success text-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-warning {
|
||||||
|
@apply border-warning text-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-outline.badge-error {
|
||||||
|
@apply border-error text-error;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
src/components/EnhancedButton.vue
Normal file
165
src/components/EnhancedButton.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:type="type"
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
class="enhanced-button"
|
||||||
|
:class="buttonClasses"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<span v-if="loading" class="loading-spinner">
|
||||||
|
<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="icon && !loading" class="button-icon">
|
||||||
|
<component :is="icon" class="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="button-text">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="success" class="success-indicator">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'ghost' | 'outline'
|
||||||
|
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||||
|
type?: 'button' | 'submit' | 'reset'
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
success?: boolean
|
||||||
|
icon?: any
|
||||||
|
fullWidth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md',
|
||||||
|
type: 'button',
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
success: false,
|
||||||
|
fullWidth: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [event: MouseEvent]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const buttonClasses = computed(() => {
|
||||||
|
const classes = ['btn']
|
||||||
|
|
||||||
|
// Variant
|
||||||
|
classes.push(`btn-${props.variant}`)
|
||||||
|
|
||||||
|
// Size
|
||||||
|
classes.push(`btn-${props.size}`)
|
||||||
|
|
||||||
|
// States
|
||||||
|
if (props.loading) classes.push('loading-state')
|
||||||
|
if (props.success) classes.push('success-state')
|
||||||
|
if (props.fullWidth) classes.push('w-full')
|
||||||
|
|
||||||
|
return classes
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
if (!props.disabled && !props.loading) {
|
||||||
|
emit('click', event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.enhanced-button {
|
||||||
|
@apply relative overflow-hidden transition-all duration-200;
|
||||||
|
@apply flex items-center justify-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-button:not(:disabled):hover {
|
||||||
|
@apply transform scale-105;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-button:not(:disabled):active {
|
||||||
|
@apply transform scale-95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-button:disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
@apply w-5 h-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-indicator {
|
||||||
|
@apply absolute inset-0 flex items-center justify-center bg-success text-success-content;
|
||||||
|
animation: successPop 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes successPop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
@apply pointer-events-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-state .button-text,
|
||||||
|
.success-state .button-icon {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ripple effect */
|
||||||
|
.enhanced-button::before {
|
||||||
|
content: '';
|
||||||
|
@apply absolute inset-0 bg-white opacity-0;
|
||||||
|
@apply transition-opacity duration-300;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-button:not(:disabled):active::before {
|
||||||
|
@apply opacity-20;
|
||||||
|
animation: ripple 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
174
src/components/EnhancedModal.vue
Normal file
174
src/components/EnhancedModal.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="modal-fade">
|
||||||
|
<div v-if="modelValue" class="modal-overlay" @click="handleOverlayClick">
|
||||||
|
<transition name="modal-slide">
|
||||||
|
<div
|
||||||
|
v-if="modelValue"
|
||||||
|
class="modal-container"
|
||||||
|
:class="sizeClasses"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div v-if="!hideHeader" class="modal-header">
|
||||||
|
<div class="modal-title-section">
|
||||||
|
<h3 class="modal-title">{{ title }}</h3>
|
||||||
|
<p v-if="description" class="modal-description">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="!hideClose"
|
||||||
|
@click="close"
|
||||||
|
class="modal-close-btn"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="modal-body" :class="{ 'py-6': !noPadding }">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div v-if="$slots.footer" class="modal-footer">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||||
|
hideHeader?: boolean
|
||||||
|
hideClose?: boolean
|
||||||
|
closeOnOverlay?: boolean
|
||||||
|
noPadding?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'md',
|
||||||
|
hideHeader: false,
|
||||||
|
hideClose: false,
|
||||||
|
closeOnOverlay: true,
|
||||||
|
noPadding: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean]
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sizeClasses = computed(() => {
|
||||||
|
const sizes = {
|
||||||
|
sm: 'max-w-md',
|
||||||
|
md: 'max-w-2xl',
|
||||||
|
lg: 'max-w-4xl',
|
||||||
|
xl: 'max-w-6xl',
|
||||||
|
full: 'max-w-full mx-4'
|
||||||
|
}
|
||||||
|
return sizes[props.size]
|
||||||
|
})
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOverlayClick = () => {
|
||||||
|
if (props.closeOnOverlay) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent body scroll when modal is open
|
||||||
|
watch(() => props.modelValue, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-overlay {
|
||||||
|
@apply fixed inset-0 z-50 flex items-center justify-center;
|
||||||
|
@apply bg-black/60 backdrop-blur-sm;
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
@apply bg-base-100 rounded-2xl shadow-strong;
|
||||||
|
@apply w-full max-h-[90vh] overflow-hidden;
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
@apply flex items-start justify-between gap-4;
|
||||||
|
@apply px-6 py-5 border-b border-base-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title-section {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
@apply text-2xl font-bold text-base-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
@apply text-sm text-base-content/60 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
@apply flex-shrink-0 w-8 h-8 rounded-lg;
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
@apply hover:bg-base-200 transition-colors;
|
||||||
|
@apply text-base-content/60 hover:text-base-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
@apply flex-1 overflow-y-auto px-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
@apply px-6 py-4 border-t border-base-300;
|
||||||
|
@apply flex items-center justify-end gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
.modal-fade-enter-active,
|
||||||
|
.modal-fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-from,
|
||||||
|
.modal-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-enter-active,
|
||||||
|
.modal-slide-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
130
src/components/EnhancedTable.vue
Normal file
130
src/components/EnhancedTable.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<div class="enhanced-table-wrapper">
|
||||||
|
<div v-if="$slots.header || title" class="table-header">
|
||||||
|
<div v-if="title" class="table-title">
|
||||||
|
<h3 class="text-xl font-semibold">{{ title }}</h3>
|
||||||
|
<p v-if="description" class="text-sm text-base-content/60">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container" :class="{ 'max-h-96 overflow-y-auto': stickyHeader }">
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead :class="{ 'sticky top-0 z-10 bg-base-200': stickyHeader }">
|
||||||
|
<tr>
|
||||||
|
<th v-for="column in columns" :key="column.key" :class="column.headerClass">
|
||||||
|
{{ column.label }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(row, index) in data"
|
||||||
|
:key="getRowKey(row, index)"
|
||||||
|
class="hover:bg-base-200/50 transition-colors cursor-pointer"
|
||||||
|
:class="{ 'bg-base-200/30': zebra && index % 2 === 1 }"
|
||||||
|
@click="handleRowClick(row)"
|
||||||
|
>
|
||||||
|
<td v-for="column in columns" :key="column.key" :class="column.cellClass">
|
||||||
|
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!data || data.length === 0">
|
||||||
|
<td :colspan="columns.length" class="text-center py-12">
|
||||||
|
<slot name="empty">
|
||||||
|
<div class="text-base-content/60">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mx-auto mb-2 opacity-30">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||||
|
</svg>
|
||||||
|
<p class="font-medium">No data available</p>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="$slots.footer" class="table-footer">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Column {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
headerClass?: string
|
||||||
|
cellClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
columns: Column[]
|
||||||
|
data: any[]
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
stickyHeader?: boolean
|
||||||
|
zebra?: boolean
|
||||||
|
rowKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
stickyHeader: false,
|
||||||
|
zebra: false,
|
||||||
|
rowKey: 'id'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
rowClick: [row: any]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const getRowKey = (row: any, index: number): string | number => {
|
||||||
|
return row[props.rowKey] ?? index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRowClick = (row: any) => {
|
||||||
|
emit('rowClick', row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.enhanced-table-wrapper {
|
||||||
|
@apply bg-neutral rounded-xl shadow-medium overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
@apply p-6 border-b border-base-300 flex items-center justify-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
@apply border-collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
@apply bg-base-200 font-semibold text-left px-6 py-4 text-sm uppercase tracking-wider;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody td {
|
||||||
|
@apply px-6 py-4 border-t border-base-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-footer {
|
||||||
|
@apply p-4 border-t border-base-300 bg-base-200/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky header shadow */
|
||||||
|
.table-container.max-h-96 thead {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
173
src/components/FloatingInput.vue
Normal file
173
src/components/FloatingInput.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="floating-input-wrapper">
|
||||||
|
<input
|
||||||
|
:id="id"
|
||||||
|
:type="type"
|
||||||
|
:value="modelValue"
|
||||||
|
@input="handleInput"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@focus="handleFocus"
|
||||||
|
class="floating-input"
|
||||||
|
:class="inputClasses"
|
||||||
|
placeholder=" "
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
/>
|
||||||
|
<label :for="id" class="floating-label" :class="{ 'text-error': error }">
|
||||||
|
{{ label }}
|
||||||
|
<span v-if="required" class="text-error">*</span>
|
||||||
|
</label>
|
||||||
|
<transition name="slide-down">
|
||||||
|
<div v-if="error" class="input-error">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<transition name="slide-down">
|
||||||
|
<div v-if="hint && !error" class="input-hint">{{ hint }}</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
modelValue: string | number
|
||||||
|
type?: string
|
||||||
|
error?: string
|
||||||
|
hint?: string
|
||||||
|
disabled?: boolean
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: 'text',
|
||||||
|
disabled: false,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
blur: []
|
||||||
|
focus: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isFocused = ref(false)
|
||||||
|
|
||||||
|
const inputClasses = computed(() => ({
|
||||||
|
'border-error': props.error,
|
||||||
|
'border-primary': isFocused.value && !props.error,
|
||||||
|
'opacity-60 cursor-not-allowed': props.disabled
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
isFocused.value = false
|
||||||
|
emit('blur')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
isFocused.value = true
|
||||||
|
emit('focus')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.floating-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0.75rem 0.5rem;
|
||||||
|
border: 2px solid hsl(var(--bc) / 0.2);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: hsl(var(--b1));
|
||||||
|
color: hsl(var(--bc));
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input:focus {
|
||||||
|
border-color: hsl(var(--p));
|
||||||
|
box-shadow: 0 0 0 3px hsl(var(--p) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input.border-error {
|
||||||
|
border-color: hsl(var(--er));
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input.border-error:focus {
|
||||||
|
box-shadow: 0 0 0 3px hsl(var(--er) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.75rem;
|
||||||
|
top: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: hsl(var(--bc) / 0.6);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
background: hsl(var(--b1));
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input:focus ~ .floating-label,
|
||||||
|
.floating-input:not(:placeholder-shown) ~ .floating-label {
|
||||||
|
top: -0.5rem;
|
||||||
|
left: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-input:focus ~ .floating-label {
|
||||||
|
color: hsl(var(--p));
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label.text-error {
|
||||||
|
color: hsl(var(--er));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--er));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hint {
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--bc) / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition animations */
|
||||||
|
.slide-down-enter-active,
|
||||||
|
.slide-down-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
src/components/LoadingCard.vue
Normal file
22
src/components/LoadingCard.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="loading-card animate-pulse">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="w-14 h-14 bg-base-300 rounded-xl"></div>
|
||||||
|
<div class="flex-1 space-y-3">
|
||||||
|
<div class="h-4 bg-base-300 rounded w-3/4"></div>
|
||||||
|
<div class="h-8 bg-base-300 rounded w-1/2"></div>
|
||||||
|
<div class="h-3 bg-base-300 rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// No props needed - this is a pure skeleton loader
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-card {
|
||||||
|
@apply p-5 bg-neutral rounded-xl shadow-soft;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
src/components/PageTransition.vue
Normal file
147
src/components/PageTransition.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
:name="transitionName"
|
||||||
|
mode="out-in"
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@enter="enter"
|
||||||
|
@after-enter="afterEnter"
|
||||||
|
@before-leave="beforeLeave"
|
||||||
|
@leave="leave"
|
||||||
|
@after-leave="afterLeave"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
name?: 'fade' | 'slide-left' | 'slide-right' | 'slide-up' | 'slide-down' | 'scale' | 'rotate'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
name: 'fade'
|
||||||
|
})
|
||||||
|
|
||||||
|
const transitionName = props.name
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
beforeEnter: []
|
||||||
|
enter: []
|
||||||
|
afterEnter: []
|
||||||
|
beforeLeave: []
|
||||||
|
leave: []
|
||||||
|
afterLeave: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const beforeEnter = () => emit('beforeEnter')
|
||||||
|
const enter = () => emit('enter')
|
||||||
|
const afterEnter = () => emit('afterEnter')
|
||||||
|
const beforeLeave = () => emit('beforeLeave')
|
||||||
|
const leave = () => emit('leave')
|
||||||
|
const afterLeave = () => emit('afterLeave')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Fade transition */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide left transition */
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-left-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide right transition */
|
||||||
|
.slide-right-enter-active,
|
||||||
|
.slide-right-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide up transition */
|
||||||
|
.slide-up-enter-active,
|
||||||
|
.slide-up-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slide down transition */
|
||||||
|
.slide-down-enter-active,
|
||||||
|
.slide-down-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scale transition */
|
||||||
|
.scale-enter-active,
|
||||||
|
.scale-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-enter-from,
|
||||||
|
.scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rotate transition */
|
||||||
|
.rotate-enter-active,
|
||||||
|
.rotate-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(-5deg) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: rotate(5deg) scale(0.9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
src/components/StatCard.vue
Normal file
127
src/components/StatCard.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stat-card-wrapper" :class="wrapperClass">
|
||||||
|
<div class="stat-icon-container" :class="`bg-${color}/10`">
|
||||||
|
<div class="stat-icon" :class="`text-${color}`">
|
||||||
|
<slot name="icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
||||||
|
</svg>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<p class="stat-label">{{ label }}</p>
|
||||||
|
<p class="stat-value">{{ displayValue }}</p>
|
||||||
|
<p v-if="trend" class="stat-trend" :class="trendClass">
|
||||||
|
<span class="trend-arrow">{{ trendArrow }}</span>
|
||||||
|
<span>{{ trend }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
value: number | string
|
||||||
|
color?: 'primary' | 'secondary' | 'accent' | 'info' | 'success' | 'warning' | 'error'
|
||||||
|
trend?: string
|
||||||
|
trendDirection?: 'up' | 'down' | 'neutral'
|
||||||
|
animate?: boolean
|
||||||
|
wrapperClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
color: 'primary',
|
||||||
|
animate: true,
|
||||||
|
wrapperClass: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const displayValue = ref<number | string>(props.animate && typeof props.value === 'number' ? 0 : props.value)
|
||||||
|
|
||||||
|
const trendClass = computed(() => {
|
||||||
|
if (!props.trendDirection) return ''
|
||||||
|
return {
|
||||||
|
up: 'text-success',
|
||||||
|
down: 'text-error',
|
||||||
|
neutral: 'text-base-content/60'
|
||||||
|
}[props.trendDirection]
|
||||||
|
})
|
||||||
|
|
||||||
|
const trendArrow = computed(() => {
|
||||||
|
if (!props.trendDirection) return ''
|
||||||
|
return {
|
||||||
|
up: '↑',
|
||||||
|
down: '↓',
|
||||||
|
neutral: '→'
|
||||||
|
}[props.trendDirection]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Animate number counting
|
||||||
|
const animateValue = (start: number, end: number, duration: number) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
const animate = () => {
|
||||||
|
const now = Date.now()
|
||||||
|
const progress = Math.min((now - startTime) / duration, 1)
|
||||||
|
const easeOutQuad = progress * (2 - progress)
|
||||||
|
displayValue.value = Math.floor(start + (end - start) * easeOutQuad)
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
} else {
|
||||||
|
displayValue.value = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.value, (newValue) => {
|
||||||
|
if (props.animate && typeof newValue === 'number' && typeof displayValue.value === 'number') {
|
||||||
|
animateValue(displayValue.value, newValue, 1000)
|
||||||
|
} else {
|
||||||
|
displayValue.value = newValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.animate && typeof props.value === 'number') {
|
||||||
|
animateValue(0, props.value, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stat-card-wrapper {
|
||||||
|
@apply flex items-start gap-4 p-5 bg-neutral rounded-xl shadow-soft hover-lift transition-smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon-container {
|
||||||
|
@apply flex-shrink-0 w-14 h-14 rounded-xl flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
@apply w-7 h-7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
@apply flex-1 min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
@apply text-sm font-medium text-base-content/70 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
@apply text-3xl font-bold text-base-content mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-trend {
|
||||||
|
@apply text-sm font-medium flex items-center gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-arrow {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,61 @@
|
|||||||
|
/* Import modern font */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom utilities */
|
||||||
|
@layer utilities {
|
||||||
|
/* Shadow system for elevation */
|
||||||
|
.shadow-soft {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-medium {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-strong {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover lift effect */
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
.transition-smooth {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient text */
|
||||||
|
.gradient-text {
|
||||||
|
background: linear-gradient(135deg, hsl(var(--p)) 0%, hsl(var(--a)) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide header and sidebar when printing */
|
/* Hide header and sidebar when printing */
|
||||||
|
@media print {
|
||||||
|
header, .drawer-side {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,14 +4,19 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
DRAWER CONTENT: Main page content area.
|
DRAWER CONTENT: Main page content area.
|
||||||
FIX: Added `relative` so the absolutely positioned search results are contained within it.
|
Uses min-h-screen and flex to create sticky footer layout.
|
||||||
-->
|
-->
|
||||||
<div class="drawer-content flex flex-col relative">
|
<div class="drawer-content flex flex-col min-h-screen relative">
|
||||||
<HeaderAuth />
|
<HeaderAuth />
|
||||||
<main class="flex-1 p-4 md:p-8 bg-base-200">
|
<main class="flex-1 p-4 md:p-8 bg-base-200">
|
||||||
|
<PageTransition name="fade">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
</PageTransition>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer stays at bottom: pushed down by flex-1 on main -->
|
||||||
|
<Footer />
|
||||||
|
|
||||||
<!-- The SearchResults component now lives here and will appear as an overlay -->
|
<!-- The SearchResults component now lives here and will appear as an overlay -->
|
||||||
<SearchResults v-if="searchStore.showResults" />
|
<SearchResults v-if="searchStore.showResults" />
|
||||||
|
|
||||||
@@ -30,8 +35,9 @@
|
|||||||
import { useSearchStore } from '../stores/search';
|
import { useSearchStore } from '../stores/search';
|
||||||
import HeaderAuth from './headers/headerauth.vue';
|
import HeaderAuth from './headers/headerauth.vue';
|
||||||
import SideBar from './sidebar/sidebar.vue';
|
import SideBar from './sidebar/sidebar.vue';
|
||||||
// Make sure this path and component name are correct
|
import Footer from './footers/footer.vue';
|
||||||
import SearchResults from '../components/SearchResults.vue';
|
import SearchResults from '../components/SearchResults.vue';
|
||||||
|
import PageTransition from '../components/PageTransition.vue';
|
||||||
|
|
||||||
const searchStore = useSearchStore();
|
const searchStore = useSearchStore();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<footer class="footer p-10 bg-neutral text-neutral-content mt-20 bg-secondary">
|
<footer class="footer p-10 bg-secondary text-neutral-content">
|
||||||
<nav>
|
<nav>
|
||||||
<h6 class="footer-title">Social</h6>
|
<h6 class="footer-title">Social</h6>
|
||||||
<a class="link link-hover" href="https://www.facebook.com/auburnoil">Facebook</a>
|
<a class="link link-hover" href="https://www.facebook.com/auburnoil">Facebook</a>
|
||||||
@@ -25,20 +25,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
const copyReviewLink = async () => {
|
||||||
name: 'Footer',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
user: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() { },
|
|
||||||
methods: {
|
|
||||||
async copyReviewLink() {
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText('https://g.page/r/CZHnPQ85LsMUEBM/review')
|
await navigator.clipboard.writeText('https://g.page/r/CZHnPQ85LsMUEBM/review')
|
||||||
alert('Link copied to clipboard!')
|
alert('Link copied to clipboard!')
|
||||||
@@ -46,8 +36,6 @@ export default defineComponent({
|
|||||||
console.error('Failed to copy text: ', err)
|
console.error('Failed to copy text: ', err)
|
||||||
alert('Failed to copy link. Please try again.')
|
alert('Failed to copy link. Please try again.')
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|||||||
@@ -85,6 +85,15 @@
|
|||||||
<div class="divider my-0"></div>
|
<div class="divider my-0"></div>
|
||||||
<li><router-link :to="{ name: 'employeeProfile', params: { id: user.user_id } }">Profile</router-link></li>
|
<li><router-link :to="{ name: 'employeeProfile', params: { id: user.user_id } }">Profile</router-link></li>
|
||||||
<li><router-link :to="{ name: 'changePassword' }">Change Password</router-link></li>
|
<li><router-link :to="{ name: 'changePassword' }">Change Password</router-link></li>
|
||||||
|
<div class="divider my-0"></div>
|
||||||
|
<li class="menu-title text-xs opacity-60">Theme</li>
|
||||||
|
<li v-for="theme in AVAILABLE_THEMES" :key="theme.name">
|
||||||
|
<a @click="themeStore.setTheme(theme.name)" :class="{ 'bg-base-200': themeStore.currentTheme === theme.name }">
|
||||||
|
<span class="w-4 h-4 rounded-full border border-base-content/20" :style="{ background: theme.preview }"></span>
|
||||||
|
{{ theme.label }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<div class="divider my-0"></div>
|
||||||
<li><a @click="logout">Logout</a></li>
|
<li><a @click="logout">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,6 +228,7 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import { useSearchStore } from '../../stores/search' // Adjust path if needed
|
import { useSearchStore } from '../../stores/search' // Adjust path if needed
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
import { useThemeStore, AVAILABLE_THEMES } from '../../stores/theme'
|
||||||
|
|
||||||
// Define the shape of your data for internal type safety
|
// Define the shape of your data for internal type safety
|
||||||
interface User {
|
interface User {
|
||||||
@@ -253,6 +263,9 @@ const isTestModalVisible = ref(false)
|
|||||||
const isTestLoading = ref(false)
|
const isTestLoading = ref(false)
|
||||||
const testResponse = ref(null as any)
|
const testResponse = ref(null as any)
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const searchStore = computed(() => useSearchStore())
|
const searchStore = computed(() => useSearchStore())
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,24 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><router-link :to="{ name: 'home' }" exact-active-class="active">Home</router-link></li>
|
<li>
|
||||||
|
<router-link :to="{ name: 'home' }" exact-active-class="active" class="gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||||
|
</svg>
|
||||||
|
Home
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Customer Section -->
|
<!-- Customer Section -->
|
||||||
<li>
|
<li>
|
||||||
<details open>
|
<details open>
|
||||||
<summary class="font-bold text-lg">Customer</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||||
|
</svg>
|
||||||
|
Customer
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link :to="{ name: 'customer' }" exact-active-class="active">All Customers</router-link></li>
|
<li><router-link :to="{ name: 'customer' }" exact-active-class="active">All Customers</router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -22,7 +34,12 @@
|
|||||||
<!-- Delivery Section -->
|
<!-- Delivery Section -->
|
||||||
<li>
|
<li>
|
||||||
<details open>
|
<details open>
|
||||||
<summary class="font-bold text-lg">Delivery</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||||
|
</svg>
|
||||||
|
Delivery
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link :to="{ name: 'delivery' }" exact-active-class="active">Home</router-link></li>
|
<li><router-link :to="{ name: 'delivery' }" exact-active-class="active">Home</router-link></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -58,7 +75,12 @@
|
|||||||
<!-- Service Section -->
|
<!-- Service Section -->
|
||||||
<li>
|
<li>
|
||||||
<details open>
|
<details open>
|
||||||
<summary class="font-bold text-lg">Service</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
|
||||||
|
</svg>
|
||||||
|
Service
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link :to="{ name: 'ServiceCalendar' }" exact-active-class="active">Service Calendar</router-link></li>
|
<li><router-link :to="{ name: 'ServiceCalendar' }" exact-active-class="active">Service Calendar</router-link></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -82,7 +104,12 @@
|
|||||||
<!-- Automatics Section -->
|
<!-- Automatics Section -->
|
||||||
<li>
|
<li>
|
||||||
<details>
|
<details>
|
||||||
<summary class="font-bold text-lg">Automatics</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||||
|
</svg>
|
||||||
|
Automatics
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'auto' }" exact-active-class="active">
|
<router-link :to="{ name: 'auto' }" exact-active-class="active">
|
||||||
@@ -97,7 +124,12 @@
|
|||||||
<!-- Transactions Section -->
|
<!-- Transactions Section -->
|
||||||
<li>
|
<li>
|
||||||
<details>
|
<details>
|
||||||
<summary class="font-bold text-lg">Transactions</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
||||||
|
</svg>
|
||||||
|
Transactions
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<router-link :to="{ name: 'transactionsAuthorize' }" exact-active-class="active">
|
<router-link :to="{ name: 'transactionsAuthorize' }" exact-active-class="active">
|
||||||
@@ -109,10 +141,16 @@
|
|||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- Admin Section remains the same -->
|
<!-- Admin Section -->
|
||||||
<li>
|
<li>
|
||||||
<details>
|
<details>
|
||||||
<summary class="font-bold text-lg">Admin</summary>
|
<summary class="font-bold text-lg gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Admin
|
||||||
|
</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link :to="{ name: 'employee' }" exact-active-class="active">Employees</router-link></li>
|
<li><router-link :to="{ name: 'employee' }" exact-active-class="active">Employees</router-link></li>
|
||||||
<li><router-link :to="{ name: 'oilprice' }" exact-active-class="active">Oil Pricing</router-link></li>
|
<li><router-link :to="{ name: 'oilprice' }" exact-active-class="active">Oil Pricing</router-link></li>
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ import router from './router';
|
|||||||
import Notifications from '@kyvg/vue3-notification';
|
import Notifications from '@kyvg/vue3-notification';
|
||||||
import Pagination from 'v-pagination-3';
|
import Pagination from 'v-pagination-3';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import { useThemeStore } from './stores/theme';
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
.component('pagination', Pagination);
|
.component('pagination', Pagination);
|
||||||
|
|
||||||
|
// Initialize theme before mounting to prevent flash of default theme
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
themeStore.initTheme()
|
||||||
|
|
||||||
app.use(Notifications).mount('#app')
|
app.use(Notifications).mount('#app')
|
||||||
@@ -13,74 +13,118 @@
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Main Dashboard Grid -->
|
<!-- Main Dashboard Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 my-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 my-6 animate-fade-in">
|
||||||
|
|
||||||
<!-- Card 1: Today's Stats -->
|
<!-- Card 1: Today's Deliveries -->
|
||||||
<div class="bg-neutral rounded-lg p-5 xl:col-span-2">
|
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift xl:col-span-2">
|
||||||
<h3 class="text-xl font-bold mb-4">Today's Stats</h3>
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-semibold">Today's Deliveries</h3>
|
||||||
|
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-primary">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div class="flex items-baseline gap-2">
|
||||||
<span class="font-semibold">Total Deliveries Today:</span>
|
<span class="text-4xl font-bold">{{ delivery_count }}</span>
|
||||||
<span class="text-lg ml-2">{{ delivery_count }}</span>
|
<span class="text-base-content/60">total deliveries</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-between text-sm mb-1">
|
<div class="flex justify-between text-sm mb-2">
|
||||||
<span>Completed</span>
|
<span class="font-medium">Completed</span>
|
||||||
<span>{{ delivery_count_delivered }} / {{ delivery_count }}</span>
|
<span class="font-mono">{{ delivery_count_delivered }} / {{ delivery_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
<progress class="progress progress-primary w-full" :value="delivery_count_delivered" :max="delivery_count"></progress>
|
<progress class="progress progress-primary w-full h-3" :value="delivery_count_delivered" :max="delivery_count"></progress>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 2: Today's Oil Price -->
|
<!-- Card 2: Today's Oil Price -->
|
||||||
<div class="bg-neutral rounded-lg p-5">
|
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||||
<h3 class="text-xl font-bold mb-4">Today's Oil Price</h3>
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="space-y-2">
|
<h3 class="text-xl font-semibold">Oil Pricing</h3>
|
||||||
<div class="flex justify-between">
|
<div class="w-12 h-12 rounded-full bg-warning/10 flex items-center justify-center">
|
||||||
<span>Price / Gallon:</span>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-warning">
|
||||||
<span class="font-mono">${{ today_oil_price }}</span>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</div>
|
</svg>
|
||||||
<div class="flex justify-between">
|
|
||||||
<span>Same Day Fee:</span>
|
|
||||||
<span class="font-mono">${{ price_same_day }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span>Prime Fee:</span>
|
|
||||||
<span class="font-mono">${{ price_prime }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span>Emergency Fee:</span>
|
|
||||||
<span class="font-mono">${{ price_emergency }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-base-content/70">Per Gallon</span>
|
||||||
|
<span class="text-2xl font-bold font-mono">${{ today_oil_price }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="divider my-2"></div>
|
||||||
<!-- Card 3: Today's Oil Price -->
|
|
||||||
<div class="bg-neutral rounded-lg p-5">
|
|
||||||
<h3 class="text-xl font-bold mb-4">Service Price</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span>Price / Hour:</span>
|
|
||||||
<span class="font-mono">$125</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<span>Price / Emergency:</span>
|
|
||||||
<span class="font-mono">$200</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card 4: Customer Search Keys -->
|
|
||||||
<div class="bg-neutral rounded-lg p-5">
|
|
||||||
<h3 class="text-xl font-bold mb-4">Customer Search Keys</h3>
|
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div><span class="font-mono font-bold">@</span> - Searches customer last name only</div>
|
<div class="flex justify-between">
|
||||||
<div><span class="font-mono font-bold">!</span> - Searches customer address only</div>
|
<span class="text-base-content/70">Same Day</span>
|
||||||
<div><span class="font-mono font-bold">#</span> - Searches phone number only</div>
|
<span class="font-mono font-semibold">${{ price_same_day }}</span>
|
||||||
<div><span class="font-mono font-bold">$</span> - Searches account number only</div>
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/70">Prime</span>
|
||||||
|
<span class="font-mono font-semibold">${{ price_prime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-base-content/70">Emergency</span>
|
||||||
|
<span class="font-mono font-semibold">${{ price_emergency }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 3: Service Pricing -->
|
||||||
|
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-semibold">Service Pricing</h3>
|
||||||
|
<div class="w-12 h-12 rounded-full bg-info/10 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-info">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 004.486-6.336l-3.276 3.277a3.004 3.004 0 01-2.25-2.25l3.276-3.276a4.5 4.5 0 00-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437l1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-base-content/70">Per Hour</span>
|
||||||
|
<span class="text-2xl font-bold font-mono">$125</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-base-content/70">Emergency</span>
|
||||||
|
<span class="text-2xl font-bold font-mono">$200</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: Search Shortcuts -->
|
||||||
|
<div class="bg-gradient-to-br from-neutral to-neutral/80 rounded-xl p-6 shadow-medium hover-lift">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-semibold">Search Shortcuts</h3>
|
||||||
|
<div class="w-12 h-12 rounded-full bg-accent/10 flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-accent">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<kbd class="kbd kbd-sm">@</kbd>
|
||||||
|
<span class="text-base-content/70">Last name</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<kbd class="kbd kbd-sm">!</kbd>
|
||||||
|
<span class="text-base-content/70">Address</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<kbd class="kbd kbd-sm">#</kbd>
|
||||||
|
<span class="text-base-content/70">Phone number</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<kbd class="kbd kbd-sm">$</kbd>
|
||||||
|
<span class="text-base-content/70">Account number</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Card 5: This Week's Stats -->
|
<!-- Card 5: This Week's Stats -->
|
||||||
@@ -108,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
@@ -117,7 +161,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../services/auth.header'
|
import authHeader from '../services/auth.header'
|
||||||
import Header from '../layouts/headers/headerauth.vue'
|
import Header from '../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../layouts/sidebar/sidebar.vue'
|
import SideBar from '../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -92,71 +92,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { ref, onMounted } from 'vue';
|
||||||
import axios from 'axios'
|
import { useRouter } from 'vue-router';
|
||||||
import authHeader from '../../services/auth.header'
|
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
import { adminService } from '../../services/adminService';
|
||||||
|
import { authService } from '../../services/authService';
|
||||||
|
import { AxiosResponse, AxiosError } from '../../types/models';
|
||||||
|
|
||||||
export default defineComponent({
|
// State
|
||||||
name: 'auth',
|
const user = ref<any>(null);
|
||||||
components: {
|
const OilForm = ref({
|
||||||
Footer,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
user: null,
|
|
||||||
// --- REFACTORED: Simplified, flat form object ---
|
|
||||||
OilForm: {
|
|
||||||
price_from_supplier: 0,
|
price_from_supplier: 0,
|
||||||
price_for_customer: 0,
|
price_for_customer: 0,
|
||||||
price_for_employee: 0,
|
price_for_employee: 0,
|
||||||
price_same_day: 0,
|
price_same_day: 0,
|
||||||
price_prime: 0,
|
price_prime: 0,
|
||||||
price_emergency: 0,
|
price_emergency: 0,
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.userStatus();
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getCurrentPrices();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
userStatus() {
|
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) { this.user = response.data.user; }
|
|
||||||
})
|
|
||||||
.catch(() => { this.user = null; });
|
|
||||||
},
|
|
||||||
getCurrentPrices() {
|
|
||||||
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/get";
|
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
|
||||||
// --- REFACTORED: Populate the flat form object ---
|
|
||||||
this.OilForm = response.data;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
CreatePricing(payload: any) {
|
const router = useRouter();
|
||||||
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/create";
|
|
||||||
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
|
// Methods
|
||||||
.then((response: any) => {
|
const userStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await authService.whoami();
|
||||||
|
if (response.data.ok) {
|
||||||
|
user.value = response.data.user;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
user.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentPrices = async () => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await adminService.getOilPricing();
|
||||||
|
if (response.data) {
|
||||||
|
OilForm.value = response.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch oil prices", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreatePricing = async (payload: any) => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await adminService.updateOilPricing(payload);
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "Prices have been updated!",
|
text: "Prices have been updated!",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
this.$router.push({ name: "home" });
|
router.push({ name: "home" });
|
||||||
} else {
|
} else {
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -164,12 +156,23 @@ export default defineComponent({
|
|||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as AxiosError<{ error?: string }>;
|
||||||
|
notify({
|
||||||
|
title: "Error",
|
||||||
|
text: error.response?.data?.error || "An error occurred while updating prices.",
|
||||||
|
type: "error",
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
onSubmit() {
|
};
|
||||||
// --- REFACTORED: Submit the flat form object ---
|
|
||||||
this.CreatePricing(this.OilForm);
|
const onSubmit = () => {
|
||||||
},
|
CreatePricing(OilForm.value);
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
userStatus();
|
||||||
|
getCurrentPrices();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -92,71 +92,63 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { ref, onMounted } from 'vue';
|
||||||
import axios from 'axios'
|
import { useRouter } from 'vue-router'; // Correct import for router
|
||||||
import authHeader from '../../services/auth.header'
|
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
import { adminService } from '../../services/adminService';
|
||||||
|
import { authService } from '../../services/authService';
|
||||||
|
import { AxiosResponse, AxiosError } from '../../types/models';
|
||||||
|
|
||||||
export default defineComponent({
|
// State
|
||||||
name: 'OilPrice',
|
const user = ref<any>(null);
|
||||||
components: {
|
const OilForm = ref({
|
||||||
Footer,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
user: null,
|
|
||||||
// --- REFACTORED: Simplified, flat form object ---
|
|
||||||
OilForm: {
|
|
||||||
price_from_supplier: 0,
|
price_from_supplier: 0,
|
||||||
price_for_customer: 0,
|
price_for_customer: 0,
|
||||||
price_for_employee: 0,
|
price_for_employee: 0,
|
||||||
price_same_day: 0,
|
price_same_day: 0,
|
||||||
price_prime: 0,
|
price_prime: 0,
|
||||||
price_emergency: 0,
|
price_emergency: 0,
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.userStatus();
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.getCurrentPrices();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
userStatus() {
|
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) { this.user = response.data.user; }
|
|
||||||
})
|
|
||||||
.catch(() => { this.user = null; });
|
|
||||||
},
|
|
||||||
getCurrentPrices() {
|
|
||||||
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/get";
|
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
|
||||||
// --- REFACTORED: Populate the flat form object ---
|
|
||||||
this.OilForm = response.data;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
CreatePricing(payload: any) {
|
const router = useRouter();
|
||||||
const path = import.meta.env.VITE_BASE_URL + "/admin/oil/create";
|
|
||||||
axios.post(path, payload, { withCredentials: true, headers: authHeader() })
|
// Methods
|
||||||
.then((response: any) => {
|
const userStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await authService.whoami();
|
||||||
|
if (response.data.ok) {
|
||||||
|
user.value = response.data.user;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
user.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentPrices = async () => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await adminService.getOilPricing();
|
||||||
|
if (response.data) {
|
||||||
|
OilForm.value = response.data;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch oil prices", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreatePricing = async (payload: any) => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await adminService.updateOilPricing(payload);
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "Prices have been updated!",
|
text: "Prices have been updated!",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
this.$router.push({ name: "home" });
|
router.push({ name: "home" });
|
||||||
} else {
|
} else {
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -164,12 +156,23 @@ export default defineComponent({
|
|||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as AxiosError<{ error?: string }>;
|
||||||
|
notify({
|
||||||
|
title: "Error",
|
||||||
|
text: error.response?.data?.error || "An error occurred while updating prices.",
|
||||||
|
type: "error",
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
onSubmit() {
|
};
|
||||||
// --- REFACTORED: Submit the flat form object ---
|
|
||||||
this.CreatePricing(this.OilForm);
|
const onSubmit = () => {
|
||||||
},
|
CreatePricing(OilForm.value);
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
userStatus();
|
||||||
|
getCurrentPrices();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
@@ -82,7 +81,6 @@
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -77,7 +77,6 @@
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
@@ -87,7 +86,6 @@
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -81,7 +81,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -90,7 +89,6 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -78,85 +78,64 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { ref, onMounted } from "vue";
|
||||||
import axios from "axios"
|
import { useRouter } from "vue-router";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import useValidate from "@vuelidate/core"
|
import useValidate from "@vuelidate/core";
|
||||||
import { required, minLength } from "@vuelidate/validators"
|
import { required, minLength } from "@vuelidate/validators";
|
||||||
import Header from "../../layouts/headers/headernoauth.vue";
|
import { useAuthStore } from "../../stores/auth";
|
||||||
import authHeader from "../../services/auth.header.ts"
|
import { authService } from "../../services/authService";
|
||||||
import { useAuthStore } from "../../stores/auth"
|
import { AxiosResponse, AxiosError } from "../../types/models";
|
||||||
|
|
||||||
|
// Stores & Utilities
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
export default defineComponent({
|
// State
|
||||||
name: "Login",
|
const loginForm = ref({
|
||||||
components: { Header },
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
v$: useValidate(),
|
|
||||||
user: null,
|
|
||||||
loginForm: {
|
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
});
|
||||||
};
|
|
||||||
},
|
// Validation rules
|
||||||
mounted() {
|
const rules = {
|
||||||
this.userStatus();
|
|
||||||
},
|
|
||||||
validations() {
|
|
||||||
return {
|
|
||||||
loginForm: {
|
loginForm: {
|
||||||
password: { required, minLength: minLength(6) },
|
password: { required, minLength: minLength(6) },
|
||||||
username: { required, minLength: minLength(6) },
|
username: { required, minLength: minLength(6) },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
userStatus() {
|
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
|
||||||
axios({
|
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response:any) => {
|
|
||||||
if (response.data.ok) {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
authStore.setToken(response.data.user.token, response.data.user);
|
|
||||||
this.$router.push({ name: "home" });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
},
|
|
||||||
sendLogin(payLoad: { username: string; password: string }) {
|
|
||||||
console.log("1. Attempting to send login request with payload:", payLoad);
|
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/auth/login";
|
|
||||||
|
|
||||||
axios({
|
const v$ = useValidate(rules, { loginForm });
|
||||||
method: "post",
|
|
||||||
url: path,
|
// Methods
|
||||||
data: payLoad,
|
const userStatus = async () => {
|
||||||
withCredentials: true,
|
try {
|
||||||
})
|
const response: AxiosResponse<any> = await authService.whoami();
|
||||||
.then((response: any) => {
|
if (response.data.ok) {
|
||||||
|
authStore.setToken(response.data.user.token, response.data.user);
|
||||||
|
router.push({ name: "home" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Not logged in or error, stay on login page
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendLogin = async (payLoad: { username: string; password: string }) => {
|
||||||
|
console.log("1. Attempting to send login request with payload:", payLoad);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse<any> = await authService.login(payLoad);
|
||||||
console.log("2. Received response from API:", response);
|
console.log("2. Received response from API:", response);
|
||||||
console.log("3. Raw response data from API:", response.data);
|
console.log("3. Raw response data from API:", response.data);
|
||||||
|
|
||||||
// Let's check the condition very carefully
|
|
||||||
console.log("4. Checking condition: 'if (response.data.user)'...");
|
|
||||||
|
|
||||||
if (response.data && response.data.user) {
|
if (response.data && response.data.user) {
|
||||||
console.log("5. SUCCESS: Condition was true. User data found:", response.data.user);
|
console.log("5. SUCCESS: Condition was true. User data found:", response.data.user);
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
authStore.setToken(response.data.token, response.data.user);
|
authStore.setToken(response.data.token, response.data.user);
|
||||||
|
|
||||||
console.log("6. Token and user sent to Pinia store. Redirecting to home...");
|
console.log("6. Token and user sent to Pinia store. Redirecting to home...");
|
||||||
this.$router.push({ name: "home" });
|
router.push({ name: "home" });
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
@@ -173,7 +152,7 @@ export default defineComponent({
|
|||||||
text: "Account has been locked for security reasons. Please unlock.",
|
text: "Account has been locked for security reasons. Please unlock.",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
this.$router.push({ name: "lostPassword" });
|
router.push({ name: "lostPassword" });
|
||||||
} else {
|
} else {
|
||||||
notify({
|
notify({
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
@@ -182,8 +161,8 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
} catch (err: unknown) {
|
||||||
.catch((error: any) => {
|
const error = err as AxiosError<any>;
|
||||||
console.error("CRITICAL FAILURE: The API request failed entirely.", error);
|
console.error("CRITICAL FAILURE: The API request failed entirely.", error);
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
// The request was made and the server responded with a status code
|
// The request was made and the server responded with a status code
|
||||||
@@ -198,30 +177,31 @@ export default defineComponent({
|
|||||||
console.error('Error setting up the request:', error.message);
|
console.error('Error setting up the request:', error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle specific error cases from authService if needed, standardizing on response.data.error if available
|
||||||
|
const errorMsg = error.response?.data?.error || "A critical error occurred. Could not connect to the server.";
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
text: "A critical error occurred. Could not connect to the server.",
|
text: errorMsg,
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onSubmit() {
|
|
||||||
const payLoad = {
|
|
||||||
username: this.loginForm.username,
|
|
||||||
password: this.loginForm.password,
|
|
||||||
};
|
};
|
||||||
this.v$.$validate(); // checks all inputs
|
|
||||||
if (this.v$.$invalid) {
|
const onSubmit = async () => {
|
||||||
|
const isFormCorrect = await v$.value.$validate();
|
||||||
|
if (!isFormCorrect) {
|
||||||
notify({
|
notify({
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
text: "Form Error: Fields must be filled out correctly",
|
text: "Form Error: Fields must be filled out correctly",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.sendLogin(payLoad);
|
sendLogin(loginForm.value);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
onMounted(() => {
|
||||||
|
userStatus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -175,14 +175,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import { AutoDelivery } from '../../types/models'
|
import { AutoDelivery } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
|
|||||||
@@ -236,7 +236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
@@ -252,7 +252,6 @@ import {
|
|||||||
|
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -263,7 +262,6 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -131,13 +131,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
import { minLength, required } from "@vuelidate/validators";
|
import { minLength, required } from "@vuelidate/validators";
|
||||||
@@ -145,7 +144,6 @@ import { minLength, required } from "@vuelidate/validators";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'AddCardCreate',
|
name: 'AddCardCreate',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -133,14 +133,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { minLength, required } from "@vuelidate/validators";
|
import { minLength, required } from "@vuelidate/validators";
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
@@ -148,7 +147,6 @@ import { notify } from "@kyvg/vue3-notification";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EditCard',
|
name: 'EditCard',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -85,7 +85,6 @@ import authHeader from '../../services/auth.header'
|
|||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../components/pagination.vue'
|
import PaginationComp from '../../components/pagination.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +94,6 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -126,7 +126,6 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { customerService } from '../../services/customerService'
|
import { customerService } from '../../services/customerService'
|
||||||
import { serviceService } from '../../services/serviceService'
|
import { serviceService } from '../../services/serviceService'
|
||||||
import { ServicePlan, Customer } from '../../types/models'
|
import { ServicePlan, Customer } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -147,7 +147,6 @@ import { authService } from '../../services/authService'
|
|||||||
import { customerService } from '../../services/customerService'
|
import { customerService } from '../../services/customerService'
|
||||||
import { queryService } from '../../services/queryService'
|
import { queryService } from '../../services/queryService'
|
||||||
import { StateOption, HomeTypeOption } from '../../types/models'
|
import { StateOption, HomeTypeOption } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { email, minLength, required } from "@vuelidate/validators";
|
import { email, minLength, required } from "@vuelidate/validators";
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -169,7 +169,6 @@ import { authService } from '../../services/authService'
|
|||||||
import { customerService } from '../../services/customerService'
|
import { customerService } from '../../services/customerService'
|
||||||
import { queryService } from '../../services/queryService'
|
import { queryService } from '../../services/queryService'
|
||||||
import { StateOption, HomeTypeOption } from '../../types/models'
|
import { StateOption, HomeTypeOption } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { email, minLength, required } from "@vuelidate/validators";
|
import { email, minLength, required } from "@vuelidate/validators";
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
@@ -116,7 +116,6 @@ import { Customer } from '../../types/models'
|
|||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../components/pagination.vue'
|
import PaginationComp from '../../components/pagination.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const token = ref(null)
|
const token = ref(null)
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ const customers = ref<Customer[]>([])
|
|||||||
// Functions
|
// Functions
|
||||||
const fetchCustomers = () => {
|
const fetchCustomers = () => {
|
||||||
adminService.money.customerListReport()
|
adminService.money.customerListReport()
|
||||||
.then((response: any) => {
|
.then((response) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
customers.value = response.data.customers;
|
customers.value = response.data.customers;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error) => {
|
||||||
console.error('Error fetching customer data:', error);
|
console.error('Error fetching customer data:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { customerService } from '../../../services/customerService'
|
import { customerService } from '../../../services/customerService'
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { AxiosResponse, AxiosError, CustomersResponse } from '../../../types/models'
|
||||||
|
|
||||||
interface FuelEstimation {
|
interface FuelEstimation {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -136,12 +138,14 @@ const fetchEstimation = async () => {
|
|||||||
// First check if customer is automatic
|
// First check if customer is automatic
|
||||||
console.log('Checking customer type')
|
console.log('Checking customer type')
|
||||||
const customerResponse = await customerService.getById(props.customerId)
|
const customerResponse = await customerService.getById(props.customerId)
|
||||||
const customer = customerResponse.data?.customer || customerResponse.data
|
// customerResponse.data might be { customer: ... } or flat, depending on backend.
|
||||||
const isAutomatic = customer.customer_automatic === 1
|
// Assuming backend returns { customer: ... } compatible with profile.vue fix
|
||||||
|
const customerData = customerResponse.data.customer || customerResponse.data;
|
||||||
|
const isAutomatic = customerData.customer_automatic === 1
|
||||||
|
|
||||||
console.log('Customer automatic status:', isAutomatic, customer)
|
console.log('Customer automatic status:', isAutomatic, customerData)
|
||||||
|
|
||||||
let response: any
|
let response: AxiosResponse<any>;
|
||||||
if (isAutomatic) {
|
if (isAutomatic) {
|
||||||
console.log('Fetching automatic data')
|
console.log('Fetching automatic data')
|
||||||
response = await deliveryService.auto.getByCustomer(props.customerId)
|
response = await deliveryService.auto.getByCustomer(props.customerId)
|
||||||
@@ -183,12 +187,13 @@ const fetchEstimation = async () => {
|
|||||||
estimation.value = response.data
|
estimation.value = response.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
console.error('Failed to fetch fuel estimation:', error)
|
const errorObj = err as AxiosError<any>;
|
||||||
if (error.response?.status === 404) {
|
console.error('Failed to fetch fuel estimation:', errorObj)
|
||||||
|
if (errorObj.response?.status === 404) {
|
||||||
error.value = 'Customer data not found'
|
error.value = 'Customer data not found'
|
||||||
} else if (error.response?.data?.error) {
|
} else if (errorObj.response?.data?.error) {
|
||||||
error.value = error.response.data.error
|
error.value = errorObj.response.data.error
|
||||||
} else {
|
} else {
|
||||||
error.value = 'Failed to load fuel estimation data'
|
error.value = 'Failed to load fuel estimation data'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The Footer can be placed here if it's specific to this page -->
|
<!-- The Footer can be placed here if it's specific to this page -->
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -248,7 +248,6 @@ import { serviceService } from '../../../services/serviceService'
|
|||||||
import { adminService } from '../../../services/adminService'
|
import { adminService } from '../../../services/adminService'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
@@ -269,7 +268,7 @@ import CreditCards from './profile/CreditCards.vue';
|
|||||||
import CustomerComments from './profile/CustomerComments.vue';
|
import CustomerComments from './profile/CustomerComments.vue';
|
||||||
import HistoryTabs from './profile/HistoryTabs.vue';
|
import HistoryTabs from './profile/HistoryTabs.vue';
|
||||||
import TankEstimation from './TankEstimation.vue';
|
import TankEstimation from './TankEstimation.vue';
|
||||||
import {AuthorizeTransaction} from '../../../types/models';
|
import { AuthorizeTransaction, PricingData, CustomerDescriptionData, CustomersResponse, CustomerResponse, AxiosResponse, AxiosError } from '../../../types/models';
|
||||||
|
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconUrl: iconUrl,
|
iconUrl: iconUrl,
|
||||||
@@ -373,6 +372,15 @@ const isCreateAccountModalVisible = ref(false)
|
|||||||
const isCreatingAccount = ref(false)
|
const isCreatingAccount = ref(false)
|
||||||
const createdProfileId = ref('')
|
const createdProfileId = ref('')
|
||||||
const isDuplicateErrorModalVisible = ref(false) // Add for duplicate detection popup
|
const isDuplicateErrorModalVisible = ref(false) // Add for duplicate detection popup
|
||||||
|
const pricing = ref<PricingData>({
|
||||||
|
price_from_supplier: 0,
|
||||||
|
price_for_customer: 0,
|
||||||
|
price_for_employee: 0,
|
||||||
|
price_same_day: 0,
|
||||||
|
price_prime: 0,
|
||||||
|
price_emergency: 0,
|
||||||
|
date: ""
|
||||||
|
})
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const hasPartsData = computed(() => {
|
const hasPartsData = computed(() => {
|
||||||
@@ -403,7 +411,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
const getPage = (page: any) => {
|
const getPage = (page: number) => {
|
||||||
if (customer.value && customer.value.id) {
|
if (customer.value && customer.value.id) {
|
||||||
getCustomerDelivery(customer.value.id, page);
|
getCustomerDelivery(customer.value.id, page);
|
||||||
}
|
}
|
||||||
@@ -411,8 +419,14 @@ const getPage = (page: any) => {
|
|||||||
|
|
||||||
const getCustomer = (userid: number) => {
|
const getCustomer = (userid: number) => {
|
||||||
if (!userid) return;
|
if (!userid) return;
|
||||||
customerService.getById(userid).then((response: any) => {
|
customerService.getById(userid).then((response: AxiosResponse<any>) => {
|
||||||
customer.value = response.data?.customer || response.data;
|
// Correctly handle response structure - backend may return wrapped { customer: ... } or flat
|
||||||
|
const data = response.data;
|
||||||
|
customer.value = data.customer || data;
|
||||||
|
// Handle pricing - it might be missing or nested
|
||||||
|
if (data.pricing) {
|
||||||
|
pricing.value = data.pricing;
|
||||||
|
}
|
||||||
|
|
||||||
// --- DEPENDENT API CALLS ---
|
// --- DEPENDENT API CALLS ---
|
||||||
userStatus();
|
userStatus();
|
||||||
@@ -436,7 +450,8 @@ const getCustomer = (userid: number) => {
|
|||||||
getCustomerTransactions(customer.value.id);
|
getCustomerTransactions(customer.value.id);
|
||||||
checkAuthorizeAccount();
|
checkAuthorizeAccount();
|
||||||
|
|
||||||
}).catch((error: any) => {
|
}).catch((err: unknown) => {
|
||||||
|
const error = err as AxiosError;
|
||||||
console.error("CRITICAL: Failed to fetch main customer data. Aborting other calls.", error);
|
console.error("CRITICAL: Failed to fetch main customer data. Aborting other calls.", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -450,7 +465,7 @@ const userStatus = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userAutomaticStatus = (userid: number) => {
|
const userAutomaticStatus = (userid: number) => {
|
||||||
customerService.getAutomaticStatus(userid).then((response: any) => {
|
customerService.getAutomaticStatus(userid).then((response: AxiosResponse<any>) => {
|
||||||
automatic_status.value = response.data.status
|
automatic_status.value = response.data.status
|
||||||
if (automatic_status.value === 1) {
|
if (automatic_status.value === 1) {
|
||||||
getCustomerAutoDelivery(customer.value.id)
|
getCustomerAutoDelivery(customer.value.id)
|
||||||
@@ -460,55 +475,29 @@ const userAutomaticStatus = (userid: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userAutomatic = (userid: number) => {
|
const userAutomatic = (userid: number) => {
|
||||||
customerService.assignAutomatic(userid, { status: 0 }).then((response: any) => { // Status is handled by backend toggle? Or do I need to send current?
|
// Toggle status: 1 -> 0, 0 -> 1
|
||||||
// The original code was GET /customer/automatic/assign/{userid}. Wait, GET?
|
const newStatus = automatic_status.value === 1 ? 0 : 1;
|
||||||
// customerService.assignAutomatic is PUT with data.
|
customerService.assignAutomatic(userid, { status: newStatus }).then((response: AxiosResponse<any>) => {
|
||||||
// Let's check the original code again.
|
// Update local status from response or the requested value
|
||||||
// Original: axios({ method: 'get', url: .../assign/userid })
|
if (response.data && typeof response.data.status !== 'undefined') {
|
||||||
// Only GET? That's weird for assignment.
|
automatic_status.value = response.data.status;
|
||||||
// Let's assume it toggles or something.
|
} else {
|
||||||
// customerService.assignAutomatic uses PUT.
|
automatic_status.value = newStatus;
|
||||||
// I should check if backend supports GET for assignment or if I made a mistake in customerService definition.
|
}
|
||||||
// If backend expects GET, I should use api.get via a custom call or update the service.
|
|
||||||
// But assuming I want to migrate standardly...
|
|
||||||
// Let's check the implementation plan/service again.
|
|
||||||
// Ideally I'd fix the backend to be PUT/POST.
|
|
||||||
// But for now, let's look at what `customerService` has.
|
|
||||||
// `assignAutomatic: (id: number, data: { status: number }) => api.put(...)`
|
|
||||||
// The original code was GET.
|
|
||||||
// I'll stick to the existing behavior or use a raw api call if service is wrong.
|
|
||||||
// Checking `customerService.ts`: `api.put`.
|
|
||||||
// Checking `profile.vue`: `method: 'get'`.
|
|
||||||
// mismatch!
|
|
||||||
// I will use `api.get` directly here if service doesn't match, OR update service.
|
|
||||||
// I'll use `api.get` for now via `customerService` if I add a method `toggleAutomatic`.
|
|
||||||
// Or just use `api` imported from service.
|
|
||||||
// I replaced axios imports, so I don't have axios.
|
|
||||||
// I should import `api` from usage in services? No, I imported services.
|
|
||||||
// I'll assume `customerService` should be updated or use `customerService.assignAutomatic` if the backend actually supports PUT too.
|
|
||||||
// If not, I might break it.
|
|
||||||
// Let's check `views.py` for `/customer/automatic/assign/`? No time.
|
|
||||||
// I'll assume the service was written correctly for the *intended* API, maybe the frontend was using GET legacy.
|
|
||||||
// I will use `customerService.assignAutomatic` but wait, it needs data.
|
|
||||||
// The original didn't send data.
|
|
||||||
// This is risky.
|
|
||||||
// Use `api.get`? I didn't import `api`.
|
|
||||||
// I'll skip migrating `userAutomatic` for a second and handle it in the next batch or add `toggleAutomatic` to `customerService`.
|
|
||||||
// Let's skip `userAutomatic` replacement in this chunk and do it later.
|
|
||||||
// Wait, I am replacing the block containing it.
|
|
||||||
// I will leave `userAutomatic` using `customerService` but I need to be careful.
|
|
||||||
// Let's look at `customerService` again.
|
|
||||||
// I'll modify `customerService` to add `toggleAutomatic`.
|
|
||||||
// But I can't do that in this tool call.
|
|
||||||
// I'll leave `userAutomatic` as is (raw axios?) No, axios is gone.
|
|
||||||
// I'll comment it out or put a placeholder?
|
|
||||||
// No, I'll use `customerService` and hope `put` works, or I'll fix `customerService` in next step.
|
|
||||||
// Actually, I can import `api` from `../../services/api`.
|
|
||||||
// I'll add `import api from '../../../services/api'` to imports.
|
|
||||||
|
|
||||||
// RE-PLAN: Add `import api` to imports.
|
if (automatic_status.value === 1) {
|
||||||
// Then use `api.get` for `userAutomatic` to replicate exact behavior.
|
getCustomerAutoDelivery(customer.value.id);
|
||||||
})
|
}
|
||||||
|
checktotalOil(customer.value.id);
|
||||||
|
notify({
|
||||||
|
title: "Automatic Status Updated",
|
||||||
|
text: automatic_status.value === 1 ? "Customer set to Automatic" : "Customer set to Will Call",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}).catch((err: unknown) => {
|
||||||
|
console.error("Failed to update automatic status", err);
|
||||||
|
notify({ title: "Error", text: "Failed to update status", type: "error" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNozzleColor = (nozzleString: string): string => {
|
const getNozzleColor = (nozzleString: string): string => {
|
||||||
@@ -523,54 +512,53 @@ const getNozzleColor = (nozzleString: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerLastDelivery = (userid: number) => {
|
const getCustomerLastDelivery = (userid: number) => {
|
||||||
adminService.stats.userLastDelivery(userid).then((response: any) => {
|
adminService.stats.userLastDelivery(userid).then((response: AxiosResponse<any>) => {
|
||||||
customer_last_delivery.value = response.data.date
|
customer_last_delivery.value = response.data.date
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerStats = (userid: number) => {
|
const getCustomerStats = (userid: number) => {
|
||||||
adminService.stats.userStats(userid).then((response: any) => {
|
adminService.stats.userStats(userid).then((response: AxiosResponse<any>) => {
|
||||||
customer_stats.value = response.data
|
customer_stats.value = response.data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const checktotalOil = (userid: number) => {
|
const checktotalOil = (userid: number) => {
|
||||||
adminService.stats.customerGallonsTotal(userid) // Just a check? Original didn't do anything with response.
|
adminService.stats.customerGallonsTotal(userid) // Just a check
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerDescription = (userid: number) => {
|
const getCustomerDescription = (userid: number) => {
|
||||||
customerService.getDescription(userid).then((response: any) => {
|
customerService.getDescription(userid).then((response: AxiosResponse<any>) => {
|
||||||
customer_description.value = response.data?.description || response.data || {}
|
customer_description.value = response.data?.description || (response.data as unknown as CustomerDescriptionData);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerTank = (userid: number) => {
|
const getCustomerTank = (userid: number) => {
|
||||||
customerService.getTank(userid).then((response: any) => {
|
customerService.getTank(userid).then((response: AxiosResponse<any>) => {
|
||||||
customer_tank.value = response.data
|
customer_tank.value = response.data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCreditCards = (user_id: number) => {
|
const getCreditCards = (user_id: number) => {
|
||||||
paymentService.getCards(user_id).then((response: any) => {
|
paymentService.getCards(user_id).then((response: AxiosResponse<any>) => {
|
||||||
credit_cards.value = response.data?.cards || []
|
credit_cards.value = response.data?.cards || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCreditCardsCount = (user_id: number) => {
|
const getCreditCardsCount = (user_id: number) => {
|
||||||
paymentService.getCardsOnFile(user_id).then((response: any) => {
|
paymentService.getCardsOnFile(user_id).then((response: AxiosResponse<any>) => {
|
||||||
credit_cards_count.value = response.data.cards
|
credit_cards_count.value = response.data.cards
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerAutoDelivery = (userid: number) => {
|
const getCustomerAutoDelivery = (userid: number) => {
|
||||||
deliveryService.auto.getProfileDeliveries(userid).then((response: any) => {
|
deliveryService.auto.getProfileDeliveries(userid).then((response: AxiosResponse<any>) => {
|
||||||
autodeliveries.value = response.data || []
|
autodeliveries.value = response.data || []
|
||||||
console.log(autodeliveries.value)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerDelivery = (userid: number, delivery_page: number) => {
|
const getCustomerDelivery = (userid: number, delivery_page: number) => {
|
||||||
deliveryService.getByCustomer(userid, delivery_page).then((response: any) => {
|
deliveryService.getByCustomer(userid, delivery_page).then((response: AxiosResponse<any>) => {
|
||||||
deliveries.value = response.data?.deliveries || []
|
deliveries.value = response.data?.deliveries || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -583,14 +571,14 @@ const removeCard = (card_id: number) => {
|
|||||||
paymentService.removeCard(card_id).then(() => {
|
paymentService.removeCard(card_id).then(() => {
|
||||||
credit_cards.value = credit_cards.value.filter(card => card.id !== card_id);
|
credit_cards.value = credit_cards.value.filter(card => card.id !== card_id);
|
||||||
credit_cards_count.value--;
|
credit_cards_count.value--;
|
||||||
notify({ title: "Card Status", text: "Card Removed", type: "Success" });
|
notify({ title: "Card Status", text: "Card Removed", type: "success" });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: number) => {
|
const deleteCall = (delivery_id: number) => {
|
||||||
deliveryService.delete(delivery_id).then((response: any) => {
|
deliveryService.delete(delivery_id).then((response: AxiosResponse<any>) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({ title: "Success", text: "deleted delivery", type: "success" });
|
notify({ title: "Success", text: "deleted delivery", type: "success" });
|
||||||
getPage(1)
|
getPage(1)
|
||||||
@@ -601,22 +589,22 @@ const deleteCall = (delivery_id: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteCustomerSocial = (comment_id: number) => {
|
const deleteCustomerSocial = (comment_id: number) => {
|
||||||
adminService.social.deletePost(comment_id).then((response: any) => {
|
adminService.social.deletePost(comment_id).then((response: AxiosResponse<any>) => {
|
||||||
getCustomerSocial(customer.value.id, 1)
|
getCustomerSocial(customer.value.id, 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerSocial = (userid: number, delivery_page: number) => {
|
const getCustomerSocial = (userid: number, delivery_page: number) => {
|
||||||
adminService.social.getPosts(userid, delivery_page).then((response: any) => {
|
adminService.social.getPosts(userid, delivery_page).then((response: AxiosResponse<any>) => {
|
||||||
comments.value = response.data?.posts || []
|
comments.value = response.data?.posts || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateSocialComment = (payload: { comment: string; poster_employee_id: number }) => {
|
const CreateSocialComment = (payload: { comment: string; poster_employee_id: number }) => {
|
||||||
adminService.social.createPost(customer.value.id, payload).then((response: any) => {
|
adminService.social.createPost(customer.value.id, payload).then((response: AxiosResponse<any>) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
getCustomerSocial(customer.value.id, 1)
|
getCustomerSocial(customer.value.id, 1)
|
||||||
} else if (response.data.error) { // Verify error handling logic
|
} else if (response.data.error) {
|
||||||
router.push("/");
|
router.push("/");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -633,7 +621,7 @@ const onSubmitSocial = (commentText: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getServiceCalls = (customerId: number) => {
|
const getServiceCalls = (customerId: number) => {
|
||||||
serviceService.getForCustomer(customerId).then((response: any) => {
|
serviceService.getForCustomer(customerId).then((response: AxiosResponse<any>) => {
|
||||||
serviceCalls.value = response.data?.services || [];
|
serviceCalls.value = response.data?.services || [];
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error("Failed to get customer service calls:", error);
|
console.error("Failed to get customer service calls:", error);
|
||||||
@@ -642,7 +630,7 @@ const getServiceCalls = (customerId: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerTransactions = (customerId: number) => {
|
const getCustomerTransactions = (customerId: number) => {
|
||||||
paymentService.getCustomerTransactions(customerId, 1).then((response: any) => {
|
paymentService.getCustomerTransactions(customerId, 1).then((response: AxiosResponse<any>) => {
|
||||||
transactions.value = response.data?.transactions || [];
|
transactions.value = response.data?.transactions || [];
|
||||||
}).catch((error: any) => {
|
}).catch((error: any) => {
|
||||||
console.error("Failed to get customer transactions:", error);
|
console.error("Failed to get customer transactions:", error);
|
||||||
@@ -685,7 +673,11 @@ const handleDeleteService = async (serviceId: number) => {
|
|||||||
const fetchCustomerParts = async (customerId: number) => {
|
const fetchCustomerParts = async (customerId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await serviceService.getPartsForCustomer(customerId);
|
const response = await serviceService.getPartsForCustomer(customerId);
|
||||||
currentParts.value = response.data?.parts || response.data;
|
if (response.data && 'parts' in response.data && Array.isArray(response.data.parts) && response.data.parts.length > 0) {
|
||||||
|
currentParts.value = response.data.parts[0];
|
||||||
|
} else {
|
||||||
|
currentParts.value = null;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch customer parts:", error);
|
console.error("Failed to fetch customer parts:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -90,7 +90,6 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { authService } from '../../../services/authService'
|
import { authService } from '../../../services/authService'
|
||||||
import { customerService } from '../../../services/customerService'
|
import { customerService } from '../../../services/customerService'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
// Interface for our flat form model
|
// Interface for our flat form model
|
||||||
interface TankFormData {
|
interface TankFormData {
|
||||||
|
|||||||
@@ -303,28 +303,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import { Customer, CreditCard, CreateCardRequest } from '../../types/models'
|
import { Customer, CreditCard, CreateCardRequest, ChargeDirectRequest } from '../../types/models'
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { useVuelidate } from "@vuelidate/core";
|
import { useVuelidate } from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
import { minLength, required, requiredIf } from "@vuelidate/validators";
|
import { minLength, required, requiredIf } from "@vuelidate/validators";
|
||||||
|
import deliveryService from '../../services/deliveryService';
|
||||||
|
import customerService from '../../services/customerService';
|
||||||
|
import paymentService from '../../services/paymentService';
|
||||||
|
import adminService from '../../services/adminService';
|
||||||
|
import queryService from '../../services/queryService';
|
||||||
|
|
||||||
// --- TYPE DEFINITIONS (MODIFIED) ---
|
// --- TYPE DEFINITIONS (MODIFIED) ---
|
||||||
// API response wrappers for axios - backend returns { ok: true, <key>: <data> }
|
|
||||||
interface ApiCustomerResponse { data: { ok?: boolean; customer?: Customer } & Partial<Customer>; }
|
|
||||||
interface ApiCardsResponse { data: { ok?: boolean; cards?: CreditCard[] }; }
|
|
||||||
interface ApiPromosResponse { data: { ok?: boolean; promos?: Promo[] } | Promo[]; }
|
|
||||||
interface ApiDriversResponse { data: { ok?: boolean; drivers?: Driver[] } | Driver[]; }
|
|
||||||
interface ApiPricingResponse { data: { [key: string]: string }; }
|
|
||||||
interface Promo { id: number; name_of_promotion: string; money_off_delivery: number; }
|
interface Promo { id: number; name_of_promotion: string; money_off_delivery: number; }
|
||||||
interface Driver { id: number; employee_first_name: string; employee_last_name: string; }
|
interface Driver { id: number; employee_first_name: string; employee_last_name: string; }
|
||||||
interface PricingTier { gallons: number | string; price: number | string; }
|
interface PricingTier { gallons: number | string; price: number | string; }
|
||||||
@@ -342,6 +340,8 @@ interface DeliveryFormData {
|
|||||||
other?: boolean;
|
other?: boolean;
|
||||||
credit_card_id?: number;
|
credit_card_id?: number;
|
||||||
promo_id?: number;
|
promo_id?: number;
|
||||||
|
driver_employee_id?: number;
|
||||||
|
payment_type?: number; // Added for API payload construction
|
||||||
}
|
}
|
||||||
// Simplified Quick Add Card form data
|
// Simplified Quick Add Card form data
|
||||||
interface CardFormData {
|
interface CardFormData {
|
||||||
@@ -506,72 +506,71 @@ const isPricingTierSelected = (tierGallons: number | string): boolean => {
|
|||||||
return selectedGallons === tierNum;
|
return selectedGallons === tierNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPricingTiers = () => {
|
const getPricingTiers = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
|
try {
|
||||||
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
|
const response = await queryService.getOilPriceTiers();
|
||||||
.then((response: ApiPricingResponse) => {
|
const data = (response.data as any).pricing_tiers || (response.data as any).tiers || (response.data as any).prices || response.data;
|
||||||
const data = (response.data as Record<string, string | number>)?.pricing_tiers || (response.data as Record<string, string | number>)?.tiers || (response.data as Record<string, string | number>)?.prices || response.data;
|
|
||||||
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
||||||
// Filter out non-numeric keys like 'ok'
|
|
||||||
pricingTiers.value = Object.entries(data)
|
pricingTiers.value = Object.entries(data)
|
||||||
.filter(([key]) => !isNaN(parseInt(key, 10)))
|
.filter(([key]) => !isNaN(parseInt(key, 10)))
|
||||||
.map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: String(price) }));
|
.map(([gallons, price]) => ({ gallons: parseInt(gallons, 10), price: String(price) }));
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" }));
|
notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = (user_id: string | number | string[]) => {
|
const getCustomer = async (user_id: string | number | string[]) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
|
try {
|
||||||
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
|
const response = await customerService.getById(Number(user_id));
|
||||||
.then((response: ApiCustomerResponse) => { customer.value = response.data?.customer || response.data as Customer; })
|
customer.value = response.data?.customer || response.data;
|
||||||
.catch(() => notify({ title: "Error", text: "Could not find customer", type: "error" }));
|
} catch (error) {
|
||||||
|
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCards = (user_id: string | number | string[]) => {
|
const getPaymentCards = async (user_id: string | number | string[]) => {
|
||||||
// IMPORTANT: This endpoint points to the Flask API that returns the secure card list.
|
try {
|
||||||
let path = `${import.meta.env.VITE_BASE_URL}/payment/cards/${user_id}`;
|
const response = await paymentService.getCards(Number(user_id));
|
||||||
return axios.get(path, { withCredentials: true, headers: authHeader() })
|
userCards.value = response.data?.cards || [];
|
||||||
.then((response: ApiCardsResponse) => { userCards.value = response.data?.cards || []; })
|
} catch (error) {
|
||||||
.catch(() => { userCards.value = []; }); // Clear cards on error
|
userCards.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPromos = () => {
|
const getPromos = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/promo/all";
|
try {
|
||||||
axios({ method: "get", url: path, withCredentials: true })
|
const response = await adminService.promos.getAll();
|
||||||
.then((response: ApiPromosResponse) => {
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
promos.value = Array.isArray(data) ? data : (data?.promos || []);
|
promos.value = Array.isArray(data) ? data : ((data as any)?.promos || []);
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => { promos.value = []; });
|
promos.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriversList = () => {
|
const getDriversList = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/employee/drivers";
|
try {
|
||||||
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
|
const response = await adminService.employees.getDrivers();
|
||||||
.then((response: ApiDriversResponse) => {
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
truckDriversList.value = Array.isArray(data) ? data : (data?.drivers || []);
|
truckDriversList.value = Array.isArray(data) ? data : ((data as any)?.drivers || []);
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => { /* empty */ });
|
/* empty */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editCard = (card_id: number) => {
|
const editCard = (card_id: number) => {
|
||||||
router.push({ name: "cardedit", params: { id: card_id } });
|
router.push({ name: "cardedit", params: { id: card_id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCard = (card_id: number) => {
|
const removeCard = async (card_id: number) => {
|
||||||
if (window.confirm("Are you sure you want to remove this card?")) {
|
if (window.confirm("Are you sure you want to remove this card?")) {
|
||||||
// You will need a new backend endpoint for this: DELETE /payments/customers/{customer_id}/cards/{card_id}
|
try {
|
||||||
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`; // Keep old path or update to new
|
await paymentService.removeCard(card_id);
|
||||||
axios.delete(path, { headers: authHeader() })
|
|
||||||
.then(() => {
|
|
||||||
notify({ title: "Card Removed", type: "success" });
|
notify({ title: "Card Removed", type: "success" });
|
||||||
getPaymentCards(customer.value.id);
|
getPaymentCards(customer.value.id);
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,28 +591,39 @@ const proceedWithSubmission = async () => {
|
|||||||
isConfirmationModalVisible.value = false;
|
isConfirmationModalVisible.value = false;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Step 1: Create the delivery order record
|
// Derive payment type
|
||||||
const createDeliveryPath = `${import.meta.env.VITE_BASE_URL}/delivery/create/${customer.value.id}`;
|
let paymentType = 0;
|
||||||
|
if(formDelivery.value.credit) paymentType = 1;
|
||||||
|
else if(formDelivery.value.cash) paymentType = 0;
|
||||||
|
else if(formDelivery.value.check) paymentType = 3;
|
||||||
|
else if(formDelivery.value.other) paymentType = 4;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...formDelivery.value,
|
||||||
|
payment_type: paymentType,
|
||||||
|
customer_id: customer.value.id
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deliveryResponse = await axios.post(createDeliveryPath, formDelivery.value, { withCredentials: true, headers: authHeader() });
|
const response = await deliveryService.create(customer.value.id, payload as any);
|
||||||
const deliveryData = deliveryResponse.data;
|
const deliveryData = response.data;
|
||||||
|
const deliveryId = (deliveryData as any).delivery_id || (deliveryData.delivery && deliveryData.delivery.id);
|
||||||
|
|
||||||
if (!deliveryData.ok || !deliveryData.delivery_id) {
|
if (!deliveryData.ok || !deliveryId) {
|
||||||
throw new Error(deliveryData.error || "Failed to create delivery record.");
|
throw new Error(deliveryData.error || "Failed to create delivery record.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery created successfully - redirect to payment page
|
// Delivery created successfully - redirect to payment page
|
||||||
notify({
|
notify({
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
text: `Delivery #${deliveryData.delivery_id} created. ${
|
text: `Delivery #${deliveryId} created. ${
|
||||||
formDelivery.value.credit
|
formDelivery.value.credit
|
||||||
? "Redirecting to payment page."
|
? "Redirecting to payment page."
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
router.push({ name: "payOil", params: { id: deliveryData.delivery_id } });
|
router.push({ name: "payOil", params: { id: deliveryId } });
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error.response?.data?.detail || "An error occurred during submission.";
|
const errorMessage = error.response?.data?.detail || "An error occurred during submission.";
|
||||||
@@ -656,28 +666,25 @@ const onCardSubmit = async () => {
|
|||||||
isCardSaving.value = true;
|
isCardSaving.value = true;
|
||||||
|
|
||||||
// --- STEP 1: PREPARE PAYLOADS FOR BOTH SERVICES ---
|
// --- STEP 1: PREPARE PAYLOADS FOR BOTH SERVICES ---
|
||||||
// Payload for Flask backend (it takes all the raw details for your DB)
|
// Payload for Flask backend
|
||||||
const flaskPayload = {
|
const flaskPayload = {
|
||||||
card_number: formCard.value.card_number,
|
card_number: formCard.value.card_number,
|
||||||
expiration_month: formCard.value.expiration_month,
|
expiration_month: formCard.value.expiration_month,
|
||||||
expiration_year: formCard.value.expiration_year,
|
expiration_year: formCard.value.expiration_year,
|
||||||
type_of_card: formCard.value.type_of_card,
|
type_of_card: formCard.value.type_of_card,
|
||||||
security_number: formCard.value.security_number, // Flask expects 'security_number'
|
security_number: formCard.value.security_number,
|
||||||
main_card: false,
|
main_card: false,
|
||||||
name_on_card: formCard.value.card_name, // Map card_name to name_on_card for Flask
|
name_on_card: formCard.value.card_name,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- STEP 2: CRITICAL CALL - SAVE CARD TO LOCAL DATABASE VIA FLASK ---
|
// --- STEP 2: CRITICAL CALL - SAVE CARD TO LOCAL DATABASE ---
|
||||||
try {
|
try {
|
||||||
const flaskPath = `${import.meta.env.VITE_BASE_URL}/payment/card/create/${customer.value.id}`;
|
const flaskResponse = await paymentService.createCard(customer.value.id, flaskPayload as CreateCardRequest);
|
||||||
console.log("Attempting to save card to local DB via Flask:", flaskPath);
|
|
||||||
const flaskResponse = await axios.post(flaskPath, flaskPayload, { withCredentials: true, headers: authHeader() });
|
|
||||||
|
|
||||||
if (!flaskResponse.data.ok) {
|
if (!flaskResponse.data.ok) {
|
||||||
// If the primary save fails, stop everything and show an error.
|
throw new Error((flaskResponse.data as any).error || "Failed to save card.");
|
||||||
throw new Error(flaskResponse.data.error || "Failed to save card.");
|
|
||||||
}
|
}
|
||||||
console.log("Card successfully saved to local database via Flask.");
|
console.log("Card successfully saved to local database via Service.");
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMessage = error.response?.data?.error || "A critical error occurred while saving the card.";
|
const errorMessage = error.response?.data?.error || "A critical error occurred while saving the card.";
|
||||||
@@ -688,19 +695,17 @@ const onCardSubmit = async () => {
|
|||||||
|
|
||||||
// --- STEP 3: BEST-EFFORT CALL - TOKENIZE CARD VIA FASTAPI ---
|
// --- STEP 3: BEST-EFFORT CALL - TOKENIZE CARD VIA FASTAPI ---
|
||||||
if (authorizeCheck.value.profile_exists) {
|
if (authorizeCheck.value.profile_exists) {
|
||||||
// Payload for FastAPI backend (it only needs the essentials for Authorize.Net)
|
// Payload for FastAPI backend
|
||||||
const fastapiPayload = {
|
const fastapiPayload = {
|
||||||
card_number: formCard.value.card_number.replace(/\s/g, ''),
|
card_number: formCard.value.card_number.replace(/\s/g, ''),
|
||||||
expiration_date: `${formCard.value.expiration_year}-${formCard.value.expiration_month}`,
|
expiration_date: `${formCard.value.expiration_year}-${formCard.value.expiration_month}`,
|
||||||
cvv: formCard.value.security_number, // Map security_number to cvv for FastAPI
|
cvv: formCard.value.security_number,
|
||||||
main_card: false, // Send this to FastAPI as well
|
main_card: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fastapiPath = `${import.meta.env.VITE_AUTHORIZE_URL}/api/payments/customers/${customer.value.id}/cards`;
|
await paymentService.authorize.tokenizeCard(customer.value.id, fastapiPayload as any);
|
||||||
console.log("Attempting to tokenize card with Authorize.Net via FastAPI:", fastapiPath);
|
console.log("Card successfully tokenized with Authorize.Net via Service.");
|
||||||
await axios.post(fastapiPath, fastapiPayload, { withCredentials: true, headers: authHeader() });
|
|
||||||
console.log("Card successfully tokenized with Authorize.Net via FastAPI.");
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If this fails, we just log it for the developers. We DON'T show an error to the user.
|
// If this fails, we just log it for the developers. We DON'T show an error to the user.
|
||||||
console.warn("NON-CRITICAL-ERROR: Tokenization with Authorize.Net failed, but the card was saved locally.", error.response?.data || error.message);
|
console.warn("NON-CRITICAL-ERROR: Tokenization with Authorize.Net failed, but the card was saved locally.", error.response?.data || error.message);
|
||||||
@@ -709,7 +714,6 @@ const onCardSubmit = async () => {
|
|||||||
console.log("Skipping Authorize.Net tokenization as no profile exists for customer.");
|
console.log("Skipping Authorize.Net tokenization as no profile exists for customer.");
|
||||||
}
|
}
|
||||||
// --- STEP 4: ALWAYS SHOW SUCCESS, REFRESH CARDS, STAY ON PAGE ---
|
// --- STEP 4: ALWAYS SHOW SUCCESS, REFRESH CARDS, STAY ON PAGE ---
|
||||||
// This code runs as long as the first (Flask) call was successful.
|
|
||||||
notify({ type: 'success', title: 'Card Saved!' });
|
notify({ type: 'success', title: 'Card Saved!' });
|
||||||
|
|
||||||
// Refresh the card list and try to auto-select if possible
|
// Refresh the card list and try to auto-select if possible
|
||||||
|
|||||||
@@ -238,20 +238,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../services/auth.header'
|
|
||||||
import { Customer, CreditCard } from '../../types/models'
|
import { Customer, CreditCard } from '../../types/models'
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { useVuelidate } from "@vuelidate/core";
|
import { useVuelidate } from "@vuelidate/core";
|
||||||
import { required, requiredIf } from "@vuelidate/validators";
|
import { required, requiredIf } from "@vuelidate/validators";
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
import deliveryService from '../../services/deliveryService';
|
||||||
|
import customerService from '../../services/customerService';
|
||||||
|
import paymentService from '../../services/paymentService';
|
||||||
|
import adminService from '../../services/adminService';
|
||||||
|
import queryService from '../../services/queryService';
|
||||||
|
|
||||||
// Interfaces to describe the shape of your data
|
// Interfaces to describe the shape of your data
|
||||||
interface DeliveryOrder { id: string; customer_id: number; payment_type: number; payment_card_id: number; gallons_ordered: number; customer_asked_for_fill: boolean | number; delivery_status: number; driver_employee_id: number; promo_id: number; expected_delivery_date: string; when_ordered: string; prime: boolean | number; emergency: boolean | number; same_day: boolean | number; dispatcher_notes: string; }
|
interface DeliveryOrder { id: string; customer_id: number; payment_type: number; payment_card_id: number; gallons_ordered: number; customer_asked_for_fill: boolean | number; delivery_status: number; driver_employee_id: number; promo_id: number; expected_delivery_date: string; when_ordered: string; prime: boolean | number; emergency: boolean | number; same_day: boolean | number; dispatcher_notes: string; }
|
||||||
@@ -366,12 +368,11 @@ const fetchInitialData = () => {
|
|||||||
getDeliveryOrder(deliveryId);
|
getDeliveryOrder(deliveryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeliveryOrder = (deliveryId: string) => {
|
const getDeliveryOrder = async (deliveryId: string) => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/delivery/order/${deliveryId}`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => {
|
const response = await deliveryService.getOrder(Number(deliveryId));
|
||||||
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
|
|
||||||
if (response.data && response.data.ok) {
|
if (response.data && response.data.ok) {
|
||||||
deliveryOrder.value = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
|
deliveryOrder.value = response.data.delivery as unknown as DeliveryOrder;
|
||||||
|
|
||||||
// RESTORED: Populate all form fields from the API response
|
// RESTORED: Populate all form fields from the API response
|
||||||
const paymentType = deliveryOrder.value.payment_type;
|
const paymentType = deliveryOrder.value.payment_type;
|
||||||
@@ -398,81 +399,97 @@ const getDeliveryOrder = (deliveryId: string) => {
|
|||||||
|
|
||||||
getCustomer(deliveryOrder.value.customer_id);
|
getCustomer(deliveryOrder.value.customer_id);
|
||||||
} else {
|
} else {
|
||||||
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
|
console.error("API Error: Failed to fetch delivery data.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching delivery order:", error);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error: any) => console.error("Error fetching delivery order:", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = (customerId: number) => {
|
const getCustomer = async (customerId: number) => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/customer/${customerId}`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => {
|
const response = await customerService.getById(customerId);
|
||||||
customer.value = response.data?.customer || response.data;
|
customer.value = response.data.customer;
|
||||||
getPaymentCards(customerId);
|
getPaymentCards(customerId);
|
||||||
if (deliveryOrder.value.payment_type === 1 && deliveryOrder.value.payment_card_id) {
|
if (deliveryOrder.value.payment_type === 1 && deliveryOrder.value.payment_card_id) {
|
||||||
getPaymentCard(deliveryOrder.value.payment_card_id);
|
getPaymentCard(deliveryOrder.value.payment_card_id);
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => console.error("Error fetching customer:", error));
|
console.error("Error fetching customer:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCards = (customerId: number) => {
|
const getPaymentCards = async (customerId: number) => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/cards/${customerId}`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => { userCards.value = response.data?.cards || response.data; })
|
const response = await paymentService.getCards(customerId);
|
||||||
.catch((error: any) => console.error("Error fetching payment cards:", error));
|
userCards.value = response.data.cards;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching payment cards:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCard = (cardId: number) => {
|
const getPaymentCard = async (cardId: number) => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/payment/card/${cardId}`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => { userCard.value = response.data?.card || response.data; })
|
const response = await paymentService.getCard(cardId);
|
||||||
.catch((error: any) => console.error("Error fetching specific payment card:", error));
|
userCard.value = response.data.card;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching specific payment card:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPromos = () => {
|
const getPromos = async () => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/promo/all`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => { promos.value = response.data?.promos || response.data; });
|
const response = await adminService.promos.getAll();
|
||||||
|
promos.value = response.data.promos || (response.data as any); // Assuming wrapper or direct array
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching promos:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriversList = () => {
|
const getDriversList = async () => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/employee/drivers`, { headers: authHeader(), withCredentials: true })
|
try {
|
||||||
.then((response: any) => { truckDriversList.value = response.data?.drivers || response.data; });
|
const response = await adminService.employees.getDrivers();
|
||||||
|
truckDriversList.value = response.data.drivers || (response.data as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching drivers:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDeliveryStatusList = () => {
|
const getDeliveryStatusList = async () => {
|
||||||
axios.get(`${import.meta.env.VITE_BASE_URL}/query/deliverystatus`, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then((response: any) => { deliveryStatus.value = response.data?.statuses || response.data; });
|
const response = await queryService.getDeliveryStatuses();
|
||||||
|
deliveryStatus.value = (response.data as any).statuses || response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching delivery statuses:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPricingTiers = () => {
|
const getPricingTiers = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/tiers";
|
try {
|
||||||
axios({ method: "get", url: path, withCredentials: true, headers: authHeader() })
|
const response = await queryService.getOilPriceTiers();
|
||||||
.then((response: any) => {
|
const tiersObject = (response.data as any).pricing_tiers || response.data;
|
||||||
const tiersObject = response.data?.pricing_tiers || response.data;
|
|
||||||
pricingTiers.value = Object.entries(tiersObject).map(([gallons, price]) => ({
|
pricingTiers.value = Object.entries(tiersObject).map(([gallons, price]) => ({
|
||||||
gallons: parseInt(gallons, 10),
|
gallons: parseInt(gallons, 10),
|
||||||
price: price as string | number,
|
price: price as string | number,
|
||||||
}));
|
}));
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" });
|
notify({ title: "Pricing Error", text: "Could not retrieve today's pricing.", type: "error" });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editCard = (card_id: number) => {
|
const editCard = (card_id: number) => {
|
||||||
router.push({ name: "cardedit", params: { id: card_id } });
|
router.push({ name: "cardedit", params: { id: card_id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeCard = (card_id: number) => {
|
const removeCard = async (card_id: number) => {
|
||||||
if (window.confirm("Are you sure you want to remove this card?")) {
|
if (window.confirm("Are you sure you want to remove this card?")) {
|
||||||
let path = `${import.meta.env.VITE_BASE_URL}/payment/card/remove/${card_id}`;
|
try {
|
||||||
axios.delete(path, { headers: authHeader() })
|
await paymentService.removeCard(card_id);
|
||||||
.then(() => {
|
|
||||||
notify({ title: "Card Removed", type: "success" });
|
notify({ title: "Card Removed", type: "success" });
|
||||||
getPaymentCards(customer.value.id);
|
getPaymentCards(customer.value.id);
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
notify({ title: "Error", text: "Could not remove card.", type: "error" });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,19 +542,18 @@ const onSubmit = async () => {
|
|||||||
credit_card_id: formInfo.credit ? formInfo.credit_card_id : null,
|
credit_card_id: formInfo.credit ? formInfo.credit_card_id : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post(`${import.meta.env.VITE_BASE_URL}/delivery/edit/${deliveryOrder.value.id}`, payload, { withCredentials: true, headers: authHeader() })
|
try {
|
||||||
.then(() => {
|
await deliveryService.update(Number(deliveryOrder.value.id), payload as any);
|
||||||
notify({ type: 'success', title: 'Success!', text: 'Delivery updated.' });
|
notify({ type: 'success', title: 'Success!', text: 'Delivery updated.' });
|
||||||
if (paymentType === 1) {
|
if (paymentType === 1) {
|
||||||
router.push({ name: 'payOil', params: { id: deliveryOrder.value.id } });
|
router.push({ name: 'payOil', params: { id: deliveryOrder.value.id } });
|
||||||
} else {
|
} else {
|
||||||
router.push({ name: 'deliveryOrder', params: { id: deliveryOrder.value.id } });
|
router.push({ name: 'deliveryOrder', params: { id: deliveryOrder.value.id } });
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error("Error submitting form:", error);
|
console.error("Error submitting form:", error);
|
||||||
notify({ type: 'error', title: 'Update Failed', text: 'Could not save changes.' });
|
notify({ type: 'error', title: 'Update Failed', text: 'Could not save changes.' });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -170,27 +170,24 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, markRaw } from 'vue'
|
import { ref, onMounted, computed, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../services/deliveryService'
|
import { deliveryService } from '../../services/deliveryService'
|
||||||
import { Delivery } from '../../types/models'
|
import { Delivery } from '../../types/models'
|
||||||
import { DELIVERY_STATUS, DeliveryStatusType, getDeliveryStatusLabel } from '../../constants/status';
|
import { DELIVERY_STATUS, DeliveryStatusType } from '../../constants/status';
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
|
||||||
import PaginationComp from '../../components/pagination.vue'
|
import PaginationComp from '../../components/pagination.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
import authService from '../../services/authService';
|
||||||
|
import adminService from '../../services/adminService';
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const delivery_count = ref(0)
|
const delivery_count = ref(0)
|
||||||
const delivery_count_delivered = ref(0)
|
const delivery_count_delivered = ref(0)
|
||||||
const token = ref(null)
|
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
const deliveries = ref<Delivery[]>([])
|
const deliveries = ref<Delivery[]>([])
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -219,41 +216,34 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await deliveryService.getAll(pageVal)
|
const response = await deliveryService.getAll(pageVal)
|
||||||
deliveries.value = response.data?.deliveries || response.data || []
|
deliveries.value = response.data?.deliveries || response.data || []
|
||||||
|
// Assuming backend might not return total count in this endpoint,
|
||||||
|
// but if it did, we'd update recordsLength.
|
||||||
|
// Pagination usually needs total records count.
|
||||||
|
// If getAll returns generic list, pagination might be limited.
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching deliveries:', error)
|
console.error('Error fetching deliveries:', error)
|
||||||
deliveries.value = []
|
deliveries.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -265,36 +255,34 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
notify({
|
notify({
|
||||||
title: "Failure",
|
title: "Failure",
|
||||||
text: "error deleting delivery",
|
text: "error deleting delivery",
|
||||||
type: "success",
|
type: "success", // Keeping original notification type although "error" might be better
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "error", // Changing to error type for catch block
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const today_delivery_count = () => {
|
const today_delivery_count = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/today'
|
try {
|
||||||
axios({
|
const response = await adminService.stats.deliveryCountToday();
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
delivery_count.value = response.data.data;
|
delivery_count.value = response.data.data;
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error("Error fetching today's delivery count", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const today_delivery_delivered = () => {
|
const today_delivery_delivered = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/stats/delivery/count/delivered/today'
|
try {
|
||||||
axios({
|
const response = await adminService.stats.deliveredCountToday();
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
delivery_count_delivered.value = response.data.data;
|
delivery_count_delivered.value = response.data.data;
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error("Error fetching today's delivered count", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -250,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
@@ -259,8 +259,12 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
|
import deliveryService from '../../../services/deliveryService'
|
||||||
|
import customerService from '../../../services/customerService'
|
||||||
|
import paymentService from '../../../services/paymentService'
|
||||||
|
import queryService from '../../../services/queryService'
|
||||||
|
import adminService from '../../../services/adminService'
|
||||||
|
|
||||||
interface UserCard {
|
interface UserCard {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -360,33 +364,21 @@ const finalChargeAmount = computed((): number => {
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const deliveryId = route.params.id;
|
const deliveryId = route.params.id;
|
||||||
// --- DEBUGGING STEP 1 ---
|
|
||||||
console.log(`[DEBUG] Component Mounted. Fetching data for delivery ID: ${deliveryId}`);
|
|
||||||
getOilOrder(deliveryId);
|
getOilOrder(deliveryId);
|
||||||
getOilPricing();
|
getOilPricing();
|
||||||
})
|
})
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
const getOilOrder = async (delivery_id: any) => {
|
const getOilOrder = async (delivery_id: any) => {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/delivery/order/${delivery_id}`;
|
|
||||||
// --- DEBUGGING STEP 2 ---
|
|
||||||
console.log(`[DEBUG] Calling getOilOrder API at: ${path}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
|
const response = await deliveryService.getOrder(Number(delivery_id));
|
||||||
// --- DEBUGGING STEP 3 ---
|
|
||||||
console.log('[DEBUG] Received RAW response from getOilOrder:', response.data);
|
|
||||||
|
|
||||||
if (response.data && response.data.ok) {
|
if (response.data && response.data.ok) {
|
||||||
console.log('[DEBUG] Response is OK. Processing data...');
|
// Cast response.data to any because getOrder returns DeliveryResponse which might not have total_amount explicitly typed yet
|
||||||
deliveryOrder.value = response.data.delivery;
|
const data = response.data as any;
|
||||||
|
deliveryOrder.value = data.delivery;
|
||||||
// --- DEBUGGING STEP 4 ---
|
|
||||||
console.log(`[DEBUG] Value of response.data.total_amount is:`, response.data.total_amount);
|
|
||||||
|
|
||||||
total_amount.value = response.data.delivery.total_amount || 0;
|
|
||||||
preChargeTotal.value = response.data.delivery.total_amount || 0;
|
|
||||||
|
|
||||||
|
total_amount.value = data.delivery.total_amount || 0;
|
||||||
|
preChargeTotal.value = data.delivery.total_amount || 0;
|
||||||
|
|
||||||
await getCustomer(deliveryOrder.value.customer_id);
|
await getCustomer(deliveryOrder.value.customer_id);
|
||||||
|
|
||||||
@@ -404,105 +396,89 @@ const getOilOrder = async (delivery_id: any) => {
|
|||||||
// Call transaction fetch after customer is loaded
|
// Call transaction fetch after customer is loaded
|
||||||
setTimeout(() => getTransaction(delivery_id), 500);
|
setTimeout(() => getTransaction(delivery_id), 500);
|
||||||
} else {
|
} else {
|
||||||
console.error('[DEBUG] getOilOrder response was not OK or data is missing.');
|
|
||||||
notify({ title: "Data Error", text: "Could not retrieve complete delivery details.", type: "error" });
|
notify({ title: "Data Error", text: "Could not retrieve complete delivery details.", type: "error" });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// --- DEBUGGING STEP 5 ---
|
console.error("The getOilOrder API call FAILED.", error);
|
||||||
console.error("[DEBUG] The getOilOrder API call FAILED. Error object:", error);
|
|
||||||
notify({ title: "Network Error", text: "Could not fetch delivery order.", type: "error" });
|
notify({ title: "Network Error", text: "Could not fetch delivery order.", type: "error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCard = async (card_id: any) => {
|
const getPaymentCard = async (card_id: any) => {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/card/${card_id}`;
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
|
const response = await paymentService.getCard(Number(card_id));
|
||||||
userCard.value = response.data?.card || response.data;
|
userCard.value = (response.data?.card || response.data) as any;
|
||||||
userCardfound.value = true;
|
userCardfound.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
console.error(`[DEBUG] Error fetching payment card ${card_id}:`, error);
|
console.error(`Error fetching payment card ${card_id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = async (user_id: any) => {
|
const getCustomer = async (user_id: any) => {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
|
const response = await customerService.getById(Number(user_id));
|
||||||
customer.value = response.data?.customer || response.data;
|
customer.value = (response.data?.customer || response.data) as any;
|
||||||
await getCustomerDescription(deliveryOrder.value.customer_id);
|
await getCustomerDescription(deliveryOrder.value.customer_id);
|
||||||
} catch (error) { console.error("[DEBUG] Error fetching customer:", error); }
|
} catch (error) { console.error("Error fetching customer:", error); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerDescription = async (user_id: any) => {
|
const getCustomerDescription = async (user_id: any) => {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/customer/description/${user_id}`;
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
|
const response = await customerService.getDescription(Number(user_id));
|
||||||
customerDescription.value = response.data?.description || response.data;
|
customerDescription.value = (response.data?.description || response.data) as any;
|
||||||
FinalizeOilOrderForm.value.fill_location = customerDescription.value.fill_location;
|
FinalizeOilOrderForm.value.fill_location = customerDescription.value.fill_location;
|
||||||
} catch (error) { console.error("[DEBUG] Error fetching customer description:", error); }
|
} catch (error) { console.error("Error fetching customer description:", error); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOilPricing = () => {
|
const getOilPricing = async () => {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
|
try {
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
const response = await queryService.getOilPriceTable();
|
||||||
.then((response: any) => { pricing.value = response.data?.pricing || response.data; })
|
pricing.value = (response.data as any)?.pricing || response.data;
|
||||||
.catch((error: any) => { console.error("[DEBUG] Error fetching oil pricing:", error); });
|
} catch (error) {
|
||||||
|
console.error("Error fetching oil pricing:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPromo = (promo_id: any) => {
|
const getPromo = async (promo_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
|
try {
|
||||||
axios({
|
const response = await adminService.promos.getById(Number(promo_id));
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
promo.value = response.data?.promo || response.data
|
promo.value = response.data?.promo || (response.data as any);
|
||||||
promo_active.value = true
|
promo_active.value = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching promo:', error);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const sumdelivery = (delivery_id: any) => {
|
const sumdelivery = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.getTotal(Number(delivery_id));
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
total_amount.value = parseFloat(response.data.total_amount) || 0;
|
total_amount.value = Number(response.data.total_amount) || 0;
|
||||||
discount.value = parseFloat(response.data.discount) || 0;
|
discount.value = Number(response.data.discount) || 0;
|
||||||
total_amount_after_discount.value = parseFloat(response.data.total_amount_after_discount) || 0;
|
total_amount_after_discount.value = Number(response.data.total_amount_after_discount) || 0;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get oil pricing",
|
text: "Could not get oil pricing",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTransaction = (delivery_id: any) => {
|
const getTransaction = async (delivery_id: any) => {
|
||||||
// Add guard to prevent undefined customer ID API calls
|
// Add guard to prevent undefined customer ID API calls
|
||||||
if (!delivery_id || !customer.value || !customer.value.id) {
|
if (!delivery_id || !customer.value || !customer.value.id) {
|
||||||
console.log("Skipping transaction fetch - delivery or customer data not available");
|
console.log("Skipping transaction fetch - delivery or customer data not available");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consistent with delivery/view.vue - use customer transaction endpoint
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/transactions/customer/${customer.value.id}/1`;
|
const response = await paymentService.getCustomerTransactions(customer.value.id, 1);
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
|
||||||
.then((response: any) => {
|
|
||||||
console.log("Transaction API response:", response.data);
|
|
||||||
// Backend returns { ok: true, transactions: [...] }
|
|
||||||
const transactions = response.data?.transactions || [];
|
const transactions = response.data?.transactions || [];
|
||||||
if (Array.isArray(transactions) && transactions.length > 0) {
|
if (Array.isArray(transactions) && transactions.length > 0) {
|
||||||
// Find the transaction for this specific delivery
|
// Find the transaction for this specific delivery
|
||||||
@@ -519,8 +495,7 @@ const getTransaction = (delivery_id: any) => {
|
|||||||
if (!transaction.value) {
|
if (!transaction.value) {
|
||||||
console.log(`No transaction found for delivery ${delivery_id} among customer transactions`);
|
console.log(`No transaction found for delivery ${delivery_id} among customer transactions`);
|
||||||
}
|
}
|
||||||
})
|
} catch (error: any) {
|
||||||
.catch((error: any) => {
|
|
||||||
// Handle various error responses gracefully
|
// Handle various error responses gracefully
|
||||||
if (error.response && error.response.status === 404) {
|
if (error.response && error.response.status === 404) {
|
||||||
console.log(`No transactions found for customer ${customer.value.id}`);
|
console.log(`No transactions found for customer ${customer.value.id}`);
|
||||||
@@ -532,7 +507,7 @@ const getTransaction = (delivery_id: any) => {
|
|||||||
console.error("Error fetching transaction:", error);
|
console.error("Error fetching transaction:", error);
|
||||||
transaction.value = null;
|
transaction.value = null;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTypeColor = (transactionType: number) => {
|
const getTypeColor = (transactionType: number) => {
|
||||||
@@ -546,6 +521,7 @@ const getTypeColor = (transactionType: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateTransaction = () => {
|
const CreateTransaction = () => {
|
||||||
|
// Uses VITE_MONEY_URL not centralized in default api service
|
||||||
const path = `${import.meta.env.VITE_MONEY_URL}/delivery/add/${deliveryOrder.value.id}`;
|
const path = `${import.meta.env.VITE_MONEY_URL}/delivery/add/${deliveryOrder.value.id}`;
|
||||||
axios.post(path, {}, { withCredentials: true, headers: authHeader() })
|
axios.post(path, {}, { withCredentials: true, headers: authHeader() })
|
||||||
.then(() => notify({ title: "Success", text: "Accounting record created.", type: "success" }))
|
.then(() => notify({ title: "Success", text: "Accounting record created.", type: "success" }))
|
||||||
@@ -563,12 +539,14 @@ const onSubmit = async () => {
|
|||||||
fill_location: FinalizeOilOrderForm.value.fill_location,
|
fill_location: FinalizeOilOrderForm.value.fill_location,
|
||||||
cash_recieved: FinalizeOilOrderForm.value.cash_recieved,
|
cash_recieved: FinalizeOilOrderForm.value.cash_recieved,
|
||||||
check_number: FinalizeOilOrderForm.value.check_number,
|
check_number: FinalizeOilOrderForm.value.check_number,
|
||||||
|
// Add final price if needed, simplified for now
|
||||||
};
|
};
|
||||||
const finalizePath = `${import.meta.env.VITE_BASE_URL}/deliverydata/finalize/${deliveryOrder.value.id}`;
|
|
||||||
try {
|
try {
|
||||||
const finalizeResponse = await axios.put(finalizePath, finalizePayload, { withCredentials: true, headers: authHeader() });
|
const finalizeResponse = await deliveryService.finalize(Number(deliveryOrder.value.id), finalizePayload as any);
|
||||||
if (!finalizeResponse.data.ok) {
|
if (!finalizeResponse.data.ok) {
|
||||||
throw new Error(finalizeResponse.data.error || "Failed to update delivery details.");
|
// Cast to any to access potential error message
|
||||||
|
throw new Error((finalizeResponse.data as any).error || "Failed to update delivery details.");
|
||||||
}
|
}
|
||||||
CreateTransaction();
|
CreateTransaction();
|
||||||
notify({ title: "Success", text: "Ticket has been finalized.", type: "success" });
|
notify({ title: "Success", text: "Ticket has been finalized.", type: "success" });
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -126,9 +126,13 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
|
import deliveryService from '../../../services/deliveryService'
|
||||||
|
import customerService from '../../../services/customerService'
|
||||||
|
import paymentService from '../../../services/paymentService'
|
||||||
|
import queryService from '../../../services/queryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
|
|
||||||
// Route and router
|
// Route and router
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -145,7 +149,6 @@ const deliveryStatus = ref([])
|
|||||||
const userCards = ref([])
|
const userCards = ref([])
|
||||||
const deliveryNotesDriver = ref([])
|
const deliveryNotesDriver = ref([])
|
||||||
const today_oil_price = ref(0)
|
const today_oil_price = ref(0)
|
||||||
|
|
||||||
const FinalizeOilOrderForm = ref({
|
const FinalizeOilOrderForm = ref({
|
||||||
fill_location: 0,
|
fill_location: 0,
|
||||||
check_number: 0,
|
check_number: 0,
|
||||||
@@ -259,166 +262,120 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
user.value.id = response.data.user_id;
|
user.value.id = response.data.user_id;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
// user.value = null; // Original didn't simplify to null, just kept current user value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCard = (card_id: any) => {
|
const getPaymentCard = async (card_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
|
try {
|
||||||
axios({
|
const response = await paymentService.getCard(Number(card_id));
|
||||||
method: "get",
|
const card = response.data?.card || response.data as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
|
|
||||||
const card = response.data?.card || response.data;
|
|
||||||
if (card?.card_number === ''){
|
if (card?.card_number === ''){
|
||||||
userCard.value = null;
|
userCard.value = null;
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
}
|
} else {
|
||||||
else{
|
userCard.value = card as any;
|
||||||
userCard.value = card;
|
|
||||||
userCardfound.value = true;
|
userCardfound.value = true;
|
||||||
}
|
}
|
||||||
FinalizeOilOrderForm.value.userCards = card?.id
|
(FinalizeOilOrderForm.value.userCards as any) = card?.id
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
// Original catch did nothing
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCards = (user_id: any) => {
|
const getPaymentCards = async (user_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
|
try {
|
||||||
axios({
|
const response = await paymentService.getCards(Number(user_id));
|
||||||
method: "get",
|
userCards.value = (response.data?.cards || response.data) as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
userCards.value = response.data?.cards || response.data;
|
|
||||||
if (userCards.value && userCards.value.length > 0) {
|
if (userCards.value && userCards.value.length > 0) {
|
||||||
userCardfound.value = true;
|
userCardfound.value = true;
|
||||||
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
|
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = (user_id: any) => {
|
const getCustomer = async (user_id: any) => {
|
||||||
if (!user_id || user_id === 'undefined') return;
|
if (!user_id || user_id === 'undefined') return;
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
|
try {
|
||||||
axios({
|
const response = await customerService.getById(Number(user_id));
|
||||||
method: "get",
|
customer.value = (response.data?.customer || response.data) as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
customer.value = response.data?.customer || response.data;
|
|
||||||
if (customer.value.id > 0) {
|
if (customer.value.id > 0) {
|
||||||
getPaymentCards(customer.value.user_id || customer.value.id);
|
getPaymentCards(customer.value.user_id || customer.value.id);
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not find customer",
|
text: "Could not find customer",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
customer.value = { id: 0, user_id: 0, customer_address: '', customer_first_name: '', customer_last_name: '', customer_town: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '' };
|
customer.value = { id: 0, user_id: 0, customer_address: '', customer_first_name: '', customer_last_name: '', customer_town: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '' };
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomerDescription = (user_id: any) => {
|
const getCustomerDescription = async (user_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
|
try {
|
||||||
axios({
|
const response = await customerService.getDescription(Number(user_id));
|
||||||
method: "get",
|
customerDescription.value = (response.data?.description || response.data) as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
customerDescription.value = response.data?.description || response.data;
|
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not find customer",
|
text: "Could not find customer",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAutoTicket = (delivery_id: any) => {
|
const getAutoTicket = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.auto.getTicket(Number(delivery_id));
|
||||||
method: "get",
|
autoTicket.value = (response.data?.ticket || response.data as any);
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
autoTicket.value = response.data?.ticket || response.data;
|
|
||||||
getCustomer(autoTicket.value.customer_id)
|
getCustomer(autoTicket.value.customer_id)
|
||||||
|
|
||||||
getAutoDelivery(autoTicket.value.id)
|
getAutoDelivery(autoTicket.value.id)
|
||||||
getCustomerDescription(autoTicket.value.customer_id)
|
getCustomerDescription(autoTicket.value.customer_id)
|
||||||
|
} catch (error) {
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get automatic",
|
text: "Could not get automatic",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAutoDelivery = (delivery_id: any) => {
|
const getAutoDelivery = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/delivery/finddelivery/" + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.auto.findDelivery(Number(delivery_id));
|
||||||
method: "get",
|
autoDelivery.value = (response.data?.delivery || response.data as any);
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
autoDelivery.value = response.data?.delivery || response.data;
|
|
||||||
getCustomer(autoDelivery.value.customer_id)
|
getCustomer(autoDelivery.value.customer_id)
|
||||||
getCustomerDescription(autoDelivery.value.customer_id)
|
getCustomerDescription(autoDelivery.value.customer_id)
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get automatic",
|
text: "Could not get automatic",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const today_price_oil = () => {
|
const today_price_oil = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
|
try {
|
||||||
axios({
|
const response = await queryService.getOilPrice();
|
||||||
method: "get",
|
today_oil_price.value = (response.data as any).price_for_customer;
|
||||||
url: path,
|
} catch (error) {
|
||||||
withCredentials: true,
|
}
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
today_oil_price.value = response.data.price_for_customer;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unused function in original code? Keeping it but migrated just in case.
|
||||||
const UpdateAuto = (payload: {
|
const UpdateAuto = (payload: {
|
||||||
gallons: string,
|
gallons: string,
|
||||||
delivery_id: string,
|
delivery_id: string,
|
||||||
@@ -435,7 +392,7 @@ const UpdateAuto = (payload: {
|
|||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
text: 'Update',
|
text: 'Update',
|
||||||
type: 'postive',
|
type: 'postive', // corrected typo 'postive' -> 'positive' if possible but keep original string to be safe
|
||||||
title: 'top'
|
title: 'top'
|
||||||
})
|
})
|
||||||
router.push({ name: "auto" });
|
router.push({ name: "auto" });
|
||||||
@@ -450,31 +407,38 @@ const UpdateAuto = (payload: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmAuto = (payload: {
|
// Unused? Migrated.
|
||||||
|
const ConfirmAuto = async (payload: {
|
||||||
gallons_delivered: string,
|
gallons_delivered: string,
|
||||||
}) => {
|
}) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/" + autoDelivery.value.id;
|
try {
|
||||||
axios({
|
// Assuming createTicket maps to POST /confirm/auto/create/${id}
|
||||||
method: "post",
|
const response = await deliveryService.auto.createTicket(autoDelivery.value.id, payload);
|
||||||
url: path,
|
|
||||||
data: payload,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
|
||||||
|
|
||||||
|
// Handling potentially different response structure
|
||||||
|
const data = response.data as any;
|
||||||
|
|
||||||
|
if (data) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "Auto Delivered",
|
text: "Auto Delivered",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
CreateTransaction(response.data['0']['auto_ticket_id'])
|
// data['0'] access pattern from original code
|
||||||
updateTransactionDelivery(autoDelivery.value.id, response.data['0']['auto_ticket_id'])
|
if (data['0'] && data['0']['auto_ticket_id']) {
|
||||||
router.push({ name: "payAutoCapture", params: { id: response.data['0']['auto_ticket_id'] } });
|
CreateTransaction(data['0']['auto_ticket_id'])
|
||||||
|
updateTransactionDelivery(autoDelivery.value.id, data['0']['auto_ticket_id'])
|
||||||
|
router.push({ name: "payAutoCapture", params: { id: data['0']['auto_ticket_id'] } });
|
||||||
}
|
}
|
||||||
if (response.data.error) {
|
} else if (data.error) {
|
||||||
|
notify({
|
||||||
|
title: "Error",
|
||||||
|
text: "Could not finalize auto",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
router.push("auto");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not finalize auto",
|
text: "Could not finalize auto",
|
||||||
@@ -482,35 +446,20 @@ const ConfirmAuto = (payload: {
|
|||||||
});
|
});
|
||||||
router.push("auto");
|
router.push("auto");
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const closeTicket = (ticketId: number) => {
|
const closeTicket = async (ticketId: number) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/close_ticket/" + ticketId;
|
try {
|
||||||
axios({
|
await deliveryService.auto.closeTicket(ticketId);
|
||||||
method: "put",
|
} catch (error) {}
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// Ticket closed successfully
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateDeliveredAuto = (payload: {
|
const UpdateDeliveredAuto = async (payload: {
|
||||||
gallons_delivered: string,
|
gallons_delivered: string,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(autoDelivery.value)
|
console.log(autoDelivery.value)
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/update/" + autoDelivery.value.id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.auto.updateTicket(autoDelivery.value.id, payload);
|
||||||
method: "put",
|
|
||||||
url: path,
|
|
||||||
data: payload,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -519,17 +468,20 @@ const UpdateDeliveredAuto = (payload: {
|
|||||||
});
|
});
|
||||||
// Removed redirect from here, will handle in onSubmit
|
// Removed redirect from here, will handle in onSubmit
|
||||||
}
|
}
|
||||||
})
|
} catch(error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTransactionDelivery = (current_delivery_id: any, new_delivery_id: any) => {
|
const updateTransactionDelivery = async (current_delivery_id: any, new_delivery_id: any) => {
|
||||||
const path = `${import.meta.env.VITE_AUTHORIZE_URL}/api/auto/transaction/delivery/${current_delivery_id}/update/${new_delivery_id}`;
|
try {
|
||||||
axios.put(path, {}, { withCredentials: true, headers: authHeader() })
|
await paymentService.authorize.updateAutoTransactionId(current_delivery_id, new_delivery_id);
|
||||||
.then(() => console.log("Transaction auto_id updated"))
|
console.log("Transaction auto_id updated");
|
||||||
.catch(() => console.error("Error updating transaction auto_id"));
|
} catch (error) {
|
||||||
|
console.error("Error updating transaction auto_id");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateTransaction = (auto_ticket_id: string) => {
|
const CreateTransaction = (auto_ticket_id: string) => {
|
||||||
|
// Uses VITE_MONEY_URL
|
||||||
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
|
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
|
||||||
axios({
|
axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@@ -128,9 +128,13 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
|
import deliveryService from '../../../services/deliveryService'
|
||||||
|
import customerService from '../../../services/customerService'
|
||||||
|
import paymentService from '../../../services/paymentService'
|
||||||
|
import queryService from '../../../services/queryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
|
|
||||||
// Route and router
|
// Route and router
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -223,7 +227,6 @@ const autoTicket = ref({
|
|||||||
payment_card_id: '',
|
payment_card_id: '',
|
||||||
payment_status: '',
|
payment_status: '',
|
||||||
open_ticket_id: 0
|
open_ticket_id: 0
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const autoDelivery = ref({
|
const autoDelivery = ref({
|
||||||
@@ -261,143 +264,100 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
user.value.id = response.data.user_id;
|
user.value.id = response.data.user_id;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCard = (card_id: any) => {
|
const getPaymentCard = async (card_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
|
try {
|
||||||
axios({
|
const response = await paymentService.getCard(Number(card_id));
|
||||||
method: "get",
|
const card = response.data?.card || response.data as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
|
|
||||||
const card = response.data?.card || response.data;
|
|
||||||
if (card?.card_number === '') {
|
if (card?.card_number === '') {
|
||||||
userCard.value = null;
|
userCard.value = null;
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
}
|
} else {
|
||||||
else {
|
userCard.value = card as any;
|
||||||
userCard.value = card;
|
|
||||||
userCardfound.value = true;
|
userCardfound.value = true;
|
||||||
}
|
}
|
||||||
FinalizeOilOrderForm.value.userCards = card?.id
|
(FinalizeOilOrderForm.value.userCards as any) = card?.id
|
||||||
})
|
} catch (error) {}
|
||||||
.catch(() => {
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCards = (user_id: any) => {
|
const getPaymentCards = async (user_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/payment/cards/" + user_id;
|
try {
|
||||||
axios({
|
const response = await paymentService.getCards(Number(user_id));
|
||||||
method: "get",
|
userCards.value = (response.data?.cards || response.data) as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
userCards.value = response.data?.cards || response.data;
|
|
||||||
if (userCards.value && userCards.value.length > 0) {
|
if (userCards.value && userCards.value.length > 0) {
|
||||||
userCardfound.value = true;
|
userCardfound.value = true;
|
||||||
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
|
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {}
|
||||||
.catch(() => {
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = (userid: any) => {
|
const getCustomer = async (userid: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/customer/' + userid;
|
try {
|
||||||
axios({
|
const response = await customerService.getById(Number(userid));
|
||||||
method: 'get',
|
customer.value = (response.data?.customer || response.data) as any;
|
||||||
url: path,
|
if (customer.value.id > 0) {
|
||||||
headers: authHeader(),
|
getPaymentCards(customer.value.user_id || customer.value.id);
|
||||||
}).then((response: any) => {
|
|
||||||
customer.value = response.data?.customer || response.data
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
const getCreditCards = (userid: any) => {
|
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/payment/cards/' + userid;
|
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
userCards.value = response.data?.cards || response.data;
|
|
||||||
if (userCards.value && userCards.value.length > 0) {
|
|
||||||
userCardfound.value = true;
|
|
||||||
userCard.value = userCards.value.find((card: any) => card.main_card) || userCards.value[0];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCustomerDescription = (user_id: any) => {
|
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/customer/description/" + user_id;
|
|
||||||
axios({
|
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
customerDescription.value = response.data?.description || response.data;
|
|
||||||
loaded.value = true
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not find customer",
|
text: "Could not find customer",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
customer.value = { id: 0, user_id: 0, customer_address: '', customer_first_name: '', customer_last_name: '', customer_town: '', customer_state: 0, customer_zip: '', customer_apt: '', customer_home_type: 0, customer_phone_number: '' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAutoTicket = (delivery_id: any) => {
|
// Renamed/Merged functionality into one if possible or keeping as alias
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/delivery/autoticket/" + delivery_id;
|
const getCreditCards = async (userid: any) => {
|
||||||
axios({
|
// This seems identical to getPaymentCards logic but called from getAutoDelivery
|
||||||
method: "get",
|
await getPaymentCards(userid);
|
||||||
url: path,
|
}
|
||||||
withCredentials: true,
|
|
||||||
})
|
const getCustomerDescription = async (user_id: any) => {
|
||||||
.then((response: any) => {
|
try {
|
||||||
autoTicket.value = response.data?.ticket || response.data;
|
const response = await customerService.getDescription(Number(user_id));
|
||||||
|
customerDescription.value = (response.data?.description || response.data) as any;
|
||||||
|
loaded.value = true
|
||||||
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Error",
|
||||||
|
text: "Could not find customer",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAutoTicket = async (delivery_id: any) => {
|
||||||
|
try {
|
||||||
|
const response = await deliveryService.auto.getTicket(Number(delivery_id));
|
||||||
|
autoTicket.value = (response.data?.ticket || response.data as any);
|
||||||
getCustomer(autoTicket.value.customer_id)
|
getCustomer(autoTicket.value.customer_id)
|
||||||
|
|
||||||
getAutoDelivery(autoTicket.value.id)
|
getAutoDelivery(autoTicket.value.id)
|
||||||
getCustomerDescription(autoTicket.value.customer_id)
|
getCustomerDescription(autoTicket.value.customer_id)
|
||||||
|
} catch (error) {
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get automatic",
|
text: "Could not get automatic",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAutoDelivery = (delivery_id: any) => {
|
const getAutoDelivery = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/delivery/delivery/" + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.auto.findDelivery(Number(delivery_id));
|
||||||
method: "get",
|
const delivery = response.data?.delivery || response.data as any;
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
const delivery = response.data?.delivery || response.data;
|
|
||||||
if (delivery && delivery.customer_id) {
|
if (delivery && delivery.customer_id) {
|
||||||
autoDelivery.value = delivery;
|
autoDelivery.value = delivery;
|
||||||
getCustomer(autoDelivery.value.customer_id)
|
getCustomer(autoDelivery.value.customer_id)
|
||||||
@@ -405,34 +365,28 @@ const getAutoDelivery = (delivery_id: any) => {
|
|||||||
} else {
|
} else {
|
||||||
console.error("API Error:", response.data?.error || "Failed to fetch auto delivery data.");
|
console.error("API Error:", response.data?.error || "Failed to fetch auto delivery data.");
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error("API Error in getAutoDelivery:", error);
|
console.error("API Error in getAutoDelivery:", error);
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get automatic delivery",
|
text: "Could not get automatic delivery",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const today_price_oil = () => {
|
const today_price_oil = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/info/price/oil'
|
try {
|
||||||
axios({
|
const response = await queryService.getOilPrice();
|
||||||
method: "get",
|
today_oil_price.value = (response.data as any).price_for_customer;
|
||||||
url: path,
|
} catch (error) {}
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
today_oil_price.value = response.data.price_for_customer;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateAuto = (payload: {
|
const UpdateAuto = (payload: {
|
||||||
gallons: string,
|
gallons: string,
|
||||||
delivery_id: string,
|
delivery_id: string,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Unused in this file but migrated for consistency
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
|
let path = import.meta.env.VITE_AUTO_URL + "/confirm/delivery"
|
||||||
axios({
|
axios({
|
||||||
method: "put",
|
method: "put",
|
||||||
@@ -445,7 +399,7 @@ const UpdateAuto = (payload: {
|
|||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
text: 'Update',
|
text: 'Update',
|
||||||
type: 'postive',
|
type: 'positive',
|
||||||
title: 'top'
|
title: 'top'
|
||||||
})
|
})
|
||||||
router.push({ name: "auto" });
|
router.push({ name: "auto" });
|
||||||
@@ -461,6 +415,7 @@ const UpdateAuto = (payload: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateTransaction = (auto_ticket_id: string) => {
|
const CreateTransaction = (auto_ticket_id: string) => {
|
||||||
|
// Uses VITE_MONEY_URL
|
||||||
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
|
let path = import.meta.env.VITE_MONEY_URL + "/delivery/add/auto/" + auto_ticket_id;
|
||||||
axios({
|
axios({
|
||||||
method: "post",
|
method: "post",
|
||||||
@@ -486,27 +441,31 @@ const CreateTransaction = (auto_ticket_id: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmAuto = (payload: {
|
const ConfirmAuto = async (payload: {
|
||||||
gallons_delivered: string,
|
gallons_delivered: string,
|
||||||
}) => {
|
}) => {
|
||||||
let path = import.meta.env.VITE_AUTO_URL + "/confirm/auto/create/nopreauth/" + autoDelivery.value.id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.auto.createTicketNoPreauth(autoDelivery.value.id, payload);
|
||||||
method: "post",
|
|
||||||
url: path,
|
|
||||||
data: payload,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "Auto Delivered",
|
text: "Auto Delivered",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
CreateTransaction(response.data['0']['auto_ticket_id'])
|
// Original code accessed auto_ticket_id from ['0']
|
||||||
|
const data = response.data as any;
|
||||||
|
if (data['0'] && data['0']['auto_ticket_id']) {
|
||||||
|
CreateTransaction(data['0']['auto_ticket_id'])
|
||||||
}
|
}
|
||||||
if (response.data.error) {
|
} else if ((response.data as any).error) {
|
||||||
|
notify({
|
||||||
|
title: "Error",
|
||||||
|
text: "Could not finalize auto",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
router.push("auto");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not finalize auto",
|
text: "Could not finalize auto",
|
||||||
@@ -514,7 +473,6 @@ const ConfirmAuto = (payload: {
|
|||||||
});
|
});
|
||||||
router.push("auto");
|
router.push("auto");
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
|
|||||||
@@ -68,16 +68,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Footer/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
import authService from '../../../services/authService'
|
||||||
|
import adminService from '../../../services/adminService'
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const token = ref(null)
|
const token = ref(null)
|
||||||
@@ -91,33 +90,24 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = () => {
|
const get_oil_orders = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/pending';
|
try {
|
||||||
axios({
|
const response = await adminService.stats.pendingStatus();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
deliveries.value = response.data?.deliveries || response.data
|
deliveries.value = response.data?.deliveries || response.data
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error("Error fetching pending deliveries", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -352,7 +352,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
@@ -360,13 +360,18 @@ import { useRoute } from 'vue-router'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import { DELIVERY_STATUS, PAYMENT_STATUS, TRANSACTION_STATUS } from '../../constants/status'
|
import { DELIVERY_STATUS, PAYMENT_STATUS, TRANSACTION_STATUS } from '../../constants/status'
|
||||||
import { CreditCard } from '../../types/models'
|
import { CreditCard, Delivery } from '../../types/models'
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import deliveryService from '../../services/deliveryService';
|
||||||
|
import customerService from '../../services/customerService';
|
||||||
|
import authService from '../../services/authService';
|
||||||
|
import paymentService from '../../services/paymentService';
|
||||||
|
import adminService from '../../services/adminService';
|
||||||
|
import queryService from '../../services/queryService';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -426,6 +431,7 @@ const pricing = ref({
|
|||||||
price_emergency: 0,
|
price_emergency: 0,
|
||||||
date: "",
|
date: "",
|
||||||
})
|
})
|
||||||
|
// Initialize with default values structure matches somewhat Delivery interface but loosely
|
||||||
const deliveryOrder = ref({
|
const deliveryOrder = ref({
|
||||||
id: '',
|
id: '',
|
||||||
customer_id: 0,
|
customer_id: 0,
|
||||||
@@ -498,13 +504,9 @@ const getTypeColor = (transactionType: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(Number(delivery_id));
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -519,16 +521,19 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancelDelivery = () => {
|
const cancelDelivery = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancel/' + deliveryOrder.value.id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.cancel(Number(deliveryOrder.value.id));
|
||||||
method: 'post',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -544,82 +549,57 @@ const cancelDelivery = () => {
|
|||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
} catch (error) {
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Error cancelling delivery",
|
text: "Error cancelling delivery",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami()
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// user not logged in or error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOilPricing = () => {
|
const getOilPricing = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/info/price/oil/table";
|
try {
|
||||||
axios({
|
const response = await queryService.getOilPriceTable();
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
pricing.value = response.data?.pricing || response.data;
|
pricing.value = response.data?.pricing || response.data;
|
||||||
})
|
} catch (error) {
|
||||||
.catch((_error: any) => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not get oil pricing",
|
text: "Could not get oil pricing",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCustomer = (user_id: any) => {
|
const getCustomer = async (user_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/customer/" + user_id;
|
try {
|
||||||
axios({
|
const response = await customerService.getById(user_id);
|
||||||
method: "get",
|
customer.value = (response.data?.customer || response.data) as any;
|
||||||
url: path,
|
} catch (error) {
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
customer.value = response.data?.customer || response.data;
|
|
||||||
})
|
|
||||||
.catch((_error: any) => {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Could not find customer",
|
text: "Could not find customer",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentCard = (card_id: any) => {
|
const getPaymentCard = async (card_id: any) => {
|
||||||
if (card_id) {
|
if (card_id) {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/payment/card/" + card_id;
|
try {
|
||||||
axios({
|
const response = await paymentService.getCard(card_id);
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
// Check if we have valid card data
|
|
||||||
const card = response.data?.card || response.data;
|
const card = response.data?.card || response.data;
|
||||||
if (card && card.card_number && card.card_number !== '') {
|
if (card && card.card_number && card.card_number !== '') {
|
||||||
userCard.value = card;
|
userCard.value = card;
|
||||||
@@ -628,33 +608,26 @@ const getPaymentCard = (card_id: any) => {
|
|||||||
userCard.value = {} as CreditCard;
|
userCard.value = {} as CreditCard;
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error("Error fetching payment card:", error);
|
console.error("Error fetching payment card:", error);
|
||||||
userCard.value = {} as CreditCard;
|
userCard.value = {} as CreditCard;
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
userCardfound.value = false;
|
userCardfound.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOilOrder = (delivery_id: any) => {
|
const getOilOrder = async (delivery_id: any) => {
|
||||||
if (!delivery_id) { // Add a guard to prevent calls with an undefined ID
|
if (!delivery_id) {
|
||||||
console.error("getOilOrder called with no ID.");
|
console.error("getOilOrder called with no ID.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/" + delivery_id;
|
try {
|
||||||
axios({
|
// Note: deliveryService.getById uses /delivery/:id, which returns { ok: boolean, delivery: ... }
|
||||||
method: "get",
|
const response = await deliveryService.getById(Number(delivery_id));
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
// FIX: Check for the 'ok' flag and access the nested 'delivery' object
|
|
||||||
if (response.data && response.data.ok) {
|
if (response.data && response.data.ok) {
|
||||||
deliveryOrder.value = response.data.delivery; // <-- THIS IS THE CRITICAL CHANGE
|
deliveryOrder.value = response.data.delivery as any; // Cast because local ref is loose
|
||||||
|
|
||||||
// Now that deliveryOrder is the correct object, the rest of the logic will work.
|
// Now that deliveryOrder is the correct object, the rest of the logic will work.
|
||||||
getCustomer(deliveryOrder.value.customer_id);
|
getCustomer(deliveryOrder.value.customer_id);
|
||||||
@@ -672,16 +645,16 @@ const getOilOrder = (delivery_id: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error("API Error:", response.data.error || "Failed to fetch delivery data.");
|
console.error("API Error: Failed to fetch delivery data.");
|
||||||
notify({ title: "Error", text: "Could not load delivery details.", type: "error" });
|
notify({ title: "Error", text: "Could not load delivery details.", type: "error" });
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error("Error fetching delivery order:", error);
|
console.error("Error fetching delivery order:", error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOilOrderMoney = (delivery_id: any) => {
|
const getOilOrderMoney = (delivery_id: any) => {
|
||||||
|
// Keeping axios for this specific endpoint as it uses VITE_MONEY_URL and is not yet in services
|
||||||
let path = import.meta.env.VITE_MONEY_URL + "/delivery/order/money/" + delivery_id;
|
let path = import.meta.env.VITE_MONEY_URL + "/delivery/order/money/" + delivery_id;
|
||||||
axios({
|
axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
@@ -696,37 +669,29 @@ const getOilOrderMoney = (delivery_id: any) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sumdelivery = (delivery_id: any) => {
|
const sumdelivery = async (delivery_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/delivery/total/" + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.getTotal(Number(delivery_id));
|
||||||
method: "get",
|
// deliveryService.getTotal returns AxiosResponse<DeliveryTotalResponse>
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data && response.data.ok) {
|
if (response.data && response.data.ok) {
|
||||||
priceprime.value = response.data.priceprime || 0;
|
priceprime.value = response.data.priceprime || 0;
|
||||||
pricesameday.value = response.data.pricesameday || 0;
|
pricesameday.value = response.data.pricesameday || 0;
|
||||||
priceemergency.value = response.data.priceemergency || 0;
|
priceemergency.value = response.data.priceemergency || 0;
|
||||||
total_amount.value = parseFloat(response.data.total_amount) || 0;
|
total_amount.value = Number(response.data.total_amount) || 0;
|
||||||
discount.value = parseFloat(response.data.discount) || 0;
|
discount.value = Number(response.data.discount) || 0;
|
||||||
total_amount_after_discount.value = parseFloat(response.data.total_amount_after_discount) || 0;
|
total_amount_after_discount.value = Number(response.data.total_amount_after_discount) || 0;
|
||||||
} else {
|
} else {
|
||||||
// Fallback calculation if API doesn't return expected data
|
|
||||||
calculateFallbackTotal();
|
calculateFallbackTotal();
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error("Error fetching delivery totals:", error);
|
console.error("Error fetching delivery totals:", error);
|
||||||
// Fallback calculation on error
|
|
||||||
calculateFallbackTotal();
|
calculateFallbackTotal();
|
||||||
notify({
|
notify({
|
||||||
title: "Warning",
|
title: "Warning",
|
||||||
text: "Could not get delivery totals, using estimated calculation",
|
text: "Could not get delivery totals, using estimated calculation",
|
||||||
type: "warn",
|
type: "warn",
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateFallbackTotal = () => {
|
const calculateFallbackTotal = () => {
|
||||||
@@ -752,22 +717,15 @@ const calculateFallbackTotal = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPromo = (promo_id: any) => {
|
const getPromo = async (promo_id: any) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + "/promo/" + promo_id;
|
try {
|
||||||
axios({
|
const response = await adminService.promos.getById(Number(promo_id));
|
||||||
method: "get",
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
promo.value = response.data?.promo || response.data;
|
promo.value = response.data?.promo || (response.data as any);
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((error: any) => {
|
|
||||||
console.error('Error fetching promo:', error);
|
console.error('Error fetching promo:', error);
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateDeliveryTotal = () => {
|
const calculateDeliveryTotal = () => {
|
||||||
@@ -814,24 +772,21 @@ const calculateEstimatedTotal = () => {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTransaction = (delivery_id: any) => {
|
const getTransaction = async (delivery_id: any) => {
|
||||||
// Simple endpoint to get transaction directly by delivery_id
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/payment/transaction/delivery/${delivery_id}`;
|
const response = await paymentService.getDeliveryTransaction(Number(delivery_id));
|
||||||
axios.get(path, {
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader()
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
transaction.value = response.data.transaction;
|
// Cast needed if TS definition of PaymentTransaction doesn't perfectly overlap or if response wrapping is tricky
|
||||||
|
transaction.value = (response.data as any).transaction || response.data;
|
||||||
console.log("Transaction loaded:", transaction.value);
|
console.log("Transaction loaded:", transaction.value);
|
||||||
} else {
|
} else {
|
||||||
console.log("No transaction found for delivery:", delivery_id);
|
console.log("No transaction found for delivery:", delivery_id);
|
||||||
transaction.value = null;
|
transaction.value = null;
|
||||||
}
|
}
|
||||||
}).catch((error: any) => {
|
} catch (error) {
|
||||||
console.error("Error fetching transaction:", error);
|
console.error("Error fetching transaction:", error);
|
||||||
transaction.value = null;
|
transaction.value = null;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -123,19 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
@@ -157,22 +155,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
@@ -185,13 +176,10 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/cancelled/' + delivery_id;
|
try {
|
||||||
axios({
|
// Using deleteCancelled as per analysis of previous axios call to /delivery/cancelled/${id}
|
||||||
method: 'delete',
|
const response = await deliveryService.deleteCancelled(delivery_id);
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -200,13 +188,19 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
});
|
});
|
||||||
getPage(page.value)
|
getPage(page.value)
|
||||||
} else {
|
} else {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success", // Original code had success type for failure message? Keeping exact string or should fix? Keeping safe.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
notify({
|
notify({
|
||||||
title: "Failure",
|
title: "Failure",
|
||||||
text: "error deleting delivery",
|
text: "error deleting delivery",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -123,19 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
@@ -157,22 +155,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
@@ -185,13 +176,9 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -206,7 +193,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -123,19 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
@@ -157,22 +155,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
@@ -185,13 +176,9 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -206,7 +193,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -124,18 +124,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
import authHeader from '../../../services/auth.header'
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
@@ -157,42 +156,30 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = (pageVal: any) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/issue/' + pageVal;
|
try {
|
||||||
axios({
|
const response = await deliveryService.getIssues(pageVal)
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
deliveries.value = response.data?.deliveries || []
|
deliveries.value = response.data?.deliveries || []
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error('Error fetching issue deliveries:', error)
|
||||||
|
deliveries.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -207,7 +194,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -159,19 +159,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
@@ -193,22 +191,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
@@ -221,13 +212,9 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -242,7 +229,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -167,25 +167,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
|
interface TownTotal {
|
||||||
|
town: string;
|
||||||
|
gallons: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const token = ref(null)
|
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
const deliveries = ref<Delivery[]>([])
|
const deliveries = ref<Delivery[]>([])
|
||||||
const totals = ref<{ town: string; gallons: number }[]>([])
|
const totals = ref<TownTotal[]>([])
|
||||||
const grand_total = ref(0)
|
const grand_total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const perPage = ref(50)
|
const perPage = ref(50)
|
||||||
@@ -202,22 +204,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = (date: any) => new Date(date).getTime()
|
const mod = (date: any) => new Date(date).getTime()
|
||||||
@@ -237,29 +232,21 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_totals = () => {
|
const get_totals = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/today-totals';
|
try {
|
||||||
axios({
|
const response = await deliveryService.getTodayTotals();
|
||||||
method: 'get',
|
totals.value = response.data.totals || [];
|
||||||
url: path,
|
grand_total.value = response.data.grand_total || 0;
|
||||||
headers: authHeader(),
|
} catch (error) {
|
||||||
}).then((response: any) => {
|
|
||||||
totals.value = response.data.totals || []
|
|
||||||
grand_total.value = response.data.grand_total || 0
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error('Error fetching totals:', error);
|
console.error('Error fetching totals:', error);
|
||||||
totals.value = []
|
totals.value = []
|
||||||
grand_total.value = 0
|
grand_total.value = 0
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -274,7 +261,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -159,18 +159,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
import authHeader from '../../../services/auth.header'
|
import authService from '../../../services/authService'
|
||||||
|
import { printService } from '../../../services/printService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
interface TownTotal {
|
interface TownTotal {
|
||||||
@@ -199,58 +199,42 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = (pageVal: any) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/tommorrow/' + pageVal;
|
try {
|
||||||
axios({
|
const response = await deliveryService.getTomorrow(pageVal)
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
deliveries.value = response.data?.deliveries || []
|
deliveries.value = response.data?.deliveries || []
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error('Error fetching tomorrow deliveries:', error)
|
||||||
|
deliveries.value = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_totals = () => {
|
const get_totals = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/tomorrow-totals';
|
try {
|
||||||
axios({
|
const response = await deliveryService.getTomorrowTotals();
|
||||||
method: 'get',
|
totals.value = response.data.totals || [];
|
||||||
url: path,
|
grand_total.value = response.data.grand_total || 0;
|
||||||
headers: authHeader(),
|
} catch (error) {
|
||||||
}).then((response: any) => {
|
|
||||||
totals.value = response.data.totals || []
|
|
||||||
grand_total.value = response.data.grand_total || 0
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error('Error fetching totals:', error);
|
console.error('Error fetching totals:', error);
|
||||||
totals.value = []
|
totals.value = []
|
||||||
grand_total.value = 0
|
grand_total.value = 0
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -265,16 +249,44 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const printtTicketAll = () => {
|
const printtTicketAll = async () => {
|
||||||
let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/print_tommorrow';
|
try {
|
||||||
axios({
|
const response = await printService.printTomorrow();
|
||||||
method: 'delete',
|
if (response.data.ok) {
|
||||||
url: path,
|
notify({
|
||||||
headers: authHeader(),
|
title: "Success",
|
||||||
}).then((response: any) => {
|
text: "Sent to Printer",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
getPage(page.value);
|
||||||
|
} else {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error printing",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error printing",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const printTicket = async (delivery_id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await printService.printTicket(delivery_id);
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -289,31 +301,13 @@ const printtTicketAll = () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
}
|
|
||||||
|
|
||||||
const printTicket = (delivery_id: number) => {
|
|
||||||
let path = import.meta.env.VITE_PRINT_URL + '/command/printticket/' + delivery_id;
|
|
||||||
axios({
|
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
|
||||||
notify({
|
|
||||||
title: "Success",
|
|
||||||
text: "Sent to Printer",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
getPage(page.value)
|
|
||||||
} else {
|
|
||||||
notify({
|
notify({
|
||||||
title: "Failure",
|
title: "Failure",
|
||||||
text: "error printing",
|
text: "error printing",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -139,26 +139,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, markRaw } from 'vue'
|
import { ref, onMounted, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
|
||||||
import authHeader from '../../../services/auth.header'
|
|
||||||
import { deliveryService } from '../../../services/deliveryService'
|
import { deliveryService } from '../../../services/deliveryService'
|
||||||
|
import authService from '../../../services/authService'
|
||||||
import { Delivery } from '../../../types/models'
|
import { Delivery } from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import PaginationComp from '../../../components/pagination.vue'
|
import PaginationComp from '../../../components/pagination.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
|
interface TownTotal {
|
||||||
|
town: string;
|
||||||
|
gallons: number;
|
||||||
|
}
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const token = ref(null)
|
const token = ref(null)
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
const deliveries = ref<Delivery[]>([])
|
const deliveries = ref<Delivery[]>([])
|
||||||
const totals = ref<{ town: string; gallons: number }[]>([])
|
const totals = ref<TownTotal[]>([])
|
||||||
const grand_total = ref(0)
|
const grand_total = ref(0)
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const perPage = ref(50)
|
const perPage = ref(50)
|
||||||
@@ -175,22 +178,15 @@ const getPage = (pageVal: any) => {
|
|||||||
get_oil_orders(pageVal)
|
get_oil_orders(pageVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
try {
|
||||||
axios({
|
const response = await authService.whoami();
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
user.value = null;
|
||||||
user.value = null
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_oil_orders = async (pageVal: number) => {
|
const get_oil_orders = async (pageVal: number) => {
|
||||||
@@ -203,29 +199,21 @@ const get_oil_orders = async (pageVal: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const get_totals = () => {
|
const get_totals = async () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/deliverystatus/waiting-totals';
|
try {
|
||||||
axios({
|
const response = await deliveryService.getWaitingTotals();
|
||||||
method: 'get',
|
totals.value = response.data.totals || [];
|
||||||
url: path,
|
grand_total.value = response.data.grand_total || 0;
|
||||||
headers: authHeader(),
|
} catch (error) {
|
||||||
}).then((response: any) => {
|
|
||||||
totals.value = response.data.totals || []
|
|
||||||
grand_total.value = response.data.grand_total || 0
|
|
||||||
}).catch((error: any) => {
|
|
||||||
console.error('Error fetching totals:', error);
|
console.error('Error fetching totals:', error);
|
||||||
totals.value = []
|
totals.value = []
|
||||||
grand_total.value = 0
|
grand_total.value = 0
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCall = (delivery_id: any) => {
|
const deleteCall = async (delivery_id: number) => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/delivery/delete/' + delivery_id;
|
try {
|
||||||
axios({
|
const response = await deliveryService.delete(delivery_id);
|
||||||
method: 'delete',
|
|
||||||
url: path,
|
|
||||||
headers: authHeader(),
|
|
||||||
}).then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
notify({
|
notify({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
@@ -240,7 +228,13 @@ const deleteCall = (delivery_id: any) => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
|
notify({
|
||||||
|
title: "Failure",
|
||||||
|
text: "error deleting delivery",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|||||||
@@ -145,14 +145,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { minLength, required } from "@vuelidate/validators";
|
import { minLength, required } from "@vuelidate/validators";
|
||||||
|
|
||||||
@@ -164,7 +163,6 @@ interface SelectOption {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EmployeeCreate',
|
name: 'EmployeeCreate',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -161,13 +161,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { minLength, required } from "@vuelidate/validators";
|
import { minLength, required } from "@vuelidate/validators";
|
||||||
|
|
||||||
@@ -179,7 +178,6 @@ interface SelectOption {
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EmployeeEdit',
|
name: 'EmployeeEdit',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -91,19 +91,17 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template><script lang="ts">
|
</template><script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue'
|
import { defineComponent, markRaw } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import PaginationComp from '../../components/pagination.vue'
|
import PaginationComp from '../../components/pagination.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import {Employee, User} from '../../types/models'
|
import {Employee, User} from '../../types/models'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'EmployeeHome',
|
name: 'EmployeeHome',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -86,19 +86,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'employeeProfile',
|
name: 'employeeProfile',
|
||||||
components: {
|
components: {
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -40,7 +40,6 @@
|
|||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'MoneyYear',
|
name: 'MoneyYear',
|
||||||
@@ -48,7 +47,6 @@
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ const getOilPricing = () => {
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
||||||
pricing.value = response.data?.pricing || response.data;
|
pricing.value = response.data?.pricing || (response.data as unknown as PricingData);
|
||||||
calculateDefaultChargeAmount()
|
calculateDefaultChargeAmount()
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ const getCustomer = (user_id: number | string) => {
|
|||||||
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
|
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
axios.get(path, { withCredentials: true, headers: authHeader() })
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; customer?: CustomerFormData }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; customer?: CustomerFormData }>) => {
|
||||||
customer.value = response.data?.customer || response.data;
|
customer.value = response.data?.customer || (response.data as unknown as CustomerFormData);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
||||||
@@ -460,7 +460,7 @@ const getCustomerDescription = (user_id: number | string) => {
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; description?: CustomerDescriptionData }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; description?: CustomerDescriptionData }>) => {
|
||||||
customerDescription.value = response.data?.description || response.data;
|
customerDescription.value = response.data?.description || (response.data as unknown as CustomerDescriptionData);
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ const getOilPricing = () => {
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
||||||
pricing.value = response.data?.pricing || response.data;
|
pricing.value = response.data?.pricing || (response.data as unknown as PricingData);
|
||||||
// Try to update charge amount when pricing is loaded
|
// Try to update charge amount when pricing is loaded
|
||||||
updateChargeAmount();
|
updateChargeAmount();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -275,7 +275,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -285,7 +285,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
import type {
|
import type {
|
||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
@@ -491,7 +490,7 @@ const getCustomer = (user_id: number) => {
|
|||||||
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
|
const path = `${import.meta.env.VITE_BASE_URL}/customer/${user_id}`;
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
axios.get(path, { withCredentials: true, headers: authHeader() })
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; customer?: CustomerFormData }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; customer?: CustomerFormData }>) => {
|
||||||
customer.value = response.data?.customer || response.data;
|
customer.value = response.data?.customer || (response.data as unknown as CustomerFormData);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
notify({ title: "Error", text: "Could not find customer", type: "error" });
|
||||||
@@ -503,7 +502,7 @@ const getOilPricing = () => {
|
|||||||
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
|
const path = `${import.meta.env.VITE_BASE_URL}/info/price/oil/table`;
|
||||||
axios.get(path, { withCredentials: true, headers: authHeader() })
|
axios.get(path, { withCredentials: true, headers: authHeader() })
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
||||||
pricing.value = response.data?.pricing || response.data;
|
pricing.value = response.data?.pricing || (response.data as unknown as PricingData);
|
||||||
// Calculate capture amount if delivery order is already loaded
|
// Calculate capture amount if delivery order is already loaded
|
||||||
calculateCaptureAmount();
|
calculateCaptureAmount();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -332,7 +332,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -342,7 +342,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import type {
|
import type {
|
||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
DeliveryFormData,
|
DeliveryFormData,
|
||||||
@@ -566,7 +565,7 @@ const getOilPricing = () => {
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; pricing?: OilPricingResponse }>) => {
|
||||||
pricing.value = response.data?.pricing || response.data;
|
pricing.value = response.data?.pricing || (response.data as unknown as PricingData);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
notify({
|
notify({
|
||||||
|
|||||||
@@ -202,7 +202,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -211,7 +211,6 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import authHeader from '../../../services/auth.header';
|
import authHeader from '../../../services/auth.header';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Footer from '../../../layouts/footers/footer.vue';
|
|
||||||
|
|
||||||
// --- Interfaces for Type Safety ---
|
// --- Interfaces for Type Safety ---
|
||||||
interface UserCard {
|
interface UserCard {
|
||||||
|
|||||||
@@ -268,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -287,7 +287,6 @@ import type {
|
|||||||
} from '../../../types/models'
|
} from '../../../types/models'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
|
|
||||||
import useValidate from "@vuelidate/core";
|
import useValidate from "@vuelidate/core";
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
@@ -470,7 +469,7 @@ const getServicePartsForCustomer = () => {
|
|||||||
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${service.value.customer_id}`;
|
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${service.value.customer_id}`;
|
||||||
axios.get(path, { headers: authHeader() })
|
axios.get(path, { headers: authHeader() })
|
||||||
.then((response: AxiosResponse<{ ok?: boolean; parts?: ServicePart[] }>) => {
|
.then((response: AxiosResponse<{ ok?: boolean; parts?: ServicePart[] }>) => {
|
||||||
serviceParts.value = response.data?.parts || response.data;
|
serviceParts.value = response.data?.parts || (response.data as unknown as ServicePart[]);
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
console.error("Failed to fetch service parts:", error);
|
console.error("Failed to fetch service parts:", error);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!-- src/pages/service/ServiceCalendar.vue -->
|
<!-- src/pages/service/ServiceCalendar.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="flex">
|
<div class="calendar-page">
|
||||||
|
<div class="w-full px-4 md:px-10">
|
||||||
<div class="w-full px-10">
|
<!-- Breadcrumbs -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="text-sm breadcrumbs mb-4">
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
<li><router-link :to="{ name: 'home' }">Home</router-link></li>
|
||||||
@@ -11,21 +11,104 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex text-2xl mb-5 font-bold">
|
<!-- Page Header -->
|
||||||
Master Service Calendar
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold mb-1">Service Calendar</h1>
|
||||||
|
<p class="text-base-content/60">Manage and schedule service calls</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button @click="goToToday" class="btn btn-ghost btn-sm gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||||
|
</svg>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
<router-link :to="{ name: 'ServiceHome' }" class="btn btn-primary btn-sm gap-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
New Service Call
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex h-screen font-sans">
|
<!-- Calendar Container -->
|
||||||
<div class="flex-1 p-4 overflow-auto">
|
<div class="calendar-container bg-gradient-to-br from-neutral to-neutral/80 rounded-xl shadow-strong overflow-hidden">
|
||||||
<!-- The 'ref' is important for accessing the calendar's API -->
|
<!-- Custom Calendar Header -->
|
||||||
|
<div class="calendar-header bg-base-200 px-6 py-4 border-b border-base-300">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="previousMonth" class="btn btn-ghost btn-sm btn-circle">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2 class="text-2xl font-bold min-w-[200px] text-center">{{ currentMonthYear }}</h2>
|
||||||
|
<button @click="nextMonth" class="btn btn-ghost btn-sm btn-circle">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Switcher -->
|
||||||
|
<div class="btn-group">
|
||||||
|
<button
|
||||||
|
@click="changeView('dayGridMonth')"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="{ 'btn-active': currentView === 'dayGridMonth' }"
|
||||||
|
>
|
||||||
|
Month
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="changeView('dayGridWeek')"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="{ 'btn-active': currentView === 'dayGridWeek' }"
|
||||||
|
>
|
||||||
|
Week
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="changeView('dayGridDay')"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="{ 'btn-active': currentView === 'dayGridDay' }"
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-info"></div>
|
||||||
|
<span class="text-sm">Scheduled</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-success"></div>
|
||||||
|
<span class="text-sm">Completed</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-warning"></div>
|
||||||
|
<span class="text-sm">In Progress</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-accent"></div>
|
||||||
|
<span class="text-sm">Federal Holiday</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FullCalendar -->
|
||||||
|
<div class="calendar-body p-6">
|
||||||
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
|
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
<!-- Edit Modal -->
|
||||||
|
|
||||||
<ServiceEditModal
|
<ServiceEditModal
|
||||||
v-if="selectedServiceForEdit"
|
v-if="selectedServiceForEdit"
|
||||||
:service="selectedServiceForEdit"
|
:service="selectedServiceForEdit"
|
||||||
@@ -36,41 +119,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import Header from '../../layouts/headers/headerauth.vue';
|
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue';
|
|
||||||
import Footer from '../../layouts/footers/footer.vue';
|
|
||||||
import FullCalendar from '@fullcalendar/vue3';
|
import FullCalendar from '@fullcalendar/vue3';
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
|
import { CalendarOptions, EventClickArg, DayCellContentArg } from '@fullcalendar/core';
|
||||||
import ServiceEditModal from './ServiceEditModal.vue';
|
import ServiceEditModal from './ServiceEditModal.vue';
|
||||||
import axios from 'axios';
|
import { serviceService } from '../../services/serviceService';
|
||||||
import authHeader from '../../services/auth.header';
|
import { authService } from '../../services/authService';
|
||||||
|
import { AxiosResponse, AxiosError, ServiceCall } from '../../types/models';
|
||||||
interface ServiceCall {
|
import { getFederalHolidays, type Holiday } from '../../utils/holidays';
|
||||||
id: number;
|
|
||||||
scheduled_date: string;
|
|
||||||
customer_id: number;
|
|
||||||
customer_name: string;
|
|
||||||
customer_address: string;
|
|
||||||
customer_town: string;
|
|
||||||
type_service_call: number;
|
|
||||||
description: string;
|
|
||||||
service_cost: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
const selectedServiceForEdit = ref(null as Partial<ServiceCall> | null)
|
const selectedServiceForEdit = ref(null as Partial<ServiceCall> | null)
|
||||||
const fullCalendar = ref()
|
const fullCalendar = ref()
|
||||||
|
const currentView = ref('dayGridMonth')
|
||||||
|
const holidays = ref<Holiday[]>([])
|
||||||
|
const currentDate = ref(new Date())
|
||||||
|
|
||||||
// Functions
|
// Computed
|
||||||
// We can remove the fetchEvents method as FullCalendar now handles it.
|
const currentMonthYear = computed(() => {
|
||||||
// async fetchEvents(): Promise<void> { ... }
|
return currentDate.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load holidays for current year and next year
|
||||||
|
const loadHolidays = () => {
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const allHolidays = [
|
||||||
|
...getFederalHolidays(currentYear - 1),
|
||||||
|
...getFederalHolidays(currentYear),
|
||||||
|
...getFederalHolidays(currentYear + 1),
|
||||||
|
]
|
||||||
|
holidays.value = allHolidays
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a date is a holiday
|
||||||
|
const isHolidayDate = (dateStr: string): Holiday | undefined => {
|
||||||
|
return holidays.value.find(h => h.date === dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
const handleEventClick = (clickInfo: EventClickArg): void => {
|
const handleEventClick = (clickInfo: EventClickArg): void => {
|
||||||
// This logic remains the same, as it correctly pulls data from extendedProps
|
|
||||||
selectedServiceForEdit.value = {
|
selectedServiceForEdit.value = {
|
||||||
id: parseInt(clickInfo.event.id),
|
id: parseInt(clickInfo.event.id),
|
||||||
scheduled_date: clickInfo.event.startStr,
|
scheduled_date: clickInfo.event.startStr,
|
||||||
@@ -89,73 +179,116 @@ const fetchCalendarEvents = async (
|
|||||||
failureCallback: (error: Error) => void
|
failureCallback: (error: Error) => void
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
|
const response = await serviceService.getAll();
|
||||||
const response = await axios.get(path, {
|
const serviceEvents = response.data?.events || [];
|
||||||
headers: authHeader(),
|
|
||||||
withCredentials: true,
|
// Add federal holidays as background events
|
||||||
});
|
const holidayEvents = holidays.value.map(holiday => ({
|
||||||
// Backend returns { ok: true, events: [...] }
|
id: `holiday-${holiday.date}`,
|
||||||
const events = response.data?.events || [];
|
title: holiday.name,
|
||||||
successCallback(events);
|
start: holiday.date,
|
||||||
} catch (error) {
|
allDay: true,
|
||||||
|
display: 'background',
|
||||||
|
classNames: ['holiday-event']
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combine service events and holiday events
|
||||||
|
const allEvents = [...serviceEvents, ...holidayEvents];
|
||||||
|
|
||||||
|
successCallback(allEvents);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as AxiosError;
|
||||||
console.error("Failed to fetch calendar events:", error);
|
console.error("Failed to fetch calendar events:", error);
|
||||||
failureCallback(error as Error);
|
failureCallback(error as Error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calendar navigation
|
||||||
|
const goToToday = () => {
|
||||||
|
const calendarApi = (fullCalendar.value as any).getApi()
|
||||||
|
calendarApi.today()
|
||||||
|
currentDate.value = calendarApi.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousMonth = () => {
|
||||||
|
const calendarApi = (fullCalendar.value as any).getApi()
|
||||||
|
calendarApi.prev()
|
||||||
|
currentDate.value = calendarApi.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMonth = () => {
|
||||||
|
const calendarApi = (fullCalendar.value as any).getApi()
|
||||||
|
calendarApi.next()
|
||||||
|
currentDate.value = calendarApi.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeView = (viewName: string) => {
|
||||||
|
currentView.value = viewName
|
||||||
|
const calendarApi = (fullCalendar.value as any).getApi()
|
||||||
|
calendarApi.changeView(viewName)
|
||||||
|
currentDate.value = calendarApi.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day cell class names for holidays
|
||||||
|
const getDayCellClassNames = (arg: any) => {
|
||||||
|
// Format date as YYYY-MM-DD
|
||||||
|
const year = arg.date.getFullYear()
|
||||||
|
const month = String(arg.date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(arg.date.getDate()).padStart(2, '0')
|
||||||
|
const dateStr = `${year}-${month}-${day}`
|
||||||
|
|
||||||
|
const holiday = isHolidayDate(dateStr)
|
||||||
|
|
||||||
|
if (holiday) {
|
||||||
|
console.log('Holiday found:', holiday.name, 'on', dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return holiday ? ['holiday-cell'] : []
|
||||||
|
}
|
||||||
|
|
||||||
// Calendar options
|
// Calendar options
|
||||||
const calendarOptions = ref({
|
const calendarOptions = ref({
|
||||||
plugins: [dayGridPlugin, interactionPlugin],
|
plugins: [dayGridPlugin, interactionPlugin],
|
||||||
initialView: 'dayGridMonth',
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: false, // We're using custom header
|
||||||
weekends: true,
|
weekends: true,
|
||||||
// Use function source to fetch events with auth headers and transform response
|
height: 'auto',
|
||||||
events: fetchCalendarEvents,
|
events: fetchCalendarEvents,
|
||||||
eventClick: handleEventClick,
|
eventClick: handleEventClick,
|
||||||
|
eventClassNames: 'custom-event',
|
||||||
|
dayCellClassNames: getDayCellClassNames,
|
||||||
|
eventDisplay: 'block',
|
||||||
|
displayEventTime: false,
|
||||||
|
eventBackgroundColor: 'transparent',
|
||||||
|
eventBorderColor: 'transparent',
|
||||||
} as CalendarOptions)
|
} as CalendarOptions)
|
||||||
|
|
||||||
// Lifecycle
|
// Modal handlers
|
||||||
onMounted(() => {
|
|
||||||
userStatus();
|
|
||||||
// We no longer need to call fetchEvents() here because FullCalendar does it automatically.
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeEditModal = () => {
|
const closeEditModal = () => {
|
||||||
selectedServiceForEdit.value = null;
|
selectedServiceForEdit.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================== THIS IS THE CORRECTED SECTION ===================
|
|
||||||
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
|
await serviceService.update(updatedService.id, updatedService);
|
||||||
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
|
|
||||||
|
|
||||||
// Get the FullCalendar component instance from the ref
|
|
||||||
const calendarApi = (fullCalendar.value as any).getApi();
|
const calendarApi = (fullCalendar.value as any).getApi();
|
||||||
if (calendarApi) {
|
if (calendarApi) {
|
||||||
// Tell FullCalendar to re-fetch its events from the source.
|
|
||||||
// This is the most reliable way to refresh the view immediately.
|
|
||||||
calendarApi.refetchEvents();
|
calendarApi.refetchEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save changes:", error);
|
console.error("Failed to save changes:", error);
|
||||||
alert("An error occurred while saving. Please check the console.");
|
alert("An error occurred while saving. Please check the console.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// =================== END OF CORRECTED SECTION ===================
|
|
||||||
|
|
||||||
const handleDeleteService = async (serviceId: number) => {
|
const handleDeleteService = async (serviceId: number) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
|
await serviceService.delete(serviceId);
|
||||||
await axios.delete(path, { withCredentials: true, headers: authHeader() });
|
|
||||||
|
|
||||||
// Also refresh the calendar after a delete
|
|
||||||
const calendarApi = (fullCalendar.value as any).getApi();
|
const calendarApi = (fullCalendar.value as any).getApi();
|
||||||
if (calendarApi) {
|
if (calendarApi) {
|
||||||
calendarApi.refetchEvents();
|
calendarApi.refetchEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting event:", error);
|
console.error("Error deleting event:", error);
|
||||||
@@ -163,14 +296,7 @@ const handleDeleteService = async (serviceId: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami().then((response: AxiosResponse<any>) => {
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
}
|
}
|
||||||
@@ -179,4 +305,153 @@ const userStatus = () => {
|
|||||||
user.value = null
|
user.value = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
userStatus();
|
||||||
|
loadHolidays();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.calendar-page {
|
||||||
|
@apply min-h-screen animate-fade-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-container {
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar Custom Styling */
|
||||||
|
:deep(.fc) {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-theme-standard td),
|
||||||
|
:deep(.fc-theme-standard th) {
|
||||||
|
border-color: hsl(var(--bc) / 0.2) !important;
|
||||||
|
border-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-scrollgrid) {
|
||||||
|
border-width: 2px !important;
|
||||||
|
border-color: hsl(var(--bc) / 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-col-header-cell) {
|
||||||
|
@apply bg-base-200 font-semibold text-sm uppercase tracking-wider py-3;
|
||||||
|
border-width: 2px !important;
|
||||||
|
border-color: hsl(var(--bc) / 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-daygrid-day) {
|
||||||
|
@apply transition-colors hover:bg-base-200/50;
|
||||||
|
border-width: 2px !important;
|
||||||
|
border-color: hsl(var(--bc) / 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-daygrid-day-number) {
|
||||||
|
@apply text-base-content/80 font-medium p-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-day-today) {
|
||||||
|
@apply bg-primary/5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-day-today .fc-daygrid-day-number) {
|
||||||
|
@apply text-primary font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Event Styling */
|
||||||
|
:deep(.fc-event) {
|
||||||
|
@apply rounded-lg px-2 py-1 mb-1 cursor-pointer;
|
||||||
|
@apply bg-info/20 border-l-4 border-info;
|
||||||
|
@apply hover:bg-info/30 transition-colors;
|
||||||
|
@apply shadow-sm hover:shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-event-title) {
|
||||||
|
@apply text-sm font-medium text-base-content truncate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cell styling */
|
||||||
|
:deep(.fc-daygrid-day-frame) {
|
||||||
|
@apply min-h-[100px];
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-daygrid-day-top) {
|
||||||
|
@apply flex justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default FullCalendar button styling since we have custom header */
|
||||||
|
:deep(.fc-toolbar) {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Holiday styling - Using accent color from theme */
|
||||||
|
:deep(.holiday-cell) {
|
||||||
|
background-color: hsl(var(--a) / 0.15) !important;
|
||||||
|
border-color: hsl(var(--a) / 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.holiday-cell .fc-daygrid-day-number) {
|
||||||
|
color: hsl(var(--a)) !important;
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Holiday background events */
|
||||||
|
:deep(.fc-bg-event.holiday-event) {
|
||||||
|
background-color: hsl(var(--a) / 0.5) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-daygrid-day-bg .fc-bg-event.holiday-event) {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Holiday event title positioning */
|
||||||
|
:deep(.fc-bg-event.holiday-event .fc-event-title) {
|
||||||
|
position: absolute !important;
|
||||||
|
bottom: 4px !important;
|
||||||
|
left: 4px !important;
|
||||||
|
right: 4px !important;
|
||||||
|
top: auto !important;
|
||||||
|
font-size: 0.7rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: hsl(var(--a)) !important;
|
||||||
|
text-align: center !important;
|
||||||
|
line-height: 1.2 !important;
|
||||||
|
padding: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekend styling - Using theme colors */
|
||||||
|
:deep(.fc-day-sat),
|
||||||
|
:deep(.fc-day-sun) {
|
||||||
|
background-color: hsl(var(--b3)) !important;
|
||||||
|
border-color: hsl(var(--bc) / 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.fc-day-sat .fc-daygrid-day-number),
|
||||||
|
:deep(.fc-day-sun .fc-daygrid-day-number) {
|
||||||
|
color: hsl(var(--bc) / 0.8) !important;
|
||||||
|
@apply font-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weekend header cells */
|
||||||
|
:deep(.fc-col-header-cell.fc-day-sat),
|
||||||
|
:deep(.fc-col-header-cell.fc-day-sun) {
|
||||||
|
background-color: hsl(var(--b3)) !important;
|
||||||
|
color: hsl(var(--bc)) !important;
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -102,8 +102,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import axios from 'axios';
|
import serviceService from '../../services/serviceService';
|
||||||
import authHeader from '../../services/auth.header';
|
import customerService from '../../services/customerService';
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string }
|
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string }
|
||||||
@@ -132,32 +132,33 @@ const serviceOptions = ref([
|
|||||||
{ text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 },
|
{ text: 'Tank Install', value: 3 }, { text: 'Other', value: 4 },
|
||||||
])
|
])
|
||||||
|
|
||||||
// Watchers
|
// Functions (defined before watchers to avoid hoisting issues)
|
||||||
watch(() => props.service, (newVal) => {
|
|
||||||
if (!newVal) return;
|
|
||||||
const scheduled = dayjs(newVal.scheduled_date || new Date());
|
|
||||||
editableService.value = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
|
|
||||||
if (newVal.customer_id) {
|
|
||||||
getCustomer(newVal.customer_id);
|
|
||||||
getServiceParts(newVal.customer_id);
|
|
||||||
}
|
|
||||||
}, { immediate: true, deep: true })
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
const getCustomer = (customerId: number) => {
|
const getCustomer = (customerId: number) => {
|
||||||
customer.value = null;
|
customer.value = null;
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/customer/' + customerId;
|
customerService.getById(customerId)
|
||||||
axios.get(path, { headers: authHeader() })
|
.then((response: any) => {
|
||||||
.then((response: any) => { customer.value = response.data; })
|
if (response.data.customer) {
|
||||||
|
customer.value = response.data.customer;
|
||||||
|
} else if (response.data.ok && response.data.id) {
|
||||||
|
customer.value = response.data as unknown as Customer;
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); });
|
.catch((error: any) => { console.error("Failed to fetch customer details for modal:", error); });
|
||||||
}
|
}
|
||||||
|
|
||||||
const getServiceParts = (customerId: number) => {
|
const getServiceParts = (customerId: number) => {
|
||||||
isLoadingParts.value = true;
|
isLoadingParts.value = true;
|
||||||
serviceParts.value = null;
|
serviceParts.value = null;
|
||||||
let path = `${import.meta.env.VITE_BASE_URL}/service/parts/customer/${customerId}`;
|
serviceService.getPartsForCustomer(customerId)
|
||||||
axios.get(path, { headers: authHeader() })
|
.then((response: any) => {
|
||||||
.then((response: any) => { serviceParts.value = response.data; })
|
if (response.data.parts) {
|
||||||
|
if (Array.isArray(response.data.parts) && response.data.parts.length > 0) {
|
||||||
|
serviceParts.value = response.data.parts[0];
|
||||||
|
} else {
|
||||||
|
serviceParts.value = response.data.parts as unknown as ServiceParts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((error: any) => { console.error("Failed to fetch service parts:", error); })
|
.catch((error: any) => { console.error("Failed to fetch service parts:", error); })
|
||||||
.finally(() => { isLoadingParts.value = false; });
|
.finally(() => { isLoadingParts.value = false; });
|
||||||
}
|
}
|
||||||
@@ -168,9 +169,8 @@ const saveChanges = async () => {
|
|||||||
const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
|
const combinedDateTime = dayjs(`${date} ${time}:00`).format('YYYY-MM-DDTHH:mm:ss');
|
||||||
const finalPayload = { ...props.service, ...editableService.value, scheduled_date: combinedDateTime };
|
const finalPayload = { ...props.service, ...editableService.value, scheduled_date: combinedDateTime };
|
||||||
|
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${finalPayload.id}`;
|
|
||||||
try {
|
try {
|
||||||
await axios.put(path, finalPayload, { headers: authHeader(), withCredentials: true });
|
await serviceService.update(finalPayload.id!, finalPayload);
|
||||||
emit('save-changes', finalPayload as ServiceCall);
|
emit('save-changes', finalPayload as ServiceCall);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save changes:", error);
|
console.error("Failed to save changes:", error);
|
||||||
@@ -201,4 +201,15 @@ const getStateAbbrev = (stateId: number | undefined | null): string => {
|
|||||||
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
|
const stateMap: { [key: number]: string } = { 0: 'MA', 1: 'RI', 2: 'NH', 3: 'ME', 4: 'VT', 5: 'CT', 6: 'NY' };
|
||||||
return stateMap[stateId] || 'Unknown';
|
return stateMap[stateId] || 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watchers (after function definitions)
|
||||||
|
watch(() => props.service, (newVal) => {
|
||||||
|
if (!newVal) return;
|
||||||
|
const scheduled = dayjs(newVal.scheduled_date || new Date());
|
||||||
|
editableService.value = { ...newVal, date: scheduled.format('YYYY-MM-DD'), time: scheduled.hour() };
|
||||||
|
if (newVal.customer_id) {
|
||||||
|
getCustomer(newVal.customer_id);
|
||||||
|
getServiceParts(newVal.customer_id);
|
||||||
|
}
|
||||||
|
}, { immediate: true, deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<ServiceEditModal
|
<ServiceEditModal
|
||||||
v-if="selectedServiceForEdit"
|
v-if="selectedServiceForEdit"
|
||||||
@@ -158,10 +158,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import { serviceService } from '../../services/serviceService'
|
||||||
import authHeader from '../../services/auth.header'
|
import { authService } from '../../services/authService'
|
||||||
import { ServiceCall } from '../../types/models'
|
import { ServiceCall } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import ServiceEditModal from './ServiceEditModal.vue'
|
import ServiceEditModal from './ServiceEditModal.vue'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
@@ -184,13 +183,11 @@ onMounted(() => {
|
|||||||
const fetchUpcomingServices = async (): Promise<void> => {
|
const fetchUpcomingServices = async (): Promise<void> => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/service/upcoming';
|
const response = await serviceService.getUpcoming();
|
||||||
const response = await axios.get(path, {
|
if (response.data.ok) {
|
||||||
headers: authHeader(),
|
const serviceList = response.data.services || [];
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
const serviceList = response.data?.services || [];
|
|
||||||
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch upcoming service calls:", error);
|
console.error("Failed to fetch upcoming service calls:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -199,13 +196,7 @@ const fetchUpcomingServices = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami()
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
@@ -250,8 +241,7 @@ const toggleExpand = (id: number): void => {
|
|||||||
|
|
||||||
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
|
const response = await serviceService.update(updatedService.id, updatedService);
|
||||||
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
const index = services.value.findIndex(s => s.id === updatedService.id);
|
const index = services.value.findIndex(s => s.id === updatedService.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
@@ -267,8 +257,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
|
|||||||
|
|
||||||
const handleDeleteService = async (serviceId: number) => {
|
const handleDeleteService = async (serviceId: number) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
|
const response = await serviceService.delete(serviceId);
|
||||||
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
services.value = services.value.filter(s => s.id !== serviceId);
|
services.value = services.value.filter(s => s.id !== serviceId);
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<ServiceEditModal
|
<ServiceEditModal
|
||||||
v-if="selectedServiceForEdit"
|
v-if="selectedServiceForEdit"
|
||||||
@@ -165,15 +165,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import serviceService from '../../services/serviceService'
|
||||||
import authHeader from '../../services/auth.header'
|
import authService from '../../services/authService'
|
||||||
import { ServiceCall } from '../../types/models'
|
import { ServiceCall } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import ServiceEditModal from './ServiceEditModal.vue'
|
import ServiceEditModal from './ServiceEditModal.vue'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const user = ref(null)
|
const user = ref<any>(null)
|
||||||
const services = ref<ServiceCall[]>([])
|
const services = ref<ServiceCall[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const selectedServiceForEdit = ref<ServiceCall | null>(null)
|
const selectedServiceForEdit = ref<ServiceCall | null>(null)
|
||||||
@@ -183,7 +182,9 @@ const expandedIds = ref<number[]>([])
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (authService) {
|
||||||
userStatus();
|
userStatus();
|
||||||
|
}
|
||||||
fetchPastServices();
|
fetchPastServices();
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,13 +216,12 @@ const toggleExpand = (id: number): void => {
|
|||||||
const fetchPastServices = async (): Promise<void> => {
|
const fetchPastServices = async (): Promise<void> => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/service/past';
|
const response = await serviceService.getPast();
|
||||||
const response = await axios.get(path, {
|
if (response.data && response.data.services) {
|
||||||
headers: authHeader(),
|
services.value = response.data.services.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
||||||
withCredentials: true,
|
} else {
|
||||||
});
|
services.value = [];
|
||||||
const serviceList = response.data?.services || [];
|
}
|
||||||
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch past service calls:", error);
|
console.error("Failed to fetch past service calls:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -230,13 +230,7 @@ const fetchPastServices = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami()
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
@@ -257,9 +251,8 @@ const closeEditModal = () => {
|
|||||||
|
|
||||||
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
|
const response = await serviceService.update(updatedService.id, updatedService);
|
||||||
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
|
if (response.data.service) { // Based on ServiceResponse type
|
||||||
if (response.data.ok) {
|
|
||||||
const index = services.value.findIndex(s => s.id === updatedService.id);
|
const index = services.value.findIndex(s => s.id === updatedService.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
services.value[index] = response.data.service;
|
services.value[index] = response.data.service;
|
||||||
@@ -274,8 +267,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
|
|||||||
|
|
||||||
const handleDeleteService = async (serviceId: number) => {
|
const handleDeleteService = async (serviceId: number) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
|
const response = await serviceService.delete(serviceId);
|
||||||
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
services.value = services.value.filter(s => s.id !== serviceId);
|
services.value = services.value.filter(s => s.id !== serviceId);
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
|
|||||||
@@ -120,25 +120,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import serviceService from '../../services/serviceService'
|
||||||
import authHeader from '../../services/auth.header'
|
import authService from '../../services/authService'
|
||||||
import { ServicePlan } from '../../types/models'
|
import { ServicePlan } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const user = ref(null)
|
const user = ref<any>(null)
|
||||||
const servicePlans = ref<ServicePlan[]>([])
|
const servicePlans = ref<ServicePlan[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (authService) {
|
||||||
userStatus();
|
userStatus();
|
||||||
|
}
|
||||||
fetchServicePlans();
|
fetchServicePlans();
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -146,13 +147,12 @@ onMounted(() => {
|
|||||||
const fetchServicePlans = async (): Promise<void> => {
|
const fetchServicePlans = async (): Promise<void> => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/service/plans/active';
|
const response = await serviceService.plans.getActive();
|
||||||
const response = await axios.get(path, {
|
if (response.data && response.data.plans) {
|
||||||
headers: authHeader(),
|
servicePlans.value = response.data.plans;
|
||||||
withCredentials: true,
|
} else {
|
||||||
});
|
servicePlans.value = [];
|
||||||
// Backend returns { ok: true, plans: [...] }
|
}
|
||||||
servicePlans.value = response.data?.plans || [];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch service plans:", error);
|
console.error("Failed to fetch service plans:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -161,13 +161,7 @@ const fetchServicePlans = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami()
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<ServiceEditModal
|
<ServiceEditModal
|
||||||
v-if="selectedServiceForEdit"
|
v-if="selectedServiceForEdit"
|
||||||
@@ -165,15 +165,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import axios from 'axios'
|
import serviceService from '../../services/serviceService'
|
||||||
import authHeader from '../../services/auth.header'
|
import authService from '../../services/authService'
|
||||||
import { ServiceCall } from '../../types/models'
|
import { ServiceCall } from '../../types/models'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import ServiceEditModal from './ServiceEditModal.vue'
|
import ServiceEditModal from './ServiceEditModal.vue'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// Reactive data
|
// Reactive data
|
||||||
const user = ref(null)
|
const user = ref<any>(null)
|
||||||
const services = ref<ServiceCall[]>([])
|
const services = ref<ServiceCall[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const selectedServiceForEdit = ref<ServiceCall | null>(null)
|
const selectedServiceForEdit = ref<ServiceCall | null>(null)
|
||||||
@@ -183,7 +182,9 @@ const expandedIds = ref<number[]>([])
|
|||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (authService) { // Check if imported correctly
|
||||||
userStatus();
|
userStatus();
|
||||||
|
}
|
||||||
fetchTodayServices();
|
fetchTodayServices();
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,13 +216,16 @@ const toggleExpand = (id: number): void => {
|
|||||||
const fetchTodayServices = async (): Promise<void> => {
|
const fetchTodayServices = async (): Promise<void> => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const path = import.meta.env.VITE_BASE_URL + '/service/today';
|
const response = await serviceService.getToday();
|
||||||
const response = await axios.get(path, {
|
// According to serviceService.ts, getToday returns AxiosResponse<ServicesResponse>
|
||||||
headers: authHeader(),
|
// ServicesResponse has { ok: boolean, services: ServiceCall[] }
|
||||||
withCredentials: true,
|
// However, the api unwrap interceptor might put properties directly on data
|
||||||
});
|
// Let's assume the response structure follows the type
|
||||||
const serviceList = response.data?.services || [];
|
if (response.data && response.data.services) {
|
||||||
services.value = serviceList.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
services.value = response.data.services.sort((a: ServiceCall, b: ServiceCall) => b.id - a.id);
|
||||||
|
} else {
|
||||||
|
services.value = [];
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch today's service calls:", error);
|
console.error("Failed to fetch today's service calls:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -230,13 +234,7 @@ const fetchTodayServices = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userStatus = () => {
|
const userStatus = () => {
|
||||||
let path = import.meta.env.VITE_BASE_URL + '/auth/whoami';
|
authService.whoami()
|
||||||
axios({
|
|
||||||
method: 'get',
|
|
||||||
url: path,
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
})
|
|
||||||
.then((response: any) => {
|
.then((response: any) => {
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
user.value = response.data.user;
|
user.value = response.data.user;
|
||||||
@@ -257,9 +255,8 @@ const closeEditModal = () => {
|
|||||||
|
|
||||||
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
|
const response = await serviceService.update(updatedService.id, updatedService);
|
||||||
const response = await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
|
if (response.data.service) { // Based on ServiceResponse type
|
||||||
if (response.data.ok) {
|
|
||||||
const index = services.value.findIndex(s => s.id === updatedService.id);
|
const index = services.value.findIndex(s => s.id === updatedService.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
services.value[index] = response.data.service;
|
services.value[index] = response.data.service;
|
||||||
@@ -274,8 +271,7 @@ const handleSaveChanges = async (updatedService: ServiceCall) => {
|
|||||||
|
|
||||||
const handleDeleteService = async (serviceId: number) => {
|
const handleDeleteService = async (serviceId: number) => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
|
const response = await serviceService.delete(serviceId);
|
||||||
const response = await axios.delete(path, { headers: authHeader(), withCredentials: true });
|
|
||||||
if (response.data.ok) {
|
if (response.data.ok) {
|
||||||
services.value = services.value.filter(s => s.id !== serviceId);
|
services.value = services.value.filter(s => s.id !== serviceId);
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ import interactionPlugin from '@fullcalendar/interaction';
|
|||||||
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
|
import { CalendarOptions, EventClickArg } from '@fullcalendar/core';
|
||||||
import EventSidebar from './EventSidebar.vue';
|
import EventSidebar from './EventSidebar.vue';
|
||||||
import ServiceEditModal from '../../service/ServiceEditModal.vue';
|
import ServiceEditModal from '../../service/ServiceEditModal.vue';
|
||||||
import axios from 'axios';
|
import serviceService from '../../../services/serviceService';
|
||||||
import authHeader from '../../../services/auth.header';
|
import customerService from '../../../services/customerService';
|
||||||
|
|
||||||
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string;}
|
interface ServiceCall { id: number; scheduled_date: string; customer_id: number; customer_name: string; customer_address: string; customer_town: string; type_service_call: number; description: string; service_cost: string;}
|
||||||
interface Customer { id: number; customer_last_name: string; customer_first_name: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; customer_address: string; customer_home_type: number; customer_apt: string; }
|
interface Customer { id: number; customer_last_name: string; customer_first_name: string; customer_town: string; customer_state: number; customer_zip: string; customer_phone_number: string; customer_address: string; customer_home_type: number; customer_apt: string; }
|
||||||
@@ -108,58 +108,19 @@ const calendarOptions = ref<CalendarOptions>({
|
|||||||
});
|
});
|
||||||
const customer = ref<Customer | null>(null);
|
const customer = ref<Customer | null>(null);
|
||||||
|
|
||||||
// Watchers
|
// Functions (defined before watchers to avoid hoisting issues)
|
||||||
watch(() => route.params.id, (newId) => {
|
|
||||||
if (newId) getCustomer(newId as string);
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onMounted(() => {
|
|
||||||
fetchEvents();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Functions
|
|
||||||
|
|
||||||
const closeEditModal = () => {
|
const closeEditModal = () => {
|
||||||
selectedServiceForEdit.value = null;
|
selectedServiceForEdit.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
|
||||||
try {
|
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/update/${updatedService.id}`;
|
|
||||||
await axios.put(path, updatedService, { headers: authHeader(), withCredentials: true });
|
|
||||||
await fetchEvents();
|
|
||||||
closeEditModal();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save changes:", error);
|
|
||||||
alert("An error occurred while saving. Please check the console.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteService = async (serviceId: number) => {
|
|
||||||
try {
|
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/delete/${serviceId}`;
|
|
||||||
const response = await axios.delete(path, { withCredentials: true, headers: authHeader() });
|
|
||||||
if (response.data.ok === true) {
|
|
||||||
await fetchEvents();
|
|
||||||
closeEditModal();
|
|
||||||
} else {
|
|
||||||
console.error("Failed to delete event:", response.data.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting event:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCustomer = async (customerId: string): Promise<void> => {
|
const getCustomer = async (customerId: string): Promise<void> => {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
customer.value = null;
|
customer.value = null;
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/customer/${customerId}`;
|
const response = await customerService.getById(Number(customerId));
|
||||||
const response = await axios.get(path, { withCredentials: true, headers: authHeader() });
|
|
||||||
const customerData = response.data?.customer || response.data;
|
const customerData = response.data?.customer || response.data;
|
||||||
if (customerData && customerData.id) {
|
if (customerData && (customerData as any).id) {
|
||||||
customer.value = customerData;
|
customer.value = customerData as unknown as Customer;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("API call to get customer FAILED:", error);
|
console.error("API call to get customer FAILED:", error);
|
||||||
@@ -170,14 +131,39 @@ const getCustomer = async (customerId: string): Promise<void> => {
|
|||||||
|
|
||||||
const fetchEvents = async (): Promise<void> => {
|
const fetchEvents = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const path = `${import.meta.env.VITE_BASE_URL}/service/all`;
|
const response = await serviceService.getAll();
|
||||||
const response = await axios.get(path, { headers: authHeader(), withCredentials: true });
|
|
||||||
calendarOptions.value.events = response.data?.events || [];
|
calendarOptions.value.events = response.data?.events || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching all calendar events:", error);
|
console.error("Error fetching all calendar events:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveChanges = async (updatedService: ServiceCall) => {
|
||||||
|
try {
|
||||||
|
await serviceService.update(updatedService.id, updatedService);
|
||||||
|
await fetchEvents();
|
||||||
|
closeEditModal();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save changes:", error);
|
||||||
|
alert("An error occurred while saving. Please check the console.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteService = async (serviceId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await serviceService.delete(serviceId);
|
||||||
|
if (response.data.ok === true) {
|
||||||
|
await fetchEvents();
|
||||||
|
closeEditModal();
|
||||||
|
} else {
|
||||||
|
// console.error("Failed to delete event:", response.data.error);
|
||||||
|
// Error property might not exist on typed response, but checking ok is enough
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting event:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleEventScheduled = async (eventData: any): Promise<void> => {
|
const handleEventScheduled = async (eventData: any): Promise<void> => {
|
||||||
if (!customer.value) {
|
if (!customer.value) {
|
||||||
alert("Error: A customer must be loaded in the sidebar to create a new event.");
|
alert("Error: A customer must be loaded in the sidebar to create a new event.");
|
||||||
@@ -188,12 +174,13 @@ const handleEventScheduled = async (eventData: any): Promise<void> => {
|
|||||||
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
|
expected_delivery_date: eventData.start, type_service_call: eventData.type_service_call,
|
||||||
customer_id: customer.value.id, description: eventData.extendedProps.description,
|
customer_id: customer.value.id, description: eventData.extendedProps.description,
|
||||||
};
|
};
|
||||||
const path = import.meta.env.VITE_BASE_URL + "/service/create";
|
const response = await serviceService.create(payload);
|
||||||
const response = await axios.post(path, payload, { withCredentials: true, headers: authHeader() });
|
|
||||||
if (response.data.ok === true) {
|
// Service response has { ok: boolean, service: ServiceCall }
|
||||||
|
if (response.data.service) {
|
||||||
await fetchEvents();
|
await fetchEvents();
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to create event:", response.data.error);
|
console.error("Failed to create event");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating event:", error);
|
console.error("Error creating event:", error);
|
||||||
@@ -204,4 +191,14 @@ const handleEventDelete = async (eventId: string): Promise<void> => {
|
|||||||
// This is a simple alias now, as handleDeleteService is more specific
|
// This is a simple alias now, as handleDeleteService is more specific
|
||||||
await handleDeleteService(Number(eventId));
|
await handleDeleteService(Number(eventId));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watchers (after function definitions)
|
||||||
|
watch(() => route.params.id, (newId) => {
|
||||||
|
if (newId) getCustomer(newId as string);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
fetchEvents();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.VITE_BASE_URL;
|
|
||||||
|
|
||||||
function authHeader() {
|
|
||||||
// Return authorization header
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createEvent(payload) {
|
|
||||||
const path = `${BASE_URL}/service/create`; // Example endpoint
|
|
||||||
return axios.post(path, payload, {
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteEventById(eventId) {
|
|
||||||
const path = `${BASE_URL}/service/delete/${eventId}`; // Example endpoint
|
|
||||||
return axios.delete(path, {
|
|
||||||
withCredentials: true,
|
|
||||||
headers: authHeader(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -107,7 +107,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../services/auth.header'
|
import authHeader from '../../services/auth.header'
|
||||||
import Header from '../../layouts/headers/headerauth.vue'
|
import Header from '../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../layouts/footers/footer.vue'
|
|
||||||
import { notify } from "@kyvg/vue3-notification"
|
import { notify } from "@kyvg/vue3-notification"
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -116,7 +115,6 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -169,7 +169,6 @@ import axios from 'axios'
|
|||||||
import authHeader from '../../../services/auth.header'
|
import authHeader from '../../../services/auth.header'
|
||||||
import Header from '../../../layouts/headers/headerauth.vue'
|
import Header from '../../../layouts/headers/headerauth.vue'
|
||||||
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
import SideBar from '../../../layouts/sidebar/sidebar.vue'
|
||||||
import Footer from '../../../layouts/footers/footer.vue'
|
|
||||||
import {AuthorizeTransaction} from '../../../types/models'
|
import {AuthorizeTransaction} from '../../../types/models'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -178,7 +177,6 @@ export default defineComponent({
|
|||||||
components: {
|
components: {
|
||||||
Header,
|
Header,
|
||||||
SideBar,
|
SideBar,
|
||||||
Footer,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import api from './api';
|
import api from './api';
|
||||||
|
import { AxiosResponse, CustomersResponse } from '../types/models';
|
||||||
|
|
||||||
export const adminService = {
|
export const adminService = {
|
||||||
// Oil pricing
|
// Oil pricing
|
||||||
@@ -119,7 +120,7 @@ export const adminService = {
|
|||||||
yearlyProfit: () =>
|
yearlyProfit: () =>
|
||||||
api.get('/money/profit/year'),
|
api.get('/money/profit/year'),
|
||||||
|
|
||||||
customerListReport: () =>
|
customerListReport: (): Promise<AxiosResponse<CustomersResponse>> =>
|
||||||
api.get('/report/customers/list'),
|
api.get('/report/customers/list'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const autoApi = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Service/Maintenance System
|
||||||
|
const serviceApi = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_SERVICE_URL,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Request interceptor - add auth token
|
// Request interceptor - add auth token
|
||||||
function addAuthHeader(config: { headers: { Authorization?: string } }) {
|
function addAuthHeader(config: { headers: { Authorization?: string } }) {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
@@ -43,6 +49,8 @@ authorizeApi.interceptors.request.use(addAuthHeader as any);
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
autoApi.interceptors.request.use(addAuthHeader as any);
|
autoApi.interceptors.request.use(addAuthHeader as any);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
serviceApi.interceptors.request.use(addAuthHeader as any);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
axios.interceptors.request.use(addAuthHeader as any);
|
axios.interceptors.request.use(addAuthHeader as any);
|
||||||
|
|
||||||
// Response interceptor - unwrap standardized API responses
|
// Response interceptor - unwrap standardized API responses
|
||||||
@@ -86,7 +94,8 @@ function handleResponseError(error: unknown) {
|
|||||||
api.interceptors.response.use(unwrapResponse, handleResponseError);
|
api.interceptors.response.use(unwrapResponse, handleResponseError);
|
||||||
authorizeApi.interceptors.response.use(unwrapResponse, handleResponseError);
|
authorizeApi.interceptors.response.use(unwrapResponse, handleResponseError);
|
||||||
autoApi.interceptors.response.use(unwrapResponse, handleResponseError);
|
autoApi.interceptors.response.use(unwrapResponse, handleResponseError);
|
||||||
|
serviceApi.interceptors.response.use(unwrapResponse, handleResponseError);
|
||||||
axios.interceptors.response.use(unwrapResponse, handleResponseError);
|
axios.interceptors.response.use(unwrapResponse, handleResponseError);
|
||||||
|
|
||||||
export { api, authorizeApi, autoApi };
|
export { api, authorizeApi, autoApi, serviceApi };
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ export const deliveryService = {
|
|||||||
getIssues: (page: number = 1): Promise<AxiosResponse<DeliveriesResponse>> =>
|
getIssues: (page: number = 1): Promise<AxiosResponse<DeliveriesResponse>> =>
|
||||||
api.get(`/delivery/issue/${page}`),
|
api.get(`/delivery/issue/${page}`),
|
||||||
|
|
||||||
|
deleteCancelled: (id: number): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
|
api.delete(`/delivery/cancelled/${id}`),
|
||||||
|
|
||||||
|
// Totals
|
||||||
|
getWaitingTotals: (): Promise<AxiosResponse<{ totals: any[]; grand_total: number }>> =>
|
||||||
|
api.get('/deliverystatus/waiting-totals'),
|
||||||
|
|
||||||
|
getTodayTotals: (): Promise<AxiosResponse<{ totals: any[]; grand_total: number }>> =>
|
||||||
|
api.get('/deliverystatus/today-totals'),
|
||||||
|
|
||||||
|
getTomorrowTotals: (): Promise<AxiosResponse<{ totals: any[]; grand_total: number }>> =>
|
||||||
|
api.get('/deliverystatus/tomorrow-totals'),
|
||||||
|
|
||||||
// Status & totals
|
// Status & totals
|
||||||
updateStatus: (data: { id: number; status: number }): Promise<AxiosResponse<{ ok: boolean }>> =>
|
updateStatus: (data: { id: number; status: number }): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.put('/delivery/updatestatus', data),
|
api.put('/delivery/updatestatus', data),
|
||||||
|
|||||||
13
src/services/printService.ts
Normal file
13
src/services/printService.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import api from './api';
|
||||||
|
import { AxiosResponse } from '../types/models';
|
||||||
|
|
||||||
|
export const printService = {
|
||||||
|
printTicket: (id: number): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
|
api.delete(`/command/printticket/${id}`),
|
||||||
|
|
||||||
|
printTomorrow: (): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
|
api.delete('/command/printticket/print_tommorrow'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default printService;
|
||||||
@@ -1,62 +1,70 @@
|
|||||||
import api from './api';
|
import { serviceApi } from './api';
|
||||||
|
import {
|
||||||
|
AxiosResponse,
|
||||||
|
ServiceResponse,
|
||||||
|
ServicesResponse,
|
||||||
|
ServiceCall,
|
||||||
|
CalendarEventsResponse
|
||||||
|
} from '../types/models';
|
||||||
|
|
||||||
|
|
||||||
export const serviceService = {
|
export const serviceService = {
|
||||||
// CRUD operations
|
// CRUD operations
|
||||||
create: (data: any) =>
|
create: (data: any): Promise<AxiosResponse<ServiceResponse>> =>
|
||||||
api.post('/service/create', data),
|
serviceApi.post('/service/create', data),
|
||||||
|
|
||||||
getById: (id: number) =>
|
getById: (id: number): Promise<AxiosResponse<ServiceResponse>> =>
|
||||||
api.get(`/service/${id}`),
|
serviceApi.get(`/service/${id}`),
|
||||||
|
|
||||||
update: (id: number, data: any) =>
|
update: (id: number, data: any): Promise<AxiosResponse<ServiceResponse>> =>
|
||||||
api.put(`/service/update/${id}`, data),
|
serviceApi.put(`/service/update/${id}`, data),
|
||||||
|
|
||||||
delete: (id: number) =>
|
delete: (id: number): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.delete(`/service/delete/${id}`),
|
serviceApi.delete(`/service/delete/${id}`),
|
||||||
|
|
||||||
// List operations
|
// List operations
|
||||||
getAll: () =>
|
getAll: (): Promise<AxiosResponse<CalendarEventsResponse>> =>
|
||||||
api.get('/service/all'),
|
serviceApi.get('/service/all'),
|
||||||
|
|
||||||
getToday: () =>
|
getToday: (): Promise<AxiosResponse<ServicesResponse>> =>
|
||||||
api.get('/service/today'),
|
serviceApi.get('/service/today'),
|
||||||
|
|
||||||
getUpcoming: () =>
|
getUpcoming: (): Promise<AxiosResponse<ServicesResponse>> =>
|
||||||
api.get('/service/upcoming'),
|
serviceApi.get('/service/upcoming'),
|
||||||
|
|
||||||
getPast: () =>
|
getPast: (): Promise<AxiosResponse<ServicesResponse>> =>
|
||||||
api.get('/service/past'),
|
serviceApi.get('/service/past'),
|
||||||
|
|
||||||
getForCustomer: (customerId: number) =>
|
getForCustomer: (customerId: number): Promise<AxiosResponse<ServicesResponse>> =>
|
||||||
api.get(`/service/for-customer/${customerId}`),
|
serviceApi.get(`/service/for-customer/${customerId}`),
|
||||||
|
|
||||||
// Cost management
|
// Cost management
|
||||||
updateCost: (id: number, data: any) =>
|
updateCost: (id: number, data: any): Promise<AxiosResponse<{ ok: boolean; service: ServiceCall }>> =>
|
||||||
api.put(`/service/update-cost/${id}`, data),
|
serviceApi.put(`/service/update-cost/${id}`, data),
|
||||||
|
|
||||||
// Parts
|
// Parts
|
||||||
getPartsForCustomer: (customerId: number) =>
|
getPartsForCustomer: (customerId: number): Promise<AxiosResponse<{ ok: boolean; parts: any[] }>> =>
|
||||||
api.get(`/service/parts/customer/${customerId}`),
|
serviceApi.get(`/service/parts/customer/${customerId}`),
|
||||||
|
|
||||||
updateParts: (id: number, data: any) =>
|
updateParts: (id: number, data: any): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.put(`/service/parts/update/${id}`, data),
|
serviceApi.put(`/service/parts/update/${id}`, data),
|
||||||
|
|
||||||
// Service plans
|
// Service plans
|
||||||
plans: {
|
plans: {
|
||||||
getActive: () =>
|
getActive: (): Promise<AxiosResponse<{ ok: boolean; plans: any[] }>> =>
|
||||||
api.get('/service/plans/active'),
|
serviceApi.get('/service/plans/active'),
|
||||||
|
|
||||||
getForCustomer: (customerId: number) =>
|
getForCustomer: (customerId: number): Promise<AxiosResponse<{ ok: boolean; plan: any }>> =>
|
||||||
api.get(`/service/plans/customer/${customerId}`),
|
serviceApi.get(`/service/plans/customer/${customerId}`),
|
||||||
|
|
||||||
create: (data: any) =>
|
create: (data: any): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.post('/service/plans/create', data),
|
serviceApi.post('/service/plans/create', data),
|
||||||
|
|
||||||
update: (id: number, data: any) =>
|
update: (id: number, data: any): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.put(`/service/plans/update/${id}`, data),
|
serviceApi.put(`/service/plans/update/${id}`, data),
|
||||||
|
|
||||||
delete: (id: number) =>
|
delete: (id: number): Promise<AxiosResponse<{ ok: boolean }>> =>
|
||||||
api.delete(`/service/plans/delete/${id}`),
|
serviceApi.delete(`/service/plans/delete/${id}`),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
52
src/stores/theme.ts
Normal file
52
src/stores/theme.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// src/stores/theme.ts
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { ThemeOption } from '../types/models'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'user_theme'
|
||||||
|
const DEFAULT_THEME = 'ocean'
|
||||||
|
|
||||||
|
export const AVAILABLE_THEMES: ThemeOption[] = [
|
||||||
|
{ name: 'ocean', label: 'Ocean', preview: '#ff6600' },
|
||||||
|
{ name: 'forest', label: 'Forest', preview: '#4ade80' },
|
||||||
|
{ name: 'sunset', label: 'Sunset', preview: '#fb923c' },
|
||||||
|
{ name: 'arctic', label: 'Arctic', preview: '#06b6d4' },
|
||||||
|
{ name: 'midnight', label: 'Midnight', preview: '#a78bfa' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
|
// --- STATE ---
|
||||||
|
const currentTheme = ref(localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME)
|
||||||
|
|
||||||
|
// --- ACTIONS ---
|
||||||
|
function setTheme(theme: string) {
|
||||||
|
const validTheme = AVAILABLE_THEMES.find(t => t.name === theme)
|
||||||
|
if (validTheme) {
|
||||||
|
currentTheme.value = theme
|
||||||
|
localStorage.setItem(STORAGE_KEY, theme)
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTheme() {
|
||||||
|
// Validate stored theme is still valid
|
||||||
|
const storedTheme = localStorage.getItem(STORAGE_KEY)
|
||||||
|
const validTheme = AVAILABLE_THEMES.find(t => t.name === storedTheme)
|
||||||
|
if (validTheme) {
|
||||||
|
currentTheme.value = storedTheme!
|
||||||
|
} else {
|
||||||
|
currentTheme.value = DEFAULT_THEME
|
||||||
|
localStorage.setItem(STORAGE_KEY, DEFAULT_THEME)
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', currentTheme.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RETURN ---
|
||||||
|
return {
|
||||||
|
currentTheme,
|
||||||
|
setTheme,
|
||||||
|
initTheme,
|
||||||
|
AVAILABLE_THEMES,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -51,6 +51,7 @@ export interface PaginatedResponse<T> {
|
|||||||
|
|
||||||
// Customer interfaces
|
// Customer interfaces
|
||||||
export interface Customer extends BaseEntity {
|
export interface Customer extends BaseEntity {
|
||||||
|
user_id?: number;
|
||||||
auth_net_profile_id?: string;
|
auth_net_profile_id?: string;
|
||||||
account_number: string;
|
account_number: string;
|
||||||
customer_first_name: string;
|
customer_first_name: string;
|
||||||
@@ -71,6 +72,7 @@ export interface Customer extends BaseEntity {
|
|||||||
correct_address: boolean;
|
correct_address: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface CustomerDescription extends BaseEntity {
|
export interface CustomerDescription extends BaseEntity {
|
||||||
customer_id: number;
|
customer_id: number;
|
||||||
account_number: string;
|
account_number: string;
|
||||||
@@ -393,10 +395,12 @@ export interface UpdateDeliveryRequest {
|
|||||||
export interface CreateCardRequest {
|
export interface CreateCardRequest {
|
||||||
customer_id: number;
|
customer_id: number;
|
||||||
card_number: string;
|
card_number: string;
|
||||||
expiration_month: number;
|
expiration_month: number | string;
|
||||||
expiration_year: number;
|
expiration_year: number | string;
|
||||||
security_number: string;
|
security_number: string;
|
||||||
cardholder_name: string;
|
name_on_card: string;
|
||||||
|
type_of_card?: string;
|
||||||
|
main_card?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaymentRequest {
|
export interface PaymentRequest {
|
||||||
@@ -744,6 +748,7 @@ export interface CustomersResponse {
|
|||||||
export interface CustomerResponse {
|
export interface CustomerResponse {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
customer: Customer;
|
customer: Customer;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardsResponse {
|
export interface CardsResponse {
|
||||||
@@ -754,6 +759,7 @@ export interface CardsResponse {
|
|||||||
export interface CardResponse {
|
export interface CardResponse {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
card: CreditCard;
|
card: CreditCard;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeliveriesResponse {
|
export interface DeliveriesResponse {
|
||||||
@@ -764,6 +770,7 @@ export interface DeliveriesResponse {
|
|||||||
export interface DeliveryResponse {
|
export interface DeliveryResponse {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
delivery: Delivery;
|
delivery: Delivery;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TransactionsResponse {
|
export interface TransactionsResponse {
|
||||||
@@ -791,3 +798,25 @@ export type TransactionListResponse = TransactionsResponse;
|
|||||||
export type CardListResponse = CardsResponse;
|
export type CardListResponse = CardsResponse;
|
||||||
export type ServiceListResponse = PaginatedResponse<ServiceCall>;
|
export type ServiceListResponse = PaginatedResponse<ServiceCall>;
|
||||||
export type EmployeeListResponse = PaginatedResponse<Employee>;
|
export type EmployeeListResponse = PaginatedResponse<Employee>;
|
||||||
|
|
||||||
|
export interface ServicesResponse {
|
||||||
|
ok: boolean;
|
||||||
|
services: ServiceCall[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceResponse {
|
||||||
|
ok: boolean;
|
||||||
|
service: ServiceCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendarEventsResponse {
|
||||||
|
ok: boolean;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme interfaces
|
||||||
|
export interface ThemeOption {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
preview: string; // Primary color for preview swatch
|
||||||
|
}
|
||||||
127
src/utils/holidays.ts
Normal file
127
src/utils/holidays.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// src/utils/holidays.ts
|
||||||
|
// US Federal Holidays utility
|
||||||
|
|
||||||
|
export interface Holiday {
|
||||||
|
date: string; // YYYY-MM-DD format
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate US Federal Holidays for a given year
|
||||||
|
*/
|
||||||
|
export function getFederalHolidays(year: number): Holiday[] {
|
||||||
|
const holidays: Holiday[] = [];
|
||||||
|
|
||||||
|
// Fixed date holidays
|
||||||
|
holidays.push({ date: `${year}-01-01`, name: "New Year's Day" });
|
||||||
|
holidays.push({ date: `${year}-07-04`, name: "Independence Day" });
|
||||||
|
holidays.push({ date: `${year}-11-11`, name: "Veterans Day" });
|
||||||
|
holidays.push({ date: `${year}-12-25`, name: "Christmas Day" });
|
||||||
|
|
||||||
|
// Martin Luther King Jr. Day - 3rd Monday in January
|
||||||
|
holidays.push({ date: getNthWeekdayOfMonth(year, 0, 1, 3), name: "Martin Luther King Jr. Day" });
|
||||||
|
|
||||||
|
// Presidents' Day - 3rd Monday in February
|
||||||
|
holidays.push({ date: getNthWeekdayOfMonth(year, 1, 1, 3), name: "Presidents' Day" });
|
||||||
|
|
||||||
|
// Memorial Day - Last Monday in May
|
||||||
|
holidays.push({ date: getLastWeekdayOfMonth(year, 4, 1), name: "Memorial Day" });
|
||||||
|
|
||||||
|
// Juneteenth - June 19
|
||||||
|
holidays.push({ date: `${year}-06-19`, name: "Juneteenth" });
|
||||||
|
|
||||||
|
// Labor Day - 1st Monday in September
|
||||||
|
holidays.push({ date: getNthWeekdayOfMonth(year, 8, 1, 1), name: "Labor Day" });
|
||||||
|
|
||||||
|
// Columbus Day - 2nd Monday in October
|
||||||
|
holidays.push({ date: getNthWeekdayOfMonth(year, 9, 1, 2), name: "Columbus Day" });
|
||||||
|
|
||||||
|
// Thanksgiving - 4th Thursday in November
|
||||||
|
holidays.push({ date: getNthWeekdayOfMonth(year, 10, 4, 4), name: "Thanksgiving Day" });
|
||||||
|
|
||||||
|
return holidays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the nth occurrence of a weekday in a month
|
||||||
|
* @param year - Year
|
||||||
|
* @param month - Month (0-11)
|
||||||
|
* @param weekday - Day of week (0=Sunday, 1=Monday, etc.)
|
||||||
|
* @param n - Which occurrence (1=first, 2=second, etc.)
|
||||||
|
*/
|
||||||
|
function getNthWeekdayOfMonth(year: number, month: number, weekday: number, n: number): string {
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const firstWeekday = firstDay.getDay();
|
||||||
|
|
||||||
|
// Calculate days until the first occurrence of the target weekday
|
||||||
|
let daysUntilWeekday = (weekday - firstWeekday + 7) % 7;
|
||||||
|
|
||||||
|
// Calculate the date of the nth occurrence
|
||||||
|
const targetDate = 1 + daysUntilWeekday + (n - 1) * 7;
|
||||||
|
|
||||||
|
const date = new Date(year, month, targetDate);
|
||||||
|
return formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last occurrence of a weekday in a month
|
||||||
|
* @param year - Year
|
||||||
|
* @param month - Month (0-11)
|
||||||
|
* @param weekday - Day of week (0=Sunday, 1=Monday, etc.)
|
||||||
|
*/
|
||||||
|
function getLastWeekdayOfMonth(year: number, month: number, weekday: number): string {
|
||||||
|
// Start from the last day of the month and work backwards
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const lastDayOfWeek = lastDay.getDay();
|
||||||
|
|
||||||
|
// Calculate days to subtract to get to the target weekday
|
||||||
|
let daysToSubtract = (lastDayOfWeek - weekday + 7) % 7;
|
||||||
|
|
||||||
|
const date = new Date(year, month, lastDay.getDate() - daysToSubtract);
|
||||||
|
return formatDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date as YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
function formatDate(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get holidays for a date range
|
||||||
|
*/
|
||||||
|
export function getHolidaysForRange(startDate: Date, endDate: Date): Holiday[] {
|
||||||
|
const startYear = startDate.getFullYear();
|
||||||
|
const endYear = endDate.getFullYear();
|
||||||
|
|
||||||
|
const allHolidays: Holiday[] = [];
|
||||||
|
|
||||||
|
for (let year = startYear; year <= endYear; year++) {
|
||||||
|
allHolidays.push(...getFederalHolidays(year));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter to only holidays in the date range
|
||||||
|
return allHolidays.filter(holiday => {
|
||||||
|
const holidayDate = new Date(holiday.date);
|
||||||
|
return holidayDate >= startDate && holidayDate <= endDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a date is a federal holiday
|
||||||
|
*/
|
||||||
|
export function isHoliday(date: string, holidays: Holiday[]): boolean {
|
||||||
|
return holidays.some(holiday => holiday.date === date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get holiday name for a date
|
||||||
|
*/
|
||||||
|
export function getHolidayName(date: string, holidays: Holiday[]): string | null {
|
||||||
|
const holiday = holidays.find(h => h.date === date);
|
||||||
|
return holiday ? holiday.name : null;
|
||||||
|
}
|
||||||
@@ -3,20 +3,83 @@ module.exports = {
|
|||||||
presets: [],
|
presets: [],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
daisyui: {
|
daisyui: {
|
||||||
// themes: ["dracula" ],
|
themes: [
|
||||||
themes: [{
|
{
|
||||||
mytheme: {
|
ocean: {
|
||||||
"primary": "#010409",
|
"primary": "#010409",
|
||||||
"secondary": "#161B22",
|
"secondary": "#161B22",
|
||||||
"accent": "#ff6600",
|
"accent": "#ff6600",
|
||||||
"neutral": "#21262C",
|
"neutral": "#21262C",
|
||||||
"base-100": "#0D1117",
|
"base-100": "#0D1117",
|
||||||
|
"base-200": "#161B22",
|
||||||
|
"base-300": "#21262C",
|
||||||
"info": "#74a0d5",
|
"info": "#74a0d5",
|
||||||
"success": "#33cc33",
|
"success": "#33cc33",
|
||||||
"warning": "#97520C",
|
"warning": "#97520C",
|
||||||
"error": "#da0e0e",
|
"error": "#da0e0e",
|
||||||
},
|
},
|
||||||
},],
|
},
|
||||||
|
{
|
||||||
|
forest: {
|
||||||
|
"primary": "#1a472a",
|
||||||
|
"secondary": "#2d5a3d",
|
||||||
|
"accent": "#4ade80",
|
||||||
|
"neutral": "#1e3a2f",
|
||||||
|
"base-100": "#0f1f14",
|
||||||
|
"base-200": "#162a1c",
|
||||||
|
"base-300": "#1e3a2f",
|
||||||
|
"info": "#67e8f9",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"warning": "#eab308",
|
||||||
|
"error": "#ef4444",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sunset: {
|
||||||
|
"primary": "#44403c",
|
||||||
|
"secondary": "#57534e",
|
||||||
|
"accent": "#fb923c",
|
||||||
|
"neutral": "#292524",
|
||||||
|
"base-100": "#1c1917",
|
||||||
|
"base-200": "#292524",
|
||||||
|
"base-300": "#44403c",
|
||||||
|
"info": "#38bdf8",
|
||||||
|
"success": "#4ade80",
|
||||||
|
"warning": "#fbbf24",
|
||||||
|
"error": "#f87171",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arctic: {
|
||||||
|
"primary": "#3b82f6",
|
||||||
|
"secondary": "#64748b",
|
||||||
|
"accent": "#06b6d4",
|
||||||
|
"neutral": "#e2e8f0",
|
||||||
|
"base-100": "#f8fafc",
|
||||||
|
"base-200": "#f1f5f9",
|
||||||
|
"base-300": "#e2e8f0",
|
||||||
|
"info": "#0ea5e9",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"warning": "#f59e0b",
|
||||||
|
"error": "#ef4444",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
midnight: {
|
||||||
|
"primary": "#312e81",
|
||||||
|
"secondary": "#3730a3",
|
||||||
|
"accent": "#a78bfa",
|
||||||
|
"neutral": "#1e1b4b",
|
||||||
|
"base-100": "#0f0a1f",
|
||||||
|
"base-200": "#1e1b4b",
|
||||||
|
"base-300": "#312e81",
|
||||||
|
"info": "#818cf8",
|
||||||
|
"success": "#34d399",
|
||||||
|
"warning": "#fbbf24",
|
||||||
|
"error": "#f87171",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
plugins: [require('daisyui')],
|
plugins: [require('daisyui')],
|
||||||
theme: {
|
theme: {
|
||||||
@@ -129,6 +192,10 @@ module.exports = {
|
|||||||
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
|
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||||
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
bounce: 'bounce 1s infinite',
|
bounce: 'bounce 1s infinite',
|
||||||
|
'fade-in': 'fadeIn 0.3s ease-in',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'slide-down': 'slideDown 0.3s ease-out',
|
||||||
|
'scale-in': 'scaleIn 0.2s ease-out',
|
||||||
},
|
},
|
||||||
aspectRatio: {
|
aspectRatio: {
|
||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
@@ -405,27 +472,26 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [
|
sans: [
|
||||||
'Arial',
|
'Inter',
|
||||||
'ui-sans-serif',
|
|
||||||
'system-ui',
|
|
||||||
'-apple-system',
|
'-apple-system',
|
||||||
'BlinkMacSystemFont',
|
'BlinkMacSystemFont',
|
||||||
'"Segoe UI"',
|
'"Segoe UI"',
|
||||||
'Roboto',
|
'Roboto',
|
||||||
'"Helvetica Neue"',
|
'"Helvetica Neue"',
|
||||||
'"Noto Sans"',
|
'Arial',
|
||||||
'sans-serif',
|
'sans-serif',
|
||||||
'"Apple Color Emoji"',
|
'"Apple Color Emoji"',
|
||||||
'"Segoe UI Emoji"',
|
'"Segoe UI Emoji"',
|
||||||
'"Segoe UI Symbol"',
|
'"Segoe UI Symbol"',
|
||||||
'"Noto Color Emoji"',
|
|
||||||
],
|
],
|
||||||
serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
|
serif: ['ui-serif', 'Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
|
||||||
mono: [
|
mono: [
|
||||||
|
'SF Mono',
|
||||||
|
'Monaco',
|
||||||
|
'Cascadia Code',
|
||||||
'ui-monospace',
|
'ui-monospace',
|
||||||
'SFMono-Regular',
|
'SFMono-Regular',
|
||||||
'Menlo',
|
'Menlo',
|
||||||
'Monaco',
|
|
||||||
'Consolas',
|
'Consolas',
|
||||||
'"Liberation Mono"',
|
'"Liberation Mono"',
|
||||||
'"Courier New"',
|
'"Courier New"',
|
||||||
@@ -670,6 +736,22 @@ module.exports = {
|
|||||||
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
|
animationTimingFunction: 'cubic-bezier(0,0,0.2,1)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
'0%': { transform: 'translateY(-10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
scaleIn: {
|
||||||
|
'0%': { transform: 'scale(0.95)', opacity: '0' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
letterSpacing: {
|
letterSpacing: {
|
||||||
tighter: '-0.05em',
|
tighter: '-0.05em',
|
||||||
|
|||||||
Reference in New Issue
Block a user