()
+ for (const [dateStr, gallons] of dailyMap.entries()) {
+ const d = new Date(dateStr + 'T12:00:00') // noon avoids DST/timezone edge cases
+ const daysToSat = (6 - d.getDay() + 7) % 7
+ const sat = new Date(d)
+ sat.setDate(d.getDate() + daysToSat)
+ const satKey = toLocalDateStr(sat)
+ weekMap.set(satKey, (weekMap.get(satKey) || 0) + gallons)
+ }
+
+ // Generate every Saturday in the 4-month window
+ const saturdays: string[] = []
+ const cur = new Date(start)
+ cur.setHours(12, 0, 0, 0)
+ cur.setDate(cur.getDate() + (6 - cur.getDay() + 7) % 7)
+ const endNoon = new Date(end)
+ endNoon.setHours(12, 0, 0, 0)
+ while (cur <= endNoon) {
+ saturdays.push(toLocalDateStr(cur))
+ cur.setDate(cur.getDate() + 7)
+ }
+
+ weeklyData.value = saturdays.map(sat => ({
+ date: sat,
+ gallons: weekMap.get(sat) || 0
+ }))
} catch (err) {
console.error('Error fetching chart data:', err)
}
diff --git a/src/pages/admin/routes.ts b/src/pages/admin/routes.ts
index 1168666..9c064c6 100755
--- a/src/pages/admin/routes.ts
+++ b/src/pages/admin/routes.ts
@@ -8,6 +8,7 @@ const PromoEdit = () => import('../admin/promo/edit.vue');
const StatsHome = () => import('../admin/stats/StatsHome.vue');
const SettingsPage = () => import('../admin/settings/SettingsPage.vue');
+const StreetManager = () => import('../admin/streets.vue');
const adminRoutes = [
@@ -43,6 +44,11 @@ const adminRoutes = [
name: 'settings',
component: SettingsPage,
},
+ {
+ path: '/streets',
+ name: 'streetManager',
+ component: StreetManager,
+ },
]
diff --git a/src/pages/admin/streets.vue b/src/pages/admin/streets.vue
new file mode 100644
index 0000000..e63520b
--- /dev/null
+++ b/src/pages/admin/streets.vue
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+ Home
+ Street Manager
+
+
+
+
+
+
+
+ Street Manager
+
+
Manage street reference data used for address autocomplete. Update re-fetches streets from OpenStreetMap.
+
+
+
+
+
+
+
+
Address checker service is unavailable. Streets cannot be updated.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No towns yet
+
Add a town above to get started.
+
+
+
+
+
+
+
+ Town
+ State
+ Streets
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+ {{ town.town }}
+
+
+
+ {{ town.state }}
+
+ {{ town.street_count }}
+
+
+
+
+
+
+
+
+ Save
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/automatic/home.vue b/src/pages/automatic/home.vue
index 10f8e46..2aa42c2 100755
--- a/src/pages/automatic/home.vue
+++ b/src/pages/automatic/home.vue
@@ -89,12 +89,6 @@
{{ sortAsc ? '▲' : '▼' }}
⇵
-
- Confidence
- {{ sortAsc ? '▲' : '▼' }}
- ⇵
-
Days Left
@@ -143,11 +137,6 @@
{{ Number(oil.house_factor).toFixed(4) }}
-
-
- {{ oil.confidence_score ?? 20 }}
-
-
N/A
@@ -225,12 +214,6 @@
Hot Water Tank
{{ oil.hot_water_summer ? 'Yes' : 'No' }}
-
-
Confidence
-
- {{ oil.confidence_score ?? 20 }}
-
-
Days Remaining
N/A
diff --git a/src/pages/customer/alert.vue b/src/pages/customer/alert.vue
new file mode 100644
index 0000000..56b797d
--- /dev/null
+++ b/src/pages/customer/alert.vue
@@ -0,0 +1,292 @@
+
+
+
+
+
+
+ Customers
+
+
+ {{ customer.customer_first_name }} {{ customer.customer_last_name }}
+
+
+ Alert
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
Customer Alert
+
+ {{ customer.customer_first_name }} {{ customer.customer_last_name }} — #{{ customer.account_number }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ existingAlert ? 'Edit Alert' : 'Add Alert' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ existingAlert ? 'Update Alert' : 'Create Alert' }}
+
+
+ Delete Alert
+
+
+ Cancel
+
+
+
+
+
+
+
Preview
+
This is how the popup will appear on the customer profile.
+
+
+
+
+
{{ form.message || 'Your alert message will appear here...' }}
+
+
+
+
+
+
+ 🚨
+ NOTICE
+ 🚨
+
+
✕
+
+
+
+ {{ form.message || 'Your alert message will appear here...' }}
+
+
+
+
+
+
+
+
+
+ {{ form.message || 'Your alert message will appear here...' }}
+
+
+
+
+ Type confirm to acknowledge
+
+
+
+ Close
+
+
+
+
+
+
+
Alert created: {{ formatDate(existingAlert.created_at) }}
+
+
+
+
+
+
+
+
Delete Alert?
+
This will remove the alert for this customer. It will no longer appear when their profile is opened.
+
+
+
+ Delete
+
+ Cancel
+
+
+
+
+
+
+
+
diff --git a/src/pages/customer/create.vue b/src/pages/customer/create.vue
index 0a0ec2e..2373ddf 100755
--- a/src/pages/customer/create.vue
+++ b/src/pages/customer/create.vue
@@ -115,6 +115,16 @@
>
No matching towns found. You can enter a new town name.
+
+
+ {{ town }}
+
{{ v$.CreateCustomerForm.customer_town.$errors[0].$message }}
@@ -252,6 +262,10 @@ const custList = ref([])
const selectedTown = ref(null)
const zipAutoFilled = ref(false)
+const quickTowns = ['Auburn', 'Millbury', 'Sutton', 'Oxford', 'North Oxford', 'Webster',
+ 'Grafton', 'Dudley', 'Charlton', 'Leicester', 'Cherry Valley', 'Rochdale',
+ 'Paxton', 'Spencer', 'Worcester']
+
// State ID to abbreviation mapping (matches backend STATE_MAPPING)
const STATE_ABBR_MAP: Record = {
0: 'MA', // Default for unmapped
@@ -344,6 +358,16 @@ const selectTown = (suggestion: TownSuggestion) => {
zipAutoFilled.value = false
}
+const selectQuickTown = (townName: string) => {
+ CreateCustomerForm.value.customer_town = townName
+ CreateCustomerForm.value.customer_state = 0 // MA
+ selectedTown.value = { town: townName, state: 'MA', state_id: 0, customer_count: 0 }
+ showTownDropdown.value = false
+ CreateCustomerForm.value.customer_address = ''
+ CreateCustomerForm.value.customer_zip = ''
+ zipAutoFilled.value = false
+}
+
// Helper to get current town and state for street search
const getCurrentTownState = () => {
// If a town was selected from dropdown, use that
diff --git a/src/pages/customer/edit.vue b/src/pages/customer/edit.vue
index 351d4c9..d616065 100755
--- a/src/pages/customer/edit.vue
+++ b/src/pages/customer/edit.vue
@@ -227,7 +227,7 @@
Fill Location
-
+
@@ -497,7 +497,7 @@ const userStatus = () => {
const getCustomerDescription = (userid: number) => {
customerService.getDescription(userid).then((response: any) => {
- customerDescription.value = response.data?.description || response.data
+ customerDescription.value = response.data
CreateCustomerForm.value.basicInfo.customer_description = customerDescription.value.description;
CreateCustomerForm.value.basicInfo.customer_fill_location = customerDescription.value.fill_location
})
diff --git a/src/pages/customer/profile/TankEstimation.vue b/src/pages/customer/profile/TankEstimation.vue
index aa75ffa..b9bba55 100644
--- a/src/pages/customer/profile/TankEstimation.vue
+++ b/src/pages/customer/profile/TankEstimation.vue
@@ -48,37 +48,42 @@
-
-
-
Estimation Confidence
-
-
-
{{ estimation.confidence_score }}%
+
+
+
+
Days Until Empty
+
+ N/A
+ {{ estimation.days_remaining }} days
+
-
- Source:
{{ estimation.k_factor_source }}
+
+
Last Fill
+
+ {{ formatDate(estimation.last_fill) }}
+ N/A
+
-
-
-
Days Until Empty
-
-
N/A
-
{{ estimation.days_remaining }} days
+
+
+
+
Gallons to Fill
+
+ {{ Math.max(0, getMaxFill(estimation.tank_size) - Math.round(estimation.estimated_gallons)) }} gal
+
+
+
+
Days Since Last Fill
+
+ {{ daysSinceLastFill(estimation.last_fill) }} days
+ N/A
+
@@ -97,8 +102,8 @@
-
-
+
+
Adjust Usage Factor (K-Factor)
0.01
@@ -169,6 +174,7 @@ interface FuelEstimation {
// Props
const props = defineProps<{
customerId: number
+ currentUserId: number
}>()
// Reactive data
@@ -287,6 +293,18 @@ const fetchEstimation = async () => {
}
}
+const TANK_MAX_FILLS: Record
= {
+ 275: 240,
+ 320: 275,
+ 330: 280,
+ 500: 475,
+ 550: 500,
+}
+
+const getMaxFill = (tankSize: number): number => {
+ return TANK_MAX_FILLS[Math.round(tankSize)] ?? Math.round(tankSize * 0.9)
+}
+
const getTankLevelPercentage = (): number => {
if (!estimation.value || !estimation.value.tank_size || estimation.value.tank_size === 0) {
return 0
@@ -347,4 +365,8 @@ const formatDate = (dateString: string): string => {
if (!dateString) return 'N/A'
return dayjs(dateString).format('MMM D, YYYY')
}
+
+const daysSinceLastFill = (dateString: string): number => {
+ return dayjs().diff(dayjs(dateString), 'day')
+}
diff --git a/src/pages/customer/profile/profile.vue b/src/pages/customer/profile/profile.vue
index 37baa6e..8ac66e6 100755
--- a/src/pages/customer/profile/profile.vue
+++ b/src/pages/customer/profile/profile.vue
@@ -46,7 +46,9 @@
+ :customer_description="customer_description.description"
+ :has-alert="!!customerAlert"
+ @toggle-automatic="userAutomatic" />
@@ -101,7 +103,7 @@
-
+
@@ -145,6 +147,9 @@
+
+
+
@@ -271,6 +276,7 @@ import CreditCards from './profile/CreditCards.vue';
import CustomerComments from './profile/CustomerComments.vue';
import HistoryTabs from './profile/HistoryTabs.vue';
import TankEstimation from './TankEstimation.vue';
+import CustomerAlertPopup from './profile/CustomerAlertPopup.vue';
import { AuthorizeTransaction, PricingData, CustomerDescriptionData, CustomersResponse, CustomerResponse, AxiosResponse, AxiosError } from '../../../types/models';
L.Icon.Default.mergeOptions({
@@ -376,6 +382,7 @@ const isCreateAccountModalVisible = ref(false)
const isCreatingAccount = ref(false)
const createdProfileId = ref('')
const isDuplicateErrorModalVisible = ref(false) // Add for duplicate detection popup
+const customerAlert = ref<{ id: number; severity: number; message: string } | null>(null)
const pricing = ref
({
price_from_supplier: 0,
price_for_customer: 0,
@@ -453,6 +460,7 @@ const getCustomer = (userid: number) => {
loadServicePlan(customer.value.id);
getCustomerTransactions(customer.value.id);
checkAuthorizeAccount();
+ getCustomerAlert(customer.value.id);
}).catch((err: unknown) => {
const error = err as AxiosError;
@@ -521,6 +529,14 @@ const getNozzleColor = (nozzleString: string): string => {
}
}
+const getCustomerAlert = (customerId: number) => {
+ customerService.getAlert(customerId).then((response: AxiosResponse) => {
+ customerAlert.value = response.data?.alert || null
+ }).catch(() => {
+ customerAlert.value = null
+ })
+}
+
const getCustomerLastDelivery = (userid: number) => {
adminService.stats.userLastDelivery(userid).then((response: AxiosResponse) => {
customer_last_delivery.value = response.data.date
@@ -695,8 +711,8 @@ const handleDeleteService = async (serviceId: number) => {
const fetchCustomerParts = async (customerId: number) => {
try {
const response = await serviceService.getPartsForCustomer(customerId);
- if (response.data && 'parts' in response.data && Array.isArray(response.data.parts) && response.data.parts.length > 0) {
- currentParts.value = response.data.parts[0];
+ if (response.data && response.data.parts && typeof response.data.parts === 'object' && !Array.isArray(response.data.parts)) {
+ currentParts.value = response.data.parts as ServiceParts;
} else {
currentParts.value = null;
}
diff --git a/src/pages/customer/profile/profile/CustomerAlertPopup.vue b/src/pages/customer/profile/profile/CustomerAlertPopup.vue
new file mode 100644
index 0000000..e15c257
--- /dev/null
+++ b/src/pages/customer/profile/profile/CustomerAlertPopup.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
✕
+
+
{{ alert.message }}
+
+
+
+
+
+
+
✕
+
+ 🚨
+
NOTICE
+ 🚨
+
+
+
+
+
+
+
+
+
+
+
+
+ Type confirm to acknowledge
+
+
+
+
+ Close
+
+
+
+
+
+
+
+
diff --git a/src/pages/customer/profile/profile/ProfileMap.vue b/src/pages/customer/profile/profile/ProfileMap.vue
index c0231a9..8a084bc 100644
--- a/src/pages/customer/profile/profile/ProfileMap.vue
+++ b/src/pages/customer/profile/profile/ProfileMap.vue
@@ -4,8 +4,21 @@
{{ customer.account_number }}
-
-
+
+
+
+
+
+
{{ customer.customer_first_name }} {{ customer.customer_last_name }}
+
{{ customer.customer_address }}, {{ customer.customer_apt }}
+
{{ customer.customer_town }}, {{ customer.customer_zip }}
+
+
+
@@ -13,21 +26,28 @@
\ No newline at end of file
+
+const center = computed<[number, number]>(() => [
+ parseFloat(props.customer.customer_latitude as string),
+ parseFloat(props.customer.customer_longitude as string),
+]);
+
diff --git a/src/pages/customer/profile/profile/ProfileSummary.vue b/src/pages/customer/profile/profile/ProfileSummary.vue
index 54b8255..b4df480 100644
--- a/src/pages/customer/profile/profile/ProfileSummary.vue
+++ b/src/pages/customer/profile/profile/ProfileSummary.vue
@@ -47,6 +47,15 @@
{{ automatic_status === 1 ? 'Set to Will Call' : 'Set to Auto' }}
+
+
+
+
+
+
+ {{ hasAlert ? 'Alert Active' : 'Alert' }}
+
@@ -119,6 +128,7 @@ interface Props {
customer: Customer;
automatic_status: number;
customer_description?: string;
+ hasAlert?: boolean;
}
const props = defineProps
();
diff --git a/src/pages/customer/routes.ts b/src/pages/customer/routes.ts
index 7840d3d..e3c0dd1 100755
--- a/src/pages/customer/routes.ts
+++ b/src/pages/customer/routes.ts
@@ -5,6 +5,7 @@ const CustomerCreate = () => import('../customer/create.vue');
const CustomerEdit = () => import("../customer/edit.vue");
const CustomerProfile = () => import("./profile/profile.vue")
const TankEdit = () => import("./tank/edit.vue")
+const CustomerAlert = () => import("./alert.vue")
const customerRoutes = [
@@ -44,6 +45,11 @@ const customerRoutes = [
name: 'customerList',
component: () => import('./list.vue'),
},
+ {
+ path: '/customer/:id/alert',
+ name: 'customerAlert',
+ component: CustomerAlert,
+ },
]
export default customerRoutes
diff --git a/src/pages/delivery/map.vue b/src/pages/delivery/map.vue
index 480ccb1..6a34186 100644
--- a/src/pages/delivery/map.vue
+++ b/src/pages/delivery/map.vue
@@ -13,16 +13,26 @@
-
+
Delivery Map
-
@@ -70,7 +80,7 @@
No deliveries found
-
No deliveries scheduled for {{ formatDate(selectedDate) }}
+
{{ showAll ? 'No eligible deliveries found' : `No deliveries scheduled for ${formatDate(selectedDate)}` }}
@@ -97,7 +107,7 @@
{{ delivery.customerName }}
{{ delivery.street }}
-
{{ delivery.town }}, {{ delivery.state }} {{ delivery.zipcode }}
+
{{ delivery.town }}, {{ STATE_ID_TO_ABBR[Number(delivery.state)] || delivery.state }} {{ delivery.zipcode }}
FILL
{{ delivery.gallonsOrdered }} gallons
@@ -141,11 +151,38 @@ import { deliveryService } from '../../services/deliveryService';
import { DeliveryMapItem } from '../../types/models';
import { STATE_ID_TO_ABBR } from '../../constants/states';
+// Date helpers
+const dateButtons = [
+ { label: 'Yesterday', offset: -1 },
+ { label: 'Today', offset: 0 },
+ { label: 'Tomorrow', offset: 1 },
+ { label: '+2 Days', offset: 2 },
+ { label: '+3 Days', offset: 3 },
+];
+
+const dateForOffset = (offset: number): string => {
+ const d = new Date();
+ d.setDate(d.getDate() + offset);
+ return d.toISOString().split('T')[0];
+};
+
+const selectDay = (offset: number) => {
+ showAll.value = false;
+ selectedDate.value = dateForOffset(offset);
+ fetchDeliveries();
+};
+
+const selectAll = () => {
+ showAll.value = true;
+ fetchDeliveries();
+};
+
// State
const loading = ref(false);
const error = ref(null);
const deliveries = ref([]);
-const selectedDate = ref(new Date().toISOString().split('T')[0]);
+const selectedDate = ref(dateForOffset(0));
+const showAll = ref(false);
const zoom = ref(11);
// Computed
@@ -221,7 +258,7 @@ const fetchDeliveries = async () => {
loading.value = true;
error.value = null;
try {
- const response = await deliveryService.getForMap(selectedDate.value);
+ const response = await deliveryService.getForMap(selectedDate.value, showAll.value);
if (response.data.ok) {
deliveries.value = response.data.deliveries || [];
} else {
diff --git a/src/pages/delivery/update_tickets/finalize_ticket_auto.vue b/src/pages/delivery/update_tickets/finalize_ticket_auto.vue
index a093a70..c1d881d 100644
--- a/src/pages/delivery/update_tickets/finalize_ticket_auto.vue
+++ b/src/pages/delivery/update_tickets/finalize_ticket_auto.vue
@@ -103,7 +103,10 @@
- Finalize Delivery
+
+
+ {{ submitting ? 'Finalizing...' : 'Finalize Delivery' }}
+
@@ -141,6 +144,7 @@ const router = useRouter()
// Reactive data
const v$ = useValidate()
const loaded = ref(false)
+const submitting = ref(false)
const user = ref({
id: 0
})
@@ -507,20 +511,24 @@ const CreateTransaction = (auto_ticket_id: string) => {
})
}
-const onSubmit = () => {
- let payload = {
- gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
- };
- UpdateDeliveredAuto(payload);
- if (autoTicket.value.payment_status == '1') {
- // Pre-authorized: redirect to capture page
- router.push({ name: "payAutoCapture", params: { id: autoTicket.value.id } });
- } else {
- // Fully charged: close ticket
- if (autoDelivery.value.open_ticket_id) {
- closeTicket(autoDelivery.value.open_ticket_id);
+const onSubmit = async () => {
+ if (submitting.value) return;
+ submitting.value = true;
+ try {
+ const payload = {
+ gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
+ };
+ await UpdateDeliveredAuto(payload);
+ if (autoTicket.value.payment_status == '1') {
+ router.push({ name: "payAutoCapture", params: { id: autoTicket.value.id } });
+ } else {
+ if (autoDelivery.value.open_ticket_id) {
+ await closeTicket(autoDelivery.value.open_ticket_id);
+ }
+ router.push({ name: "auto" });
}
- router.push({ name: "auto" });
+ } finally {
+ submitting.value = false;
}
}
diff --git a/src/pages/delivery/update_tickets/finalize_ticket_auto_nocc.vue b/src/pages/delivery/update_tickets/finalize_ticket_auto_nocc.vue
index bc6c85e..5453ad7 100644
--- a/src/pages/delivery/update_tickets/finalize_ticket_auto_nocc.vue
+++ b/src/pages/delivery/update_tickets/finalize_ticket_auto_nocc.vue
@@ -105,7 +105,10 @@
- Finalize Delivery
+
+
+ {{ submitting ? 'Finalizing...' : 'Finalize Delivery' }}
+
@@ -143,6 +146,7 @@ const router = useRouter()
// Reactive data
const v$ = useValidate()
const loaded = ref(false)
+const submitting = ref(false)
const user = ref({
id: 0
})
@@ -475,13 +479,18 @@ const ConfirmAuto = async (payload: {
}
}
-const onSubmit = () => {
- let payload = {
- gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
- };
-
- ConfirmAuto(payload)
- router.push({ name: "auto" });
+const onSubmit = async () => {
+ if (submitting.value) return;
+ submitting.value = true;
+ try {
+ const payload = {
+ gallons_delivered: FinalizeOilOrderForm.value.gallons_delivered,
+ };
+ await ConfirmAuto(payload);
+ router.push({ name: "auto" });
+ } finally {
+ submitting.value = false;
+ }
}
diff --git a/src/pages/delivery/viewstatus/cancelled.vue b/src/pages/delivery/viewstatus/cancelled.vue
index e77a11c..24cbd07 100755
--- a/src/pages/delivery/viewstatus/cancelled.vue
+++ b/src/pages/delivery/viewstatus/cancelled.vue
@@ -89,8 +89,8 @@
{{ oil.customer_address }}
{{ oil.customer_address }}
@@ -232,7 +232,7 @@
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/pages/service/calender/EventSidebar.vue b/src/pages/service/calender/EventSidebar.vue
index 26fab75..0ccc06e 100644
--- a/src/pages/service/calender/EventSidebar.vue
+++ b/src/pages/service/calender/EventSidebar.vue
@@ -10,12 +10,6 @@
Schedule Service
@@ -78,7 +82,7 @@
diff --git a/src/pages/stats/DailyDeliveriesGraph.vue b/src/pages/stats/DailyDeliveriesGraph.vue
index d77b139..e8bbcb2 100644
--- a/src/pages/stats/DailyDeliveriesGraph.vue
+++ b/src/pages/stats/DailyDeliveriesGraph.vue
@@ -1,54 +1,24 @@
-
-
-
-
- Time Range
-
-
-
-
-
- Compare Years
-
-
-
+
+
+
+ Compare Years
+
+
-
-
+
-
+
-
+
+
+
+ No data available for the selected years.
+
+
-
+
-
-
{{ yearData.year }} Total
-
- {{ yearData.totalGallons.toLocaleString() }}
+
+
{{ stat.year }} Total
+
+ {{ stat.totalGallons.toLocaleString() }}
-
{{ yearData.totalDeliveries }} deliveries
+
{{ stat.totalDeliveries }} deliveries
@@ -81,202 +54,164 @@
diff --git a/src/pages/stats/routes.ts b/src/pages/stats/routes.ts
index d1238ae..fd29a83 100644
--- a/src/pages/stats/routes.ts
+++ b/src/pages/stats/routes.ts
@@ -1,6 +1,7 @@
const StatsLayout = () => import('./StatsLayout.vue');
const DailyDeliveriesGraph = () => import('./DailyDeliveriesGraph.vue');
const TotalsComparison = () => import('./TotalsComparison.vue');
+const CustomerSignupsGraph = () => import('./CustomerSignupsGraph.vue');
const statsRoutes = [
{
@@ -20,6 +21,11 @@ const statsRoutes = [
path: 'totals',
name: 'statsTotals',
component: TotalsComparison
+ },
+ {
+ path: 'customers',
+ name: 'statsCustomers',
+ component: CustomerSignupsGraph
}
]
}
diff --git a/src/pages/ticket/ticket.vue b/src/pages/ticket/ticket.vue
index 4e4a812..50246f5 100644
--- a/src/pages/ticket/ticket.vue
+++ b/src/pages/ticket/ticket.vue
@@ -32,15 +32,16 @@
-
+
{{ customer_description.description }}
Promo: {{ promo_text }}
-
-
PRIME
-
SAME DAY
-
EMERGENCY
+
+ PRIME
+ SAME DAY
+ EMERGENCY
+
CASH
Credit Card
@@ -74,15 +75,12 @@
01501
-
508 426 8800
-
- {{ past_delivery.when_delivered }} - {{ past_delivery.gallons_delivered }}
-
+ :key="past_delivery.when_delivered">
+ {{ past_delivery.automatic == 1 ? 'a' : 'wc' }}-{{ past_delivery.when_delivered }} - {{ past_delivery.gallons_delivered }}
@@ -127,6 +125,9 @@
+
+ This heating oil is dyed and sold solely for home heating use only — not for on-road vehicle use or taxable purposes. Not responsible for driveways damaged from delivery.
+
@@ -143,9 +144,9 @@ import { adminService } from '../../services/adminService'
import { queryService } from '../../services/queryService'
interface PastDelivery {
- id: number
- gallons_delivered: number
when_delivered: string
+ gallons_delivered: number
+ automatic: number
}
const route = useRoute()
@@ -267,7 +268,7 @@ async function getCustomerTank(userId: number) {
async function getCustomerDescription(userId: number) {
try {
const response = await customerService.getDescription(userId)
- customer_description.value = (response.data as any)?.description || response.data
+ customer_description.value = response.data
} catch (error) {
console.error('Failed to fetch description:', error)
}
@@ -300,8 +301,8 @@ async function getTodayPrice() {
async function getPastDeliveries(userId: number) {
try {
- const response = await deliveryService.getByCustomer(userId)
- past_deliveries.value = (response.data as any)?.deliveries || response.data || []
+ const response = await deliveryService.getTicketPast(userId)
+ past_deliveries.value = (response.data as any)?.deliveries || []
} catch (error) {
console.error('Failed to fetch past deliveries:', error)
}
diff --git a/src/pages/ticket/ticketauto.vue b/src/pages/ticket/ticketauto.vue
index eb5e757..2e5c622 100644
--- a/src/pages/ticket/ticketauto.vue
+++ b/src/pages/ticket/ticketauto.vue
@@ -36,7 +36,11 @@
{{ customer_description.description }}
-
+
+ PRIME
+ SAME DAY
+ EMERGENCY
+
Credit Card
AUTO
@@ -66,9 +70,9 @@
-
- {{ past_delivery.fill_date }} - {{ past_delivery.gallons_delivered }}
+ a-{{ past_delivery.fill_date }} - {{ past_delivery.gallons_delivered }}
@@ -94,6 +98,9 @@
+
+ This heating oil is dyed and sold solely for home heating use only — not for on-road vehicle use or taxable purposes. Not responsible for driveways damaged from delivery.
+
@@ -218,7 +225,7 @@ async function getCustomerTank(userId: number) {
async function getCustomerDescription(userId: number) {
try {
const response = await customerService.getDescription(userId)
- customer_description.value = (response.data as any)?.description || response.data
+ customer_description.value = response.data
} catch (error) {
console.error('Failed to fetch description:', error)
}
diff --git a/src/services/addressService.ts b/src/services/addressService.ts
index bd0de7f..e44de2d 100644
--- a/src/services/addressService.ts
+++ b/src/services/addressService.ts
@@ -35,6 +35,36 @@ export interface StreetSearchResponse {
query: string;
}
+export interface TownStreetInfo {
+ town: string;
+ state: string;
+ street_count: number;
+}
+
+export interface TownListResponse {
+ ok: boolean;
+ towns: TownStreetInfo[];
+}
+
+export interface StreetPopulateResponse {
+ status: string;
+ message: string;
+ town: string;
+ state: string;
+ streets_added: number;
+ streets_updated: number;
+ total_found: number;
+ errors: string[];
+}
+
+export interface DeleteStreetsResponse {
+ ok: boolean;
+ message: string;
+ deleted: number;
+ town: string;
+ state: string;
+}
+
export const addressService = {
/**
* Search for towns based on partial input.
@@ -59,6 +89,45 @@ export const addressService = {
params: { town, state, q: query, limit }
}),
+ /**
+ * List all towns that have streets in the StreetReference table.
+ * Used by the admin Street Manager page.
+ */
+ listTowns: (): Promise
> =>
+ addressApi.get('/towns/list'),
+
+ /**
+ * Fetch streets from OSM and populate the StreetReference table for a town.
+ * Pass clearExisting=true to replace existing streets (fixes bad data).
+ */
+ populateStreets: (
+ town: string,
+ state: string,
+ clearExisting: boolean = false
+ ): Promise> =>
+ addressApi.post(`/streets/${encodeURIComponent(town)}/${state}`, null, {
+ params: { clear_existing: clearExisting }
+ }),
+
+ /**
+ * Delete all streets for a town from the StreetReference table.
+ */
+ deleteStreets: (
+ town: string,
+ state: string
+ ): Promise> =>
+ addressApi.delete(`/streets/${encodeURIComponent(town)}/${state}`),
+
+ /**
+ * Rename a town across all its StreetReference rows.
+ */
+ renameTown: (
+ oldTown: string,
+ state: string,
+ newTown: string
+ ): Promise> =>
+ addressApi.patch(`/streets/${encodeURIComponent(oldTown)}/${state}/rename`, { new_town: newTown }),
+
/**
* Check if address checker service is available.
*/
diff --git a/src/services/customerService.ts b/src/services/customerService.ts
index 14a43b5..3cc034f 100644
--- a/src/services/customerService.ts
+++ b/src/services/customerService.ts
@@ -64,6 +64,19 @@ export const customerService = {
// Search
search: (query: string): Promise> =>
api.get(`/search/customer?q=${encodeURIComponent(query)}`),
+
+ // Customer alerts
+ getAlert: (id: number): Promise> =>
+ api.get(`/deliverydata/customer/alert/${id}`),
+
+ createAlert: (id: number, data: { severity: number; message: string }): Promise> =>
+ api.post(`/deliverydata/customer/alert/${id}`, data),
+
+ updateAlert: (id: number, data: { severity: number; message: string }): Promise> =>
+ api.put(`/deliverydata/customer/alert/${id}`, data),
+
+ deleteAlert: (id: number): Promise> =>
+ api.delete(`/deliverydata/customer/alert/${id}`),
};
export default customerService;
diff --git a/src/services/deliveryService.ts b/src/services/deliveryService.ts
index 4f26707..734fd5e 100644
--- a/src/services/deliveryService.ts
+++ b/src/services/deliveryService.ts
@@ -25,8 +25,8 @@ export const deliveryService = {
getOrder: (id: number): Promise> =>
api.get(`/delivery/order/${id}`),
- getForMap: (date: string): Promise> =>
- api.get(`/delivery/map`, { params: { date } }),
+ getForMap: (date: string, all = false): Promise> =>
+ api.get(`/delivery/map`, { params: all ? { all: true } : { date } }),
getHistory: (startDate: string, endDate: string): Promise> =>
api.get(`/delivery/history`, { params: { start_date: startDate, end_date: endDate } }),
@@ -56,6 +56,9 @@ export const deliveryService = {
getPast2: (customerId: number): Promise> =>
api.get(`/delivery/past2/${customerId}`),
+ getTicketPast: (customerId: number): Promise> =>
+ api.get(`/delivery/ticket/past/${customerId}`),
+
// Status-based lists
getWaiting: (page: number = 1): Promise> =>
api.get(`/delivery/waiting/${page}`),
diff --git a/src/services/serviceService.ts b/src/services/serviceService.ts
index 630f16d..63a73f1 100644
--- a/src/services/serviceService.ts
+++ b/src/services/serviceService.ts
@@ -47,7 +47,7 @@ export const serviceService = {
serviceApi.get(`/service/parts/customer/${customerId}`),
updateParts: (id: number, data: any): Promise> =>
- serviceApi.put(`/service/parts/update/${id}`, data),
+ serviceApi.post(`/service/parts/update/${id}`, data),
// Service plans
plans: {
diff --git a/src/services/statsService.ts b/src/services/statsService.ts
index 735adb3..8187b85 100644
--- a/src/services/statsService.ts
+++ b/src/services/statsService.ts
@@ -7,7 +7,8 @@ import type {
TotalsComparisonResponse,
DailyGallonsParams,
WeeklyGallonsParams,
- MonthlyGallonsParams
+ MonthlyGallonsParams,
+ WeeklyCustomerResponse
} from '../types/stats';
/**
@@ -58,6 +59,15 @@ export const statsService = {
*/
getTotalsComparison: (): Promise> => {
return api.get('/stats/totals/comparison');
+ },
+
+ /**
+ * Get weekly new customer signups by year
+ * @param years - array of years to compare
+ */
+ getWeeklyCustomers: (years: number[]): Promise> => {
+ const queryParams = new URLSearchParams({ years: years.join(',') });
+ return api.get(`/stats/customers/weekly?${queryParams.toString()}`);
}
};
diff --git a/src/types/models.ts b/src/types/models.ts
index 88f7104..4a36f2e 100644
--- a/src/types/models.ts
+++ b/src/types/models.ts
@@ -110,7 +110,7 @@ export interface Delivery extends BaseEntity {
customer_address: string;
customer_town: string;
customer_state: string;
- customer_zip: number;
+ customer_zip: string;
gallons_ordered: number;
customer_asked_for_fill: number;
gallons_delivered: number;
diff --git a/src/types/stats.ts b/src/types/stats.ts
index 94f3bdf..ff96578 100644
--- a/src/types/stats.ts
+++ b/src/types/stats.ts
@@ -47,6 +47,26 @@ export interface WeeklyGallonsResponse {
years: WeeklyGallonsYearData[];
}
+// ============================================
+// Weekly Customer Signups Endpoint Types
+// ============================================
+
+export interface WeeklyCustomerDataPoint {
+ week_number: number;
+ week_start: string; // YYYY-MM-DD
+ signups: number;
+}
+
+export interface WeeklyCustomerYearData {
+ year: number;
+ data: WeeklyCustomerDataPoint[];
+}
+
+export interface WeeklyCustomerResponse {
+ ok: boolean;
+ years: WeeklyCustomerYearData[];
+}
+
// ============================================
// Monthly Gallons Endpoint Types
// ============================================
diff --git a/src/utils/addressUtils.ts b/src/utils/addressUtils.ts
index ac86b2b..6ef70f2 100644
--- a/src/utils/addressUtils.ts
+++ b/src/utils/addressUtils.ts
@@ -1,5 +1,9 @@
import { STATE_ID_TO_ABBR } from '../constants/states';
+export const formatZip = (zip: string | number): string => {
+ return String(zip || '').trim().padStart(5, '0');
+};
+
/**
* Formats an address string with street, town, state, and zip.
* Handles missing or invalid state IDs gracefully.
@@ -19,7 +23,7 @@ export const formatAddress = (
): string => {
const cleanAddress = (address || '').trim();
const cleanTown = (town || '').trim();
- const cleanZip = String(zip || '').trim();
+ const cleanZip = formatZip(zip);
const parsedStateId = typeof stateId === 'string' ? parseInt(stateId, 10) : stateId;
const stateAbbr = STATE_ID_TO_ABBR[parsedStateId] || 'MA'; // Default to MA if not found