new shop calcs

This commit is contained in:
NotARoomba 2026-02-23 14:34:42 -05:00
parent 064a10578b
commit c9643e3df4
7 changed files with 98 additions and 7 deletions

View file

@ -32329,6 +32329,7 @@ var shopOrdersTable = pgTable("shop_orders", {
shippingAddress: text("shipping_address"),
phone: varchar(),
notes: text(),
trackingNumber: varchar("tracking_number"),
isFulfilled: boolean("is_fulfilled").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
@ -33203,6 +33204,7 @@ shop.get("/orders", async ({ headers }) => {
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
shippingAddress: shopOrdersTable.shippingAddress,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
createdAt: shopOrdersTable.createdAt,
itemId: shopItemsTable.id,
@ -35497,6 +35499,7 @@ admin.get("/orders", async ({ headers, query, status: status2 }) => {
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
notes: shopOrdersTable.notes,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
shippingAddress: shopOrdersTable.shippingAddress,
phone: shopOrdersTable.phone,
@ -35526,7 +35529,8 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
const {
status: orderStatus,
notes,
isFulfilled
isFulfilled,
trackingNumber
} = body;
const validStatuses = [
"pending",
@ -35544,6 +35548,8 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
updateData.status = orderStatus;
if (notes !== undefined)
updateData.notes = notes;
if (trackingNumber !== undefined)
updateData.trackingNumber = trackingNumber;
if (isFulfilled !== undefined)
updateData.isFulfilled = isFulfilled;
const updated = await db.update(shopOrdersTable).set(updateData).where(eq(shopOrdersTable.id, parseInt(params.id))).returning({
@ -35554,6 +35560,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status: status2 }) =>
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
notes: shopOrdersTable.notes,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
shippingAddress: shopOrdersTable.shippingAddress,
createdAt: shopOrdersTable.createdAt

View file

@ -0,0 +1 @@
ALTER TABLE "shop_orders" ADD COLUMN "tracking_number" varchar;

View file

@ -2140,6 +2140,7 @@ admin.get("/orders", async ({ headers, query, status }) => {
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
notes: shopOrdersTable.notes,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
shippingAddress: shopOrdersTable.shippingAddress,
phone: shopOrdersTable.phone,
@ -2183,7 +2184,8 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
status: orderStatus,
notes,
isFulfilled,
} = body as { status?: string; notes?: string; isFulfilled?: boolean };
trackingNumber,
} = body as { status?: string; notes?: string; isFulfilled?: boolean; trackingNumber?: string };
const validStatuses = [
"pending",
@ -2200,6 +2202,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
const updateData: Record<string, unknown> = { updatedAt: new Date() };
if (orderStatus) updateData.status = orderStatus;
if (notes !== undefined) updateData.notes = notes;
if (trackingNumber !== undefined) updateData.trackingNumber = trackingNumber;
if (isFulfilled !== undefined) updateData.isFulfilled = isFulfilled;
const updated = await db
@ -2214,6 +2217,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
notes: shopOrdersTable.notes,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
shippingAddress: shopOrdersTable.shippingAddress,
createdAt: shopOrdersTable.createdAt,

View file

@ -499,6 +499,7 @@ shop.get("/orders", async ({ headers }) => {
status: shopOrdersTable.status,
orderType: shopOrdersTable.orderType,
shippingAddress: shopOrdersTable.shippingAddress,
trackingNumber: shopOrdersTable.trackingNumber,
isFulfilled: shopOrdersTable.isFulfilled,
createdAt: shopOrdersTable.createdAt,
itemId: shopItemsTable.id,

View file

@ -62,6 +62,7 @@ export const shopOrdersTable = pgTable("shop_orders", {
shippingAddress: text("shipping_address"),
phone: varchar(),
notes: text(),
trackingNumber: varchar("tracking_number"),
isFulfilled: boolean("is_fulfilled").notNull().default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),

View file

@ -38,6 +38,7 @@
status: string;
orderType: string;
notes: string | null;
trackingNumber: string | null;
isFulfilled: boolean;
shippingAddress: string | null;
phone: string | null;
@ -82,9 +83,19 @@
let actionLoading = $state(false);
let expandedOrders = $state<Record<number, boolean>>({});
let collapsedGroups = $state<Record<string, boolean>>({});
let trackingInputs = $state<Record<number, string>>({});
let lastDeleted = $state<Order | null>(null);
let lastDeletedTimer = $state<number | null>(null);
let lastDeletedError = $state<string | null>(null);
let filterItem = $state('');
let filterUser = $state('');
let uniqueItems = $derived(
[...new Map(orders.map((o) => [o.itemName, o.itemName])).values()].sort()
);
let uniqueUsers = $derived(
[...new Map(orders.map((o) => [o.userId, o.username])).values()].sort()
);
let filteredOrders = $derived.by(() => {
let result =
@ -94,6 +105,14 @@
? orders.filter((o) => !o.isFulfilled)
: orders.filter((o) => o.isFulfilled);
if (filterItem) {
result = result.filter((o) => o.itemName === filterItem);
}
if (filterUser) {
result = result.filter((o) => o.username === filterUser);
}
if (searchQuery.trim()) {
const q = searchQuery.trim().toLowerCase();
result = result.filter((o) => {
@ -181,14 +200,23 @@
async function toggleFulfilled(order: Order) {
actionLoading = true;
try {
const patchBody: Record<string, unknown> = { isFulfilled: !order.isFulfilled };
// Include tracking number when fulfilling (not when unfulfilling)
if (!order.isFulfilled) {
const tracking = trackingInputs[order.id]?.trim() || null;
patchBody.trackingNumber = tracking;
}
const response = await fetch(`${API_URL}/admin/orders/${order.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ isFulfilled: !order.isFulfilled })
body: JSON.stringify(patchBody)
});
if (response.ok) {
orders = orders.map((o) => (o.id === order.id ? { ...o, isFulfilled: !o.isFulfilled } : o));
const trackingValue = !order.isFulfilled ? (trackingInputs[order.id]?.trim() || null) : order.trackingNumber;
orders = orders.map((o) =>
o.id === order.id ? { ...o, isFulfilled: !o.isFulfilled, trackingNumber: trackingValue } : o
);
}
} catch (e) {
console.error('Failed to update order:', e);
@ -381,6 +409,32 @@
class="w-full rounded-full border-4 border-black py-2 pr-4 pl-10 font-bold transition-all duration-200 placeholder:text-gray-400 focus:border-dashed focus:ring-0 focus:outline-none"
/>
</div>
<div class="flex flex-col">
<label for="filter-item" class="mb-1 text-xs font-bold text-gray-500 uppercase">item</label>
<select
id="filter-item"
bind:value={filterItem}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none {filterItem ? 'bg-black text-white' : ''}"
>
<option value="">all items</option>
{#each uniqueItems as item}
<option value={item}>{item}</option>
{/each}
</select>
</div>
<div class="flex flex-col">
<label for="filter-user" class="mb-1 text-xs font-bold text-gray-500 uppercase">user</label>
<select
id="filter-user"
bind:value={filterUser}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none {filterUser ? 'bg-black text-white' : ''}"
>
<option value="">all users</option>
{#each uniqueUsers as username}
<option value={username}>@{username}</option>
{/each}
</select>
</div>
<div class="flex items-end gap-2">
<div class="flex flex-col">
<label for="date-from" class="mb-1 text-xs font-bold text-gray-500 uppercase">from</label>
@ -400,14 +454,16 @@
class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none"
/>
</div>
{#if dateFrom || dateTo}
{#if dateFrom || dateTo || filterItem || filterUser}
<button
onclick={() => {
dateFrom = '';
dateTo = '';
filterItem = '';
filterUser = '';
}}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed"
title="clear dates"
title="clear filters"
>
<X size={18} />
</button>
@ -600,6 +656,19 @@
<!-- Actions -->
<div class="flex shrink-0 flex-col gap-3 md:w-48">
{#if !order.isFulfilled}
<input
type="text"
placeholder="tracking # (optional)"
bind:value={trackingInputs[order.id]}
class="w-full rounded-lg border-2 border-black px-3 py-2 text-sm font-bold transition-all duration-200 placeholder:text-gray-400 focus:border-dashed focus:outline-none"
/>
{:else if order.trackingNumber}
<div class="rounded-lg border-2 border-gray-300 bg-gray-50 px-3 py-2 text-sm">
<p class="text-xs font-bold text-gray-500 uppercase">tracking</p>
<p class="font-bold break-all">{order.trackingNumber}</p>
</div>
{/if}
<button
onclick={() => toggleFulfilled(order)}
class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 {order.isFulfilled

View file

@ -23,6 +23,7 @@
status: string;
orderType: string;
shippingAddress: string | null;
trackingNumber: string | null;
isFulfilled: boolean;
createdAt: string;
itemId: number;
@ -256,13 +257,20 @@
{#if order.shippingAddress}
<div class="mt-3 flex items-start gap-2 text-sm text-gray-600">
<MapPin size={16} class="mt-0.5 shrink-0" />
<span class="break-words">{formatAddress(order.shippingAddress)}</span>
<span class="wrap-break-word">{formatAddress(order.shippingAddress)}</span>
</div>
{:else if !order.isFulfilled}
<p class="mt-3 text-sm font-bold text-yellow-600">
{$t.orders.noShippingAddress}
</p>
{/if}
{#if order.trackingNumber}
<div class="mt-3 flex items-start gap-2 text-sm text-gray-600">
<Truck size={16} class="mt-0.5 shrink-0" />
<span class="font-bold">tracking: {order.trackingNumber}</span>
</div>
{/if}
</div>
</div>
</div>