working pages no maps

This commit is contained in:
2025-12-29 21:38:32 -05:00
parent f08432e417
commit 984c8e8169
8 changed files with 642 additions and 205 deletions

View File

@@ -21,9 +21,15 @@
// Check for user session on mount (this is a placeholder, actual implementation may vary) // Check for user session on mount (this is a placeholder, actual implementation may vary)
onMount(() => { onMount(() => {
const storedUserString = localStorage.getItem('user'); const storedUserString = localStorage.getItem('user');
if (storedUserString) { const token = localStorage.getItem('auth_token');
if (storedUserString && token) {
storedUser = JSON.parse(storedUserString); storedUser = JSON.parse(storedUserString);
user.set(storedUser); user.set(storedUser);
} else {
// Clear if inconsistent
localStorage.removeItem('user');
localStorage.removeItem('auth_token');
user.set(null);
} }
}); });
@@ -39,6 +45,7 @@
const logout = () => { const logout = () => {
user.set(null); user.set(null);
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.removeItem('auth_token');
window.location.href = '/'; window.location.href = '/';
}; };

View File

@@ -1,42 +1,84 @@
<!-- src/routes/[stateSlug]/+page.svelte --> <!-- src/routes/[stateSlug]/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { newEnglandStates, allCounties, type NewEnglandState } from '$lib/states'; // <--- Import type and counties import {
newEnglandStates,
type NewEnglandState,
massachusettsCounties,
maineCounties,
vermontCounties,
newHampshireCounties,
rhodeIslandCounties,
connecticutCounties
} from '$lib/states';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { browser } from '$app/environment';
// The stateSlug from $page.params is guaranteed to be a string if this route matches const { stateSlug } = $page.params as { stateSlug: string };
// because [stateSlug] is a required parameter.
const { stateSlug } = $page.params as { stateSlug: string }; // Type assertion for clarity
let stateData: NewEnglandState | undefined; let stateData: NewEnglandState | undefined;
let stateCounties: NewEnglandState[] = [];
let filteredCounties: any[] = []; let filteredCounties: any[] = [];
let loading = false; let loading = false;
let error: string | null = null; let error: string | null = null;
let hoveredCounty: string | null = null;
// Clean county data access using object lookup
const countyDataMap: Record<string, NewEnglandState[]> = {
MA: massachusettsCounties,
ME: maineCounties,
VT: vermontCounties,
NH: newHampshireCounties,
RI: rhodeIslandCounties,
CT: connecticutCounties
};
function getStateCounties(stateId: string): NewEnglandState[] {
return countyDataMap[stateId] || [];
}
function handleCountyClick(county: NewEnglandState) {
// Match county names between map data and API data
const cleanMapName = county.name.toLowerCase().replace(/\s+county$/, '').replace(/[^a-z]/g, '');
const apiCounty = filteredCounties.find((c: any) =>
cleanMapName === c.name.toLowerCase().replace(/[^a-z]/g, '')
);
goto(apiCounty ? `/${stateSlug}/${apiCounty.id}` : `/${stateSlug}/${county.slug}`);
}
function handleMouseEnter(countyId: string) {
if (browser) {
hoveredCounty = countyId;
}
}
function handleMouseLeave() {
if (browser) {
hoveredCounty = null;
}
}
onMount(async () => { onMount(async () => {
stateData = newEnglandStates.find(s => s.id === stateSlug); stateData = newEnglandStates.find((s: NewEnglandState) => s.id === stateSlug);
if (stateData) { if (stateData) {
// Load API county data
loading = true; loading = true;
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
const headers: Record<string, string> = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`, { const response = await fetch(`http://localhost:9552/state/${stateSlug.toUpperCase()}`, {
headers headers: token ? { Authorization: `Bearer ${token}` } : {}
}); });
if (!response.ok) { filteredCounties = response.ok ? await response.json() : [];
throw new Error(`API call failed with status ${response.status}`);
}
filteredCounties = await response.json();
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : 'Failed to fetch counties'; error = err instanceof Error ? err.message : 'Failed to fetch counties';
filteredCounties = []; filteredCounties = [];
} finally {
loading = false;
} }
loading = false;
// Load map county data
stateCounties = getStateCounties(stateSlug);
} }
}); });
@@ -52,39 +94,66 @@
</ul> </ul>
</nav> </nav>
<div class="card lg:card-side bg-base-100 shadow-xl"> <!-- Centered State Name -->
<figure class=" "> <div class="text-center mb-6">
<img <h1 class="text-4xl font-bold">{stateData.name}</h1>
src={stateData.image} </div>
alt="Map or notable feature of {stateData.id}"
class="object-cover" <!-- Interactive County Map -->
/> <div class="flex justify-center mb-6">
</figure> {#if browser && stateCounties.length > 0}
<div class="card-body lg:w-1/2"> <svg
<h1 class="card-title text-4xl">{stateData.name}</h1> xmlns="http://www.w3.org/2000/svg"
<p>This is the page for {stateData.name}. Here you can add more specific information about this wonderful state!</p> viewBox="0 0 1000 600"
<p>The URL for this page is <code class="bg-base-300 p-1 rounded">/{stateData.id}</code> which is {stateData.id.length} characters long.</p> class="w-full max-w-2xl h-auto border border-gray-300 rounded-lg shadow-md"
<div class="mt-4"> aria-labelledby="countyMapTitle"
<h2 class="text-xl font-semibold mb-2">Counties:</h2> role="img"
{#if loading} >
<p>Loading counties...</p> <title id="countyMapTitle">Interactive Map of {stateData.name} Counties</title>
{:else if error} {#each stateCounties as county}
<p class="text-error">Error: {error}</p> <path
{:else} d={county.pathD}
<ul class="list-disc pl-5"> class={`stroke-black stroke-1 cursor-pointer transition-all duration-150 ease-in-out
{#each filteredCounties as county} ${hoveredCounty === county.id ? county.hoverFill : county.fill}`}
<li> on:click={() => handleCountyClick(county)}
<a href="/{stateSlug}/{county.id}" class="text-blue-600 hover:underline">{county.name}</a> on:mouseenter={() => handleMouseEnter(county.id)}
</li> on:mouseleave={handleMouseLeave}
aria-label={county.name}
tabindex="0"
role="link"
on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleCountyClick(county)}}
>
<title>{county.name}</title>
</path>
{/each} {/each}
</ul> </svg>
{:else}
<div class="w-full max-w-2xl h-[400px] bg-gray-200 animate-pulse rounded-lg flex justify-center items-center">
<p>Loading county map...</p>
</div>
{/if} {/if}
</div> </div>
<div class="card-actions justify-end mt-4">
<!-- Counties List -->
<div class="mt-4">
<h2 class="text-xl font-semibold mb-2 text-center">Counties:</h2>
{#if loading}
<p class="text-center">Loading counties...</p>
{:else if error}
<p class="text-center text-error">Error: {error}</p>
{:else}
<div class="flex flex-col items-center gap-2">
{#each filteredCounties as county}
<a href="/{stateSlug}/{county.id}" class="text-center text-blue-600 hover:underline block">{county.name}</a>
{/each}
</div>
{/if}
</div>
<!-- Back Button -->
<div class="text-center mt-6">
<a href="/" class="btn btn-primary">Back to Map</a> <a href="/" class="btn btn-primary">Back to Map</a>
</div> </div>
</div>
</div>
{:else if !stateData && stateSlug} <!-- Check if stateData is still undefined after onMount attempted to find it --> {:else if !stateData && stateSlug} <!-- Check if stateData is still undefined after onMount attempted to find it -->
<div class="text-center py-10"> <div class="text-center py-10">
<h1 class="text-3xl font-bold text-error">State Not Found</h1> <h1 class="text-3xl font-bold text-error">State Not Found</h1>
@@ -92,11 +161,3 @@
<a href="/" class="btn btn-primary mt-6">Go Back to Map</a> <a href="/" class="btn btn-primary mt-6">Go Back to Map</a>
</div> </div>
{/if} {/if}
<style>
figure img {
object-fit: cover;
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,9 +1,8 @@
<!-- src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte --> <!-- src/routes/(app)/[stateSlug]/[countySlug]/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores import { page } from '$app/stores';
';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { newEnglandStates } from '$lib/states'; import { newEnglandStates } from '../../../../lib/states';
interface Listing { interface Listing {
id: number; id: number;
@@ -18,6 +17,7 @@
phone: string | null; phone: string | null;
online_ordering: string; online_ordering: string;
county_id: number; county_id: number;
town: string | null;
user_id: number; user_id: number;
last_edited: string; last_edited: string;
} }
@@ -157,7 +157,24 @@
<p class="text-lg text-gray-600">No active listings found for this county.</p> <p class="text-lg text-gray-600">No active listings found for this county.</p>
</div> </div>
{:else} {:else}
<div class="overflow-x-auto"> <!-- Sort Controls -->
<div class="mb-4 flex flex-wrap gap-2">
<select class="select select-bordered select-sm" bind:value={sortColumn} on:change={sortListings}>
<option value="company_name">Company</option>
<option value="town">Town</option>
<option value="price_per_gallon">Price per Gallon</option>
<option value="bio_percent">Bio %</option>
<option value="service">Service</option>
<option value="phone">Contact</option>
<option value="last_edited">Last Updated</option>
</select>
<button class="btn btn-sm" on:click={() => { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; sortListings(); }}>
{sortDirection === 'asc' ? '↑' : '↓'}
</button>
</div>
<!-- Desktop Table View -->
<div class="hidden md:block overflow-x-auto">
<table class="table table-zebra w-full"> <table class="table table-zebra w-full">
<thead> <thead>
<tr> <tr>
@@ -167,15 +184,15 @@
{sortDirection === 'asc' ? '↑' : '↓'} {sortDirection === 'asc' ? '↑' : '↓'}
{/if} {/if}
</th> </th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}> <th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('town')}>
Price per Gallon Card Town
{#if sortColumn === 'price_per_gallon'} {#if sortColumn === 'town'}
{sortDirection === 'asc' ? '↑' : '↓'} {sortDirection === 'asc' ? '↑' : '↓'}
{/if} {/if}
</th> </th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon_cash')}> <th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('price_per_gallon')}>
Price per Gallon Cash Price per Gallon
{#if sortColumn === 'price_per_gallon_cash'} {#if sortColumn === 'price_per_gallon'}
{sortDirection === 'asc' ? '↑' : '↓'} {sortDirection === 'asc' ? '↑' : '↓'}
{/if} {/if}
</th> </th>
@@ -185,12 +202,6 @@
{sortDirection === 'asc' ? '↑' : '↓'} {sortDirection === 'asc' ? '↑' : '↓'}
{/if} {/if}
</th> </th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('online_ordering')}>
Online Ordering
{#if sortColumn === 'online_ordering'}
{sortDirection === 'asc' ? '↑' : '↓'}
{/if}
</th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('service')}> <th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('service')}>
Service Service
{#if sortColumn === 'service'} {#if sortColumn === 'service'}
@@ -198,29 +209,31 @@
{/if} {/if}
</th> </th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}> <th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('phone')}>
Phone Contact
{#if sortColumn === 'phone'} {#if sortColumn === 'phone'}
{sortDirection === 'asc' ? '↑' : '↓'} {sortDirection === 'asc' ? '↑' : '↓'}
{/if} {/if}
</th> </th>
<th class="cursor-pointer hover:bg-base-200" on:click={() => handleSort('last_edited')}>
Last Updated
{#if sortColumn === 'last_edited'}
{sortDirection === 'asc' ? '↑' : '↓'}
{/if}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each listings as listing} {#each listings as listing}
<tr> <tr>
<td>{listing.company_name}</td> <td>{listing.company_name}</td>
<td>${listing.price_per_gallon.toFixed(2)}</td> <td>{listing.town || 'N/A'}</td>
<td>{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</td>
<td>{listing.bio_percent}%</td>
<td> <td>
{#if listing.online_ordering === 'none'} <div class="text-sm">
No <div><strong>Card:</strong> ${listing.price_per_gallon.toFixed(2)}</div>
{:else if listing.online_ordering === 'online_only'} <div><strong>Cash:</strong> {listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}</div>
Online Only </div>
{:else if listing.online_ordering === 'both'}
Both
{/if}
</td> </td>
<td>{listing.bio_percent}%</td>
<td> <td>
{#if listing.service} {#if listing.service}
<span class="badge badge-success">Yes</span> <span class="badge badge-success">Yes</span>
@@ -229,17 +242,90 @@
{/if} {/if}
</td> </td>
<td> <td>
<div class="text-sm">
{#if listing.phone} {#if listing.phone}
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a> <div><strong>Phone:</strong> <a href="tel:{listing.phone}" class="text-blue-600 hover:underline">{listing.phone}</a></div>
{:else} {:else}
N/A <div><strong>Phone:</strong> N/A</div>
{/if} {/if}
<div>
<strong>Online:</strong>
{#if listing.online_ordering === 'none'}
No
{:else if listing.online_ordering === 'online_only'}
Online Only
{:else if listing.online_ordering === 'both'}
Both
{/if}
</div>
</div>
</td> </td>
<td>{new Date(listing.last_edited).toLocaleDateString()}</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Mobile Card View -->
<div class="block md:hidden space-y-4">
{#each listings as listing}
<div class="card bg-base-100 shadow-lg">
<div class="card-body p-4">
<h3 class="card-title text-lg font-bold">
{listing.company_name}
{#if listing.town}
<br><small class="text-gray-500 font-normal">{listing.town}</small>
{/if}
</h3>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="font-semibold">Card Price:</span><br>
${listing.price_per_gallon.toFixed(2)}
</div>
<div>
<span class="font-semibold">Cash Price:</span><br>
{listing.price_per_gallon_cash ? `$${listing.price_per_gallon_cash.toFixed(2)}` : 'N/A'}
</div>
<div>
<span class="font-semibold">Bio %:</span><br>
{listing.bio_percent}%
</div>
<div>
<span class="font-semibold">Service:</span><br>
{#if listing.service}
<span class="badge badge-success badge-sm">Yes</span>
{:else}
<span class="badge badge-neutral badge-sm">No</span>
{/if}
</div>
<div>
<span class="font-semibold">Contact:</span><br>
{#if listing.phone}
<a href="tel:{listing.phone}" class="text-blue-600 hover:underline text-sm">{listing.phone}</a><br>
{:else}
N/A<br>
{/if}
<small>
Online:
{#if listing.online_ordering === 'none'}
No
{:else if listing.online_ordering === 'online_only'}
Online Only
{:else if listing.online_ordering === 'both'}
Both
{/if}
</small>
</div>
<div>
<span class="font-semibold">Last Updated:</span><br>
<small>{new Date(listing.last_edited).toLocaleDateString()}</small>
</div>
</div>
</div>
</div>
{/each}
</div>
{/if} {/if}
</div> </div>
{:else if error} {:else if error}

View File

@@ -34,7 +34,7 @@
// Assuming the backend returns a token or some success indicator // Assuming the backend returns a token or some success indicator
// Store the token in localStorage for authentication checks // Store the token in localStorage for authentication checks
if (data.token) { if (data.token) {
localStorage.setItem('token', data.token); localStorage.setItem('auth_token', data.token);
} }
// Store the user object in localStorage // Store the user object in localStorage
localStorage.setItem('user', JSON.stringify(data.user)); localStorage.setItem('user', JSON.stringify(data.user));

View File

@@ -14,13 +14,32 @@
phone: string | null; phone: string | null;
online_ordering: string; online_ordering: string;
county_id: number; county_id: number;
town: string | null;
user_id: number; user_id: number;
last_edited: string; last_edited: string;
} }
interface Company {
id: number;
active: boolean;
created: string;
name: string;
address?: string;
town?: string;
state?: string;
phone?: string;
owner_name?: string;
owner_phone_number?: string;
email?: string;
user_id?: number;
}
let listings: Listing[] = []; let listings: Listing[] = [];
let company: Company | null = null;
let isLoading = true; let isLoading = true;
let companyLoading = true;
let error = ''; let error = '';
let companyError = '';
let successMessage = ''; let successMessage = '';
// Inline editing state // Inline editing state
@@ -28,16 +47,67 @@
let editingField: string | null = null; let editingField: string | null = null;
let editingValue: string = ''; let editingValue: string = '';
async function fetchListings() { async function fetchCompany() {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
const response = await fetch('http://localhost:9552/listing', {
if (!token) {
// Redirect to login if no token
window.location.href = '/login';
return;
}
const response = await fetch('http://localhost:9552/company', {
headers: { headers: {
'Authorization': token ? `Bearer ${token}` : '' 'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) {
company = await response.json();
} else if (response.status === 401) {
// Token expired or invalid, redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
window.location.href = '/login';
return;
} else if (response.status === 404) {
// Company not found - that's okay, user can create one
company = null;
} else {
companyError = 'Failed to load company information';
}
} catch (err) {
companyError = 'Network error loading company information';
} finally {
companyLoading = false;
}
}
async function fetchListings() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
// Redirect to login if no token
window.location.href = '/login';
return;
}
const response = await fetch('http://localhost:9552/listing', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) { if (response.ok) {
listings = await response.json(); listings = await response.json();
} else if (response.status === 401) {
// Token expired or invalid, redirect to login
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
window.location.href = '/login';
return;
} else { } else {
error = 'Failed to load listings'; error = 'Failed to load listings';
} }
@@ -52,7 +122,7 @@
if (!confirm('Are you sure you want to delete this listing?')) return; if (!confirm('Are you sure you want to delete this listing?')) return;
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/listing/${id}`, { const response = await fetch(`http://localhost:9552/listing/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@@ -73,7 +143,7 @@
function editListing(listing: Listing) { function editListing(listing: Listing) {
// Navigate to the edit page for this listing // Navigate to the edit page for this listing
window.location.href = `/vendor/price/${listing.id}`; window.location.href = `/vendor/listing/${listing.id}`;
} }
function startEditing(listing: Listing, field: string) { function startEditing(listing: Listing, field: string) {
@@ -128,7 +198,7 @@
updateData[editingField] = newValue; updateData[editingField] = newValue;
} }
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/listing/${listing.id}`, { const response = await fetch(`http://localhost:9552/listing/${listing.id}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
@@ -160,13 +230,31 @@
} }
} }
function formatPhoneNumber(phone: string): string {
// Remove all non-digit characters
const cleaned = phone.replace(/\D/g, '');
// Check if it's a valid US phone number (10 or 11 digits)
if (cleaned.length === 10) {
// Format as (XXX) XXX-XXXX
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
} else if (cleaned.length === 11 && cleaned.startsWith('1')) {
// Format as +1 (XXX) XXX-XXXX for 1-prefixed numbers
return `+1 (${cleaned.slice(1, 4)}) ${cleaned.slice(4, 7)}-${cleaned.slice(7)}`;
}
// Return original if it doesn't match expected formats
return phone;
}
onMount(() => { onMount(() => {
fetchCompany();
fetchListings(); fetchListings();
}); });
</script> </script>
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4">Vendor Dashboard</h1> <h1 class="text-3xl font-bold mb-6">Vendor Dashboard</h1>
<!-- Success Banner --> <!-- Success Banner -->
{#if successMessage} {#if successMessage}
@@ -175,10 +263,91 @@
</div> </div>
{/if} {/if}
<p>Welcome to the Vendor section. Navigate to specific pages using the links below.</p> <!-- Company Information Box (Facebook-like profile) -->
<div class="mt-4"> <div class="card bg-base-100 shadow-xl mb-8">
<a href="/vendor/profile" class="text-blue-500 hover:underline">Profile</a> <div class="card-body">
<a href="/vendor/price" class="text-blue-500 hover:underline ml-4">Price</a> {#if companyLoading}
<div class="flex justify-center py-8">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if companyError}
<div class="alert alert-error">
<span>{companyError}</span>
</div>
{:else if company}
<div class="flex flex-col md:flex-row gap-6">
<!-- Company Avatar/Icon -->
<div class="flex-shrink-0">
<div class="avatar">
<div class="w-24 h-24 rounded-full bg-primary flex items-center justify-center">
<span class="text-3xl text-primary-content font-bold">
{company.name.charAt(0).toUpperCase()}
</span>
</div>
</div>
</div>
<!-- Company Details -->
<div class="flex-grow">
<h2 class="card-title text-2xl mb-2">{company.name}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
{#if company.address}
<div>
<span class="font-semibold">Address:</span><br>
{company.address}
</div>
{/if}
{#if company.town && company.state}
<div>
<span class="font-semibold">Location:</span><br>
{company.town}, {company.state}
</div>
{/if}
{#if company.phone}
<div>
<span class="font-semibold">Phone:</span><br>
<a href="tel:{company.phone}" class="text-blue-600 hover:underline">{formatPhoneNumber(company.phone)}</a>
</div>
{/if}
{#if company.owner_name}
<div>
<span class="font-semibold">Owner:</span><br>
{company.owner_name}
</div>
{/if}
{#if company.owner_phone_number}
<div>
<span class="font-semibold">Owner Phone:</span><br>
<a href="tel:{company.owner_phone_number}" class="text-blue-600 hover:underline">{formatPhoneNumber(company.owner_phone_number)}</a>
</div>
{/if}
{#if company.email}
<div>
<span class="font-semibold">Email:</span><br>
<a href="mailto:{company.email}" class="text-blue-600 hover:underline">{company.email}</a>
</div>
{/if}
<div>
<span class="font-semibold">Member Since:</span><br>
{new Date(company.created).toLocaleDateString()}
</div>
</div>
</div>
</div>
{:else}
<div class="text-center py-8">
<h3 class="text-lg font-semibold mb-2">No Company Profile Found</h3>
<p class="text-gray-600 mb-4">Create your company profile to get started.</p>
<a href="/vendor/profile" class="btn btn-primary">Create Company Profile</a>
</div>
{/if}
</div>
</div>
<!-- Navigation Links -->
<div class="flex flex-wrap gap-4 mb-8">
<a href="/vendor/profile" class="btn btn-outline">Company Profile</a>
<a href="/vendor/listing" class="btn btn-primary">Create Listing</a>
</div> </div>
<!-- Listings Table --> <!-- Listings Table -->
@@ -196,7 +365,7 @@
{:else if listings.length === 0} {:else if listings.length === 0}
<div class="text-center py-8"> <div class="text-center py-8">
<p class="text-lg text-gray-600">No listings found.. create one :)</p> <p class="text-lg text-gray-600">No listings found.. create one :)</p>
<a href="/vendor/price" class="btn btn-primary mt-4">Create Listing</a> <a href="/vendor/listing" class="btn btn-primary mt-4">Create Listing</a>
</div> </div>
{:else} {:else}
<div class="overflow-x-auto"> <div class="overflow-x-auto">
@@ -204,6 +373,7 @@
<thead> <thead>
<tr> <tr>
<th>Company</th> <th>Company</th>
<th>Location</th>
<th>Price Card</th> <th>Price Card</th>
<th>Price Cash</th> <th>Price Cash</th>
<th>Min Order</th> <th>Min Order</th>
@@ -218,6 +388,13 @@
{#each listings as listing} {#each listings as listing}
<tr> <tr>
<td>{listing.company_name}</td> <td>{listing.company_name}</td>
<td>
{#if listing.town}
{listing.town} (ID: {listing.county_id})
{:else}
County ID: {listing.county_id}
{/if}
</td>
<!-- Price Card (editable) --> <!-- Price Card (editable) -->
<td> <td>

View File

@@ -19,7 +19,8 @@
bioPercent: 0, bioPercent: 0,
phone: '', phone: '',
state: '', state: '',
countyId: 0 countyId: 0,
town: ''
}; };
// Active status // Active status
@@ -39,7 +40,9 @@
bioPercent: '', bioPercent: '',
phone: '', phone: '',
state: '', state: '',
countyId: '' countyId: '',
minimumOrder: '',
town: ''
}; };
// Form submission state // Form submission state
@@ -76,6 +79,8 @@
errors.phone = ''; errors.phone = '';
errors.state = ''; errors.state = '';
errors.countyId = ''; errors.countyId = '';
errors.minimumOrder = '';
errors.town = '';
let isValid = true; let isValid = true;
@@ -110,6 +115,16 @@
isValid = false; isValid = false;
} }
if (formData.minimumOrder === null || formData.minimumOrder < 1) {
errors.minimumOrder = 'Minimum order must be at least 1';
isValid = false;
}
if (!formData.town.trim()) {
errors.town = 'Town is required';
isValid = false;
}
return isValid; return isValid;
} }
@@ -140,7 +155,8 @@
bio_percent: formData.bioPercent, bio_percent: formData.bioPercent,
phone: formData.phone, phone: formData.phone,
online_ordering: onlineOrdering, online_ordering: onlineOrdering,
county_id: formData.countyId county_id: formData.countyId,
town: formData.town.trim() || null
}) })
}); });
@@ -196,7 +212,7 @@
<ul> <ul>
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li> <li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li> <li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li>
<li>Price Management</li> <li>Listing Management</li>
</ul> </ul>
</nav> </nav>
@@ -320,10 +336,16 @@
min="1" min="1"
max="200" max="200"
step="1" step="1"
class="input input-bordered w-full" required
class="input input-bordered w-full {errors.minimumOrder ? 'input-error' : ''}"
bind:value={formData.minimumOrder} bind:value={formData.minimumOrder}
placeholder="Optional (1-200)" placeholder="Enter minimum order quantity"
/> />
{#if errors.minimumOrder}
<label class="label">
<span class="label-text-alt text-error">{errors.minimumOrder}</span>
</label>
{/if}
</div> </div>
</div> </div>
</div> </div>
@@ -347,51 +369,10 @@
</div> </div>
</div> </div>
<!-- Contact & Location Section --> <!-- Location Section -->
<div class="bg-base-200 p-4 rounded-lg"> <div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3> <h3 class="text-lg font-semibold mb-3">Location</h3>
<div class="space-y-4"> <div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
<!-- State Dropdown --> <!-- State Dropdown -->
<div class="form-control"> <div class="form-control">
<label class="label" for="state"> <label class="label" for="state">
@@ -445,6 +426,77 @@
</label> </label>
{/if} {/if}
</div> </div>
<!-- Town Input -->
<div class="form-control">
<label class="label" for="town">
<span class="label-text">Town</span>
</label>
<input
id="town"
type="text"
required
class="input input-bordered w-full {errors.town ? 'input-error' : ''}"
bind:value={formData.town}
placeholder="Enter town name"
maxlength="100"
/>
{#if errors.town}
<label class="label">
<span class="label-text-alt text-error">{errors.town}</span>
</label>
{/if}
<label class="label">
<span class="label-text-alt">Specify the town within the selected county</span>
</label>
</div>
</div>
</div>
<!-- Contact Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact</h3>
<div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
</div> </div>
</div> </div>

View File

@@ -23,6 +23,7 @@
phone: string | null; phone: string | null;
online_ordering: string; online_ordering: string;
county_id: number; county_id: number;
town: string | null;
user_id: number; user_id: number;
last_edited: string; last_edited: string;
} }
@@ -41,7 +42,8 @@
bioPercent: 0, bioPercent: 0,
phone: '', phone: '',
state: '', state: '',
countyId: 0 countyId: 0,
town: ''
}; };
// Active status // Active status
@@ -61,7 +63,9 @@
bioPercent: '', bioPercent: '',
phone: '', phone: '',
state: '', state: '',
countyId: '' countyId: '',
minimumOrder: '',
town: ''
}; };
// Form submission state // Form submission state
@@ -76,7 +80,7 @@
// Load existing listing data // Load existing listing data
async function loadListing() { async function loadListing() {
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/listing/${listingId}`, { const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
headers: { headers: {
'Authorization': token ? `Bearer ${token}` : '' 'Authorization': token ? `Bearer ${token}` : ''
@@ -97,6 +101,7 @@
formData.phone = listing.phone || ''; formData.phone = listing.phone || '';
onlineOrdering = listing.online_ordering; onlineOrdering = listing.online_ordering;
formData.countyId = listing.county_id; formData.countyId = listing.county_id;
formData.town = listing.town || '';
// Load the state for this county // Load the state for this county
await loadStateForCounty(listing.county_id); await loadStateForCounty(listing.county_id);
@@ -158,6 +163,8 @@
errors.phone = ''; errors.phone = '';
errors.state = ''; errors.state = '';
errors.countyId = ''; errors.countyId = '';
errors.minimumOrder = '';
errors.town = '';
let isValid = true; let isValid = true;
@@ -192,6 +199,16 @@
isValid = false; isValid = false;
} }
if (formData.minimumOrder === null || formData.minimumOrder < 1) {
errors.minimumOrder = 'Minimum order must be at least 1';
isValid = false;
}
if (!formData.town.trim()) {
errors.town = 'Town is required';
isValid = false;
}
return isValid; return isValid;
} }
@@ -205,7 +222,7 @@
submitMessage = ''; submitMessage = '';
try { try {
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
const response = await fetch(`http://localhost:9552/listing/${listingId}`, { const response = await fetch(`http://localhost:9552/listing/${listingId}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
@@ -222,7 +239,8 @@
bio_percent: formData.bioPercent, bio_percent: formData.bioPercent,
phone: formData.phone, phone: formData.phone,
online_ordering: onlineOrdering, online_ordering: onlineOrdering,
county_id: formData.countyId county_id: formData.countyId,
town: formData.town.trim() || null
}) })
}); });
@@ -282,7 +300,7 @@
<ul> <ul>
<li><a href="/" class="text-blue-500 hover:underline">Home</a></li> <li><a href="/" class="text-blue-500 hover:underline">Home</a></li>
<li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li> <li><a href="/vendor" class="text-blue-500 hover:underline">Vendor Dashboard</a></li>
<li><a href="/vendor/price" class="text-blue-500 hover:underline">Price Management</a></li> <li><a href="/vendor/listing" class="text-blue-500 hover:underline">Listing Management</a></li>
<li>Edit Listing</li> <li>Edit Listing</li>
</ul> </ul>
</nav> </nav>
@@ -411,10 +429,16 @@
min="1" min="1"
max="200" max="200"
step="1" step="1"
class="input input-bordered w-full" required
class="input input-bordered w-full {errors.minimumOrder ? 'input-error' : ''}"
bind:value={formData.minimumOrder} bind:value={formData.minimumOrder}
placeholder="Optional (1-200)" placeholder="Enter minimum order quantity"
/> />
{#if errors.minimumOrder}
<label class="label">
<span class="label-text-alt text-error">{errors.minimumOrder}</span>
</label>
{/if}
</div> </div>
</div> </div>
</div> </div>
@@ -438,51 +462,10 @@
</div> </div>
</div> </div>
<!-- Contact & Location Section --> <!-- Location Section -->
<div class="bg-base-200 p-4 rounded-lg"> <div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact & Location</h3> <h3 class="text-lg font-semibold mb-3">Location</h3>
<div class="space-y-4"> <div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
<!-- State Dropdown --> <!-- State Dropdown -->
<div class="form-control"> <div class="form-control">
<label class="label" for="state"> <label class="label" for="state">
@@ -536,6 +519,77 @@
</label> </label>
{/if} {/if}
</div> </div>
<!-- Town Input -->
<div class="form-control">
<label class="label" for="town">
<span class="label-text">Town</span>
</label>
<input
id="town"
type="text"
required
class="input input-bordered w-full {errors.town ? 'input-error' : ''}"
bind:value={formData.town}
placeholder="Enter town name"
maxlength="100"
/>
{#if errors.town}
<label class="label">
<span class="label-text-alt text-error">{errors.town}</span>
</label>
{/if}
<label class="label">
<span class="label-text-alt">Specify the town within the selected county</span>
</label>
</div>
</div>
</div>
<!-- Contact Section -->
<div class="bg-base-200 p-4 rounded-lg">
<h3 class="text-lg font-semibold mb-3">Contact</h3>
<div class="space-y-4">
<!-- Phone Number -->
<div class="form-control">
<label class="label" for="phone">
<span class="label-text">Phone</span>
</label>
<input
id="phone"
type="tel"
required={!phoneDisabled}
disabled={phoneDisabled}
class="input input-bordered w-full {phoneDisabled ? 'input-disabled' : ''} {errors.phone ? 'input-error' : ''}"
bind:value={formData.phone}
on:input={handlePhoneInput}
placeholder="123-456-7890"
/>
{#if errors.phone}
<label class="label">
<span class="label-text-alt text-error">{errors.phone}</span>
</label>
{/if}
</div>
<!-- Online Ordering -->
<div class="form-control">
<fieldset class="fieldset">
<legend class="fieldset-legend">Online Ordering</legend>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="none" />
No online ordering
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="online_only" />
Yes, online only
</label>
<label class="fieldset-label">
<input type="radio" name="onlineOrdering" bind:group={onlineOrdering} value="both" />
Both online and phone
</label>
</fieldset>
</div>
</div> </div>
</div> </div>

View File

@@ -23,7 +23,7 @@
try { try {
const response = await fetch('http://localhost:9552/company', { const response = await fetch('http://localhost:9552/company', {
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
} }
}); });
@@ -67,7 +67,7 @@
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
@@ -105,7 +105,7 @@
const response = await fetch('http://localhost:9552/company', { const response = await fetch('http://localhost:9552/company', {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}` 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
} }
}); });
@@ -128,7 +128,7 @@
} }
onMount(() => { onMount(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('auth_token');
if (token) { if (token) {
fetchCompany(); fetchCompany();
} else { } else {