datewise/app/routes/app.locations._index.tsx
metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

91 lines
3.7 KiB
TypeScript

import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router";
import { redirect, useLoaderData } from "react-router";
import { Form } from "react-router";
import prisma from "../db.server";
import { METHODS } from "../engine/types";
import { adminContext } from "../lib/admin.server";
import { METHOD_LABEL } from "../lib/format";
import { bumpConfig } from "../services/config.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { cfg, features } = await adminContext(request);
return {
locations: cfg.locations.map((l) => ({
id: l.id,
name: l.name,
address: l.address,
enabled: l.enabled,
priority: l.priority,
methods: METHODS.filter((m) => l.methods[m]?.enabled),
zones: l.zones.length,
})),
maxLocations: features.maxLocations,
};
};
export const action = async ({ request }: ActionFunctionArgs) => {
const { cfg, features } = await adminContext(request);
if (cfg.locations.length >= features.maxLocations) return { error: `Your plan includes ${features.maxLocations} location(s). Upgrade in Settings to add more.` };
const hours = Object.fromEntries([1, 2, 3, 4, 5, 6].map((d) => [d, [{ start: "10:00", end: "18:00" }]]));
const loc = await prisma.location.create({
data: {
shopId: cfg.shop.id,
name: "New location",
address: "",
timezone: cfg.shop.timezone,
enabled: false,
priority: cfg.locations.length + 1,
methods: {
pickup: { enabled: true, weeklyHours: hours, slotMode: "generated", slotMinutes: 60, prepMinutes: 120, prepMode: "operating", sameDayCutoff: "14:00", horizonDays: 30, slotCapacity: 5 },
},
},
});
await bumpConfig(cfg.shop.id, cfg.shop.domain);
return redirect(`/app/locations/${loc.id}`);
};
export default function Locations() {
const { locations, maxLocations } = useLoaderData<typeof loader>();
return (
<s-page heading="Locations & hours">
<Form method="post" slot="primary-action">
<s-button type="submit" variant="primary" disabled={locations.length >= maxLocations}>Add location</s-button>
</Form>
<s-section padding="none">
<s-table>
<s-table-header-row>
<s-table-header listSlot="primary">Location</s-table-header>
<s-table-header>Offers</s-table-header>
<s-table-header>Delivery zones</s-table-header>
<s-table-header>Priority</s-table-header>
<s-table-header>Status</s-table-header>
</s-table-header-row>
<s-table-body>
{locations.map((l) => (
<s-table-row key={l.id} clickDelegate={`l-${l.id}`}>
<s-table-cell>
<s-stack gap="small-300">
<s-link id={`l-${l.id}`} href={`/app/locations/${l.id}`}>{l.name}</s-link>
<s-text color="subdued">{l.address || "No address yet"}</s-text>
</s-stack>
</s-table-cell>
<s-table-cell>{l.methods.map((m) => METHOD_LABEL[m]).join(", ") || "Nothing yet"}</s-table-cell>
<s-table-cell>{l.zones}</s-table-cell>
<s-table-cell>{l.priority}</s-table-cell>
<s-table-cell>
<s-badge tone={l.enabled ? "success" : "neutral"}>{l.enabled ? "Taking bookings" : "Off"}</s-badge>
</s-table-cell>
</s-table-row>
))}
</s-table-body>
</s-table>
</s-section>
<s-section>
<s-paragraph>
Locations were imported from Shopify and start switched off. Check the hours, then turn each one on. When several locations can take an order, customers are offered them in priority order (1 first).
</s-paragraph>
</s-section>
</s-page>
);
}