distributor screen functional
This commit is contained in:
parent
f733c1e36b
commit
11eca48354
18 changed files with 936 additions and 18 deletions
Binary file not shown.
|
|
@ -1,13 +1,19 @@
|
||||||
import {
|
import {
|
||||||
createStorageSpacesRepo,
|
createStorageSpacesRepo,
|
||||||
|
createDistributorsRepo,
|
||||||
type StorageSpacesRepo,
|
type StorageSpacesRepo,
|
||||||
|
type DistributorsRepo,
|
||||||
} from "data-manager-sqlite";
|
} from "data-manager-sqlite";
|
||||||
|
|
||||||
export class DataManager {
|
export class DataManager {
|
||||||
readonly storageSpaces: StorageSpacesRepo;
|
readonly storageSpaces: StorageSpacesRepo;
|
||||||
|
readonly distributors: DistributorsRepo;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const { repo } = createStorageSpacesRepo();
|
const { repo: storageSpacesRepo } = createStorageSpacesRepo();
|
||||||
this.storageSpaces = repo;
|
const { repo: distributorsRepo } = createDistributorsRepo();
|
||||||
|
|
||||||
|
this.storageSpaces = storageSpacesRepo;
|
||||||
|
this.distributors = distributorsRepo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
apps/backend/src/trpc/pharmanager/v1/distributor.ts
Normal file
63
apps/backend/src/trpc/pharmanager/v1/distributor.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { t } from "../../init";
|
||||||
|
import { dataManager } from "../../../lib/data-manager-instance";
|
||||||
|
|
||||||
|
export const DistributorSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
agency: z.string(),
|
||||||
|
contact: z.string(),
|
||||||
|
mobile: z.string(),
|
||||||
|
address: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { shape } = DistributorSchema;
|
||||||
|
|
||||||
|
export const CreateDistributorInput = z.object({
|
||||||
|
agency: shape.agency.min(1),
|
||||||
|
contact: shape.contact.min(1),
|
||||||
|
mobile: shape.mobile.length(10),
|
||||||
|
address: shape.address.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateDistributorInput = z
|
||||||
|
.object({ id: z.number().int() })
|
||||||
|
.merge(CreateDistributorInput.partial());
|
||||||
|
|
||||||
|
export type Distributor = z.infer<typeof DistributorSchema>;
|
||||||
|
|
||||||
|
export const distributorRouter = t.router({
|
||||||
|
list: t.procedure
|
||||||
|
.output(z.array(DistributorSchema))
|
||||||
|
.query(() => dataManager.distributors.getDistributors()),
|
||||||
|
|
||||||
|
byId: t.procedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(DistributorSchema.nullable())
|
||||||
|
.query(({ input }) =>
|
||||||
|
dataManager.distributors.getDistributorById(input.id),
|
||||||
|
),
|
||||||
|
|
||||||
|
create: t.procedure
|
||||||
|
.input(CreateDistributorInput)
|
||||||
|
.output(DistributorSchema)
|
||||||
|
.mutation(({ input }) =>
|
||||||
|
dataManager.distributors.createDistributor(input),
|
||||||
|
),
|
||||||
|
|
||||||
|
update: t.procedure
|
||||||
|
.input(UpdateDistributorInput)
|
||||||
|
.output(DistributorSchema.nullable())
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
const { id, ...patch } = input;
|
||||||
|
return dataManager.distributors.updateDistributor(id, patch);
|
||||||
|
}),
|
||||||
|
|
||||||
|
remove: t.procedure
|
||||||
|
.input(z.object({ id: z.number().int() }))
|
||||||
|
.output(z.object({ ok: z.boolean() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const ok =
|
||||||
|
await dataManager.distributors.deleteDistributor(input.id);
|
||||||
|
return { ok };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { t } from "./init";
|
import { t } from "./init";
|
||||||
import { storageRouter } from "./pharmanager/v1/storage";
|
import { storageRouter } from "./pharmanager/v1/storage";
|
||||||
|
import { distributorRouter } from "./pharmanager/v1/distributor";
|
||||||
|
|
||||||
export const appRouter = t.router({
|
export const appRouter = t.router({
|
||||||
storage: storageRouter,
|
storage: storageRouter,
|
||||||
|
distributor: distributorRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,11 @@ import { Route as CustomersRouteImport } from './routes/customers'
|
||||||
import { Route as BillingRouteImport } from './routes/billing'
|
import { Route as BillingRouteImport } from './routes/billing'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as StorageIndexRouteImport } from './routes/storage/index'
|
import { Route as StorageIndexRouteImport } from './routes/storage/index'
|
||||||
|
import { Route as DistributorsIndexRouteImport } from './routes/distributors/index'
|
||||||
import { Route as StorageAddRouteImport } from './routes/storage/add'
|
import { Route as StorageAddRouteImport } from './routes/storage/add'
|
||||||
import { Route as StorageIdRouteImport } from './routes/storage/$id'
|
import { Route as StorageIdRouteImport } from './routes/storage/$id'
|
||||||
|
import { Route as DistributorsAddRouteImport } from './routes/distributors/add'
|
||||||
|
import { Route as DistributorsIdRouteImport } from './routes/distributors/$id'
|
||||||
|
|
||||||
const StorageRoute = StorageRouteImport.update({
|
const StorageRoute = StorageRouteImport.update({
|
||||||
id: '/storage',
|
id: '/storage',
|
||||||
|
|
@ -72,6 +75,11 @@ const StorageIndexRoute = StorageIndexRouteImport.update({
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => StorageRoute,
|
getParentRoute: () => StorageRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DistributorsIndexRoute = DistributorsIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => DistributorsRoute,
|
||||||
|
} as any)
|
||||||
const StorageAddRoute = StorageAddRouteImport.update({
|
const StorageAddRoute = StorageAddRouteImport.update({
|
||||||
id: '/add',
|
id: '/add',
|
||||||
path: '/add',
|
path: '/add',
|
||||||
|
|
@ -82,32 +90,47 @@ const StorageIdRoute = StorageIdRouteImport.update({
|
||||||
path: '/$id',
|
path: '/$id',
|
||||||
getParentRoute: () => StorageRoute,
|
getParentRoute: () => StorageRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const DistributorsAddRoute = DistributorsAddRouteImport.update({
|
||||||
|
id: '/add',
|
||||||
|
path: '/add',
|
||||||
|
getParentRoute: () => DistributorsRoute,
|
||||||
|
} as any)
|
||||||
|
const DistributorsIdRoute = DistributorsIdRouteImport.update({
|
||||||
|
id: '/$id',
|
||||||
|
path: '/$id',
|
||||||
|
getParentRoute: () => DistributorsRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/distributors': typeof DistributorsRoute
|
'/distributors': typeof DistributorsRouteWithChildren
|
||||||
'/products': typeof ProductsRoute
|
'/products': typeof ProductsRoute
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
'/stock': typeof StockRoute
|
||||||
'/storage': typeof StorageRouteWithChildren
|
'/storage': typeof StorageRouteWithChildren
|
||||||
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
|
'/distributors/': typeof DistributorsIndexRoute
|
||||||
'/storage/': typeof StorageIndexRoute
|
'/storage/': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/distributors': typeof DistributorsRoute
|
|
||||||
'/products': typeof ProductsRoute
|
'/products': typeof ProductsRoute
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
'/stock': typeof StockRoute
|
||||||
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
|
'/distributors': typeof DistributorsIndexRoute
|
||||||
'/storage': typeof StorageIndexRoute
|
'/storage': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
|
|
@ -115,14 +138,17 @@ export interface FileRoutesById {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/billing': typeof BillingRoute
|
'/billing': typeof BillingRoute
|
||||||
'/customers': typeof CustomersRoute
|
'/customers': typeof CustomersRoute
|
||||||
'/distributors': typeof DistributorsRoute
|
'/distributors': typeof DistributorsRouteWithChildren
|
||||||
'/products': typeof ProductsRoute
|
'/products': typeof ProductsRoute
|
||||||
'/profile': typeof ProfileRoute
|
'/profile': typeof ProfileRoute
|
||||||
'/staff': typeof StaffRoute
|
'/staff': typeof StaffRoute
|
||||||
'/stock': typeof StockRoute
|
'/stock': typeof StockRoute
|
||||||
'/storage': typeof StorageRouteWithChildren
|
'/storage': typeof StorageRouteWithChildren
|
||||||
|
'/distributors/$id': typeof DistributorsIdRoute
|
||||||
|
'/distributors/add': typeof DistributorsAddRoute
|
||||||
'/storage/$id': typeof StorageIdRoute
|
'/storage/$id': typeof StorageIdRoute
|
||||||
'/storage/add': typeof StorageAddRoute
|
'/storage/add': typeof StorageAddRoute
|
||||||
|
'/distributors/': typeof DistributorsIndexRoute
|
||||||
'/storage/': typeof StorageIndexRoute
|
'/storage/': typeof StorageIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
|
|
@ -137,21 +163,26 @@ export interface FileRouteTypes {
|
||||||
| '/staff'
|
| '/staff'
|
||||||
| '/stock'
|
| '/stock'
|
||||||
| '/storage'
|
| '/storage'
|
||||||
|
| '/distributors/$id'
|
||||||
|
| '/distributors/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
|
| '/distributors/'
|
||||||
| '/storage/'
|
| '/storage/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/billing'
|
| '/billing'
|
||||||
| '/customers'
|
| '/customers'
|
||||||
| '/distributors'
|
|
||||||
| '/products'
|
| '/products'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/staff'
|
| '/staff'
|
||||||
| '/stock'
|
| '/stock'
|
||||||
|
| '/distributors/$id'
|
||||||
|
| '/distributors/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
|
| '/distributors'
|
||||||
| '/storage'
|
| '/storage'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
|
|
@ -164,8 +195,11 @@ export interface FileRouteTypes {
|
||||||
| '/staff'
|
| '/staff'
|
||||||
| '/stock'
|
| '/stock'
|
||||||
| '/storage'
|
| '/storage'
|
||||||
|
| '/distributors/$id'
|
||||||
|
| '/distributors/add'
|
||||||
| '/storage/$id'
|
| '/storage/$id'
|
||||||
| '/storage/add'
|
| '/storage/add'
|
||||||
|
| '/distributors/'
|
||||||
| '/storage/'
|
| '/storage/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +207,7 @@ export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
BillingRoute: typeof BillingRoute
|
BillingRoute: typeof BillingRoute
|
||||||
CustomersRoute: typeof CustomersRoute
|
CustomersRoute: typeof CustomersRoute
|
||||||
DistributorsRoute: typeof DistributorsRoute
|
DistributorsRoute: typeof DistributorsRouteWithChildren
|
||||||
ProductsRoute: typeof ProductsRoute
|
ProductsRoute: typeof ProductsRoute
|
||||||
ProfileRoute: typeof ProfileRoute
|
ProfileRoute: typeof ProfileRoute
|
||||||
StaffRoute: typeof StaffRoute
|
StaffRoute: typeof StaffRoute
|
||||||
|
|
@ -253,6 +287,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof StorageIndexRouteImport
|
preLoaderRoute: typeof StorageIndexRouteImport
|
||||||
parentRoute: typeof StorageRoute
|
parentRoute: typeof StorageRoute
|
||||||
}
|
}
|
||||||
|
'/distributors/': {
|
||||||
|
id: '/distributors/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/distributors/'
|
||||||
|
preLoaderRoute: typeof DistributorsIndexRouteImport
|
||||||
|
parentRoute: typeof DistributorsRoute
|
||||||
|
}
|
||||||
'/storage/add': {
|
'/storage/add': {
|
||||||
id: '/storage/add'
|
id: '/storage/add'
|
||||||
path: '/add'
|
path: '/add'
|
||||||
|
|
@ -267,9 +308,39 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof StorageIdRouteImport
|
preLoaderRoute: typeof StorageIdRouteImport
|
||||||
parentRoute: typeof StorageRoute
|
parentRoute: typeof StorageRoute
|
||||||
}
|
}
|
||||||
|
'/distributors/add': {
|
||||||
|
id: '/distributors/add'
|
||||||
|
path: '/add'
|
||||||
|
fullPath: '/distributors/add'
|
||||||
|
preLoaderRoute: typeof DistributorsAddRouteImport
|
||||||
|
parentRoute: typeof DistributorsRoute
|
||||||
|
}
|
||||||
|
'/distributors/$id': {
|
||||||
|
id: '/distributors/$id'
|
||||||
|
path: '/$id'
|
||||||
|
fullPath: '/distributors/$id'
|
||||||
|
preLoaderRoute: typeof DistributorsIdRouteImport
|
||||||
|
parentRoute: typeof DistributorsRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DistributorsRouteChildren {
|
||||||
|
DistributorsIdRoute: typeof DistributorsIdRoute
|
||||||
|
DistributorsAddRoute: typeof DistributorsAddRoute
|
||||||
|
DistributorsIndexRoute: typeof DistributorsIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const DistributorsRouteChildren: DistributorsRouteChildren = {
|
||||||
|
DistributorsIdRoute: DistributorsIdRoute,
|
||||||
|
DistributorsAddRoute: DistributorsAddRoute,
|
||||||
|
DistributorsIndexRoute: DistributorsIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DistributorsRouteWithChildren = DistributorsRoute._addFileChildren(
|
||||||
|
DistributorsRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
interface StorageRouteChildren {
|
interface StorageRouteChildren {
|
||||||
StorageIdRoute: typeof StorageIdRoute
|
StorageIdRoute: typeof StorageIdRoute
|
||||||
StorageAddRoute: typeof StorageAddRoute
|
StorageAddRoute: typeof StorageAddRoute
|
||||||
|
|
@ -289,7 +360,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
BillingRoute: BillingRoute,
|
BillingRoute: BillingRoute,
|
||||||
CustomersRoute: CustomersRoute,
|
CustomersRoute: CustomersRoute,
|
||||||
DistributorsRoute: DistributorsRoute,
|
DistributorsRoute: DistributorsRouteWithChildren,
|
||||||
ProductsRoute: ProductsRoute,
|
ProductsRoute: ProductsRoute,
|
||||||
ProfileRoute: ProfileRoute,
|
ProfileRoute: ProfileRoute,
|
||||||
StaffRoute: StaffRoute,
|
StaffRoute: StaffRoute,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute("/distributors")({
|
export const Route = createFileRoute("/distributors")({
|
||||||
component: DistributorsPage,
|
component: () => <Outlet />,
|
||||||
staticData: {
|
|
||||||
title: "Distributors",
|
|
||||||
subtitle: "Agency management with contact details",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function DistributorsPage() {
|
|
||||||
return <div>Distributors</div>;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
146
apps/pharmanager/src/routes/distributors/$id.tsx
Normal file
146
apps/pharmanager/src/routes/distributors/$id.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Pencil, Trash2, Truck } from "lucide-react";
|
||||||
|
import {
|
||||||
|
useGetDistributorById,
|
||||||
|
useRemoveDistributor,
|
||||||
|
trpc,
|
||||||
|
} from "shared-react";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/distributors/$id")({
|
||||||
|
component: DistributorDetailsPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Distributor Details",
|
||||||
|
subtitle: "Distributor information",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function DistributorDetailsPage() {
|
||||||
|
const { id } = Route.useParams();
|
||||||
|
const distributorId = Number(id);
|
||||||
|
const { data: distributor, isLoading, error } =
|
||||||
|
useGetDistributorById(distributorId);
|
||||||
|
const removeMutation = useRemoveDistributor();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (!distributor) return;
|
||||||
|
if (!confirm(`Delete ${distributor.agency}?`)) return;
|
||||||
|
removeMutation.mutate(
|
||||||
|
{ id: distributor.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.distributor.list.invalidate();
|
||||||
|
window.location.href = "/distributors";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-slate-600 py-8">
|
||||||
|
Loading distributor details...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !distributor) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||||
|
<Truck className="w-12 h-12 mb-4 opacity-40" />
|
||||||
|
<h3 className="text-base font-semibold text-slate-900 mb-1.5">
|
||||||
|
Distributor not found
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm mb-4">
|
||||||
|
The distributor you're looking for doesn't exist.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
to="/distributors"
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Back to Distributors
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/distributors"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to Distributors
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-7 mb-5">
|
||||||
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-slate-600 uppercase tracking-wider mb-4">
|
||||||
|
<Truck className="w-[18px] h-[18px]" />
|
||||||
|
Distributor Info
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-slate-600">Agency Name</span>
|
||||||
|
<span className="text-[15px] font-medium text-slate-900">
|
||||||
|
{distributor.agency}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-slate-600">
|
||||||
|
Distributor ID
|
||||||
|
</span>
|
||||||
|
<span className="text-[15px] font-medium font-mono text-slate-900">
|
||||||
|
DIST-{String(distributor.id).padStart(3, "0")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-slate-600">
|
||||||
|
Contact Person
|
||||||
|
</span>
|
||||||
|
<span className="text-[15px] font-medium text-slate-900">
|
||||||
|
{distributor.contact}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-slate-600">
|
||||||
|
Contact Mobile
|
||||||
|
</span>
|
||||||
|
<span className="text-[15px] font-medium font-mono text-slate-900">
|
||||||
|
{distributor.mobile}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full mt-2 flex flex-col gap-0.5">
|
||||||
|
<span className="text-xs text-slate-600">Address</span>
|
||||||
|
<span className="text-sm text-slate-700 leading-relaxed">
|
||||||
|
{distributor.address || "No address provided"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5 mt-6 pt-5 border-t border-slate-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Pencil className="w-[15px] h-[15px]" />
|
||||||
|
Edit Distributor
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-[15px] h-[15px]" />
|
||||||
|
Delete Distributor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
181
apps/pharmanager/src/routes/distributors/add.tsx
Normal file
181
apps/pharmanager/src/routes/distributors/add.tsx
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
|
||||||
|
import { ArrowLeft, Plus } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useCreateDistributor, trpc } from "shared-react";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
agency: z.string().min(1, "Agency name is required"),
|
||||||
|
contact: z.string().min(1, "Contact person is required"),
|
||||||
|
mobile: z
|
||||||
|
.string()
|
||||||
|
.length(10, "Valid 10-digit mobile number is required")
|
||||||
|
.regex(/^\d{10}$/, "Must be 10 digits"),
|
||||||
|
address: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/distributors/add")({
|
||||||
|
component: AddDistributorPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Add Distributor",
|
||||||
|
subtitle: "Register a new distributor agency",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function AddDistributorPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createMutation = useCreateDistributor();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
agency: "",
|
||||||
|
contact: "",
|
||||||
|
mobile: "",
|
||||||
|
address: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(values: FormValues) {
|
||||||
|
createMutation.mutate(
|
||||||
|
{
|
||||||
|
agency: values.agency,
|
||||||
|
contact: values.contact,
|
||||||
|
mobile: values.mobile,
|
||||||
|
address: values.address ?? null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.distributor.list.invalidate();
|
||||||
|
navigate({ to: "/distributors" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
to="/distributors"
|
||||||
|
className="inline-flex items-center gap-1.5 text-sm text-blue-600 hover:underline mb-5"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Back to Distributors
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="bg-white rounded-lg shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04)] p-8 max-w-2xl"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 gap-5">
|
||||||
|
<div className="col-span-full">
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Agency Name{" "}
|
||||||
|
<span className="text-red-600 ml-0.5">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register("agency")}
|
||||||
|
placeholder="e.g. MediDistributors"
|
||||||
|
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.agency ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||||
|
/>
|
||||||
|
{errors.agency && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{errors.agency.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Contact Person{" "}
|
||||||
|
<span className="text-red-600 ml-0.5">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register("contact")}
|
||||||
|
placeholder="e.g. Rahul Mehta"
|
||||||
|
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.contact ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||||
|
/>
|
||||||
|
{errors.contact && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{errors.contact.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Contact Mobile{" "}
|
||||||
|
<span className="text-red-600 ml-0.5">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
{...register("mobile", {
|
||||||
|
onChange: (e) => {
|
||||||
|
e.target.value = e.target.value
|
||||||
|
.replace(/\D/g, "")
|
||||||
|
.slice(0, 10);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
placeholder="e.g. 9876500001"
|
||||||
|
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.mobile ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||||
|
/>
|
||||||
|
{errors.mobile && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{errors.mobile.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full">
|
||||||
|
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||||
|
Address
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
{...register("address", {
|
||||||
|
setValueAs: (v: string) => v || null,
|
||||||
|
})}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Full address of the distributor"
|
||||||
|
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white resize-y min-h-[80px] transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-5 mt-2 border-t border-slate-200">
|
||||||
|
<Link
|
||||||
|
to="/distributors"
|
||||||
|
className="inline-flex items-center px-5 py-2.5 border border-slate-200 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
className="inline-flex items-center gap-1.5 px-6 py-2.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-[15px] h-[15px]" />
|
||||||
|
{createMutation.isPending
|
||||||
|
? "Saving..."
|
||||||
|
: "Save Distributor"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{createMutation.error && (
|
||||||
|
<p className="text-sm text-red-600 mt-4">
|
||||||
|
Failed to create distributor. Please try again.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
apps/pharmanager/src/routes/distributors/index.tsx
Normal file
190
apps/pharmanager/src/routes/distributors/index.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
import { Search, Plus, Pencil, Trash2, Truck } from "lucide-react";
|
||||||
|
import { GridTable } from "#/components/GridTable";
|
||||||
|
import type { GridTableColumn } from "#/components/GridTable";
|
||||||
|
import {
|
||||||
|
useListDistributors,
|
||||||
|
useRemoveDistributor,
|
||||||
|
trpc,
|
||||||
|
} from "shared-react";
|
||||||
|
|
||||||
|
interface DistributorRow {
|
||||||
|
id: number;
|
||||||
|
agency: string;
|
||||||
|
contact: string;
|
||||||
|
mobile: string;
|
||||||
|
address: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeColumns(
|
||||||
|
onDelete: (row: DistributorRow) => void,
|
||||||
|
): GridTableColumn<DistributorRow>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "agency",
|
||||||
|
header: "Agency Name",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Link
|
||||||
|
to="/distributors/$id"
|
||||||
|
params={{ id: row.id.toString() }}
|
||||||
|
className="font-medium text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{row.agency}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contact",
|
||||||
|
header: "Contact Person",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm">{row.contact}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mobile",
|
||||||
|
header: "Contact Mobile",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-slate-600 font-mono">
|
||||||
|
{row.mobile}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "address",
|
||||||
|
header: "Address",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span
|
||||||
|
className="text-sm text-slate-600 max-w-[200px] truncate block"
|
||||||
|
title={row.address ?? undefined}
|
||||||
|
>
|
||||||
|
{row.address || "—"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Actions",
|
||||||
|
size: 90,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition-colors"
|
||||||
|
aria-label={`Edit ${row.agency}`}
|
||||||
|
>
|
||||||
|
<Pencil className="w-[15px] h-[15px]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 transition-colors"
|
||||||
|
aria-label={`Delete ${row.agency}`}
|
||||||
|
onClick={() => onDelete(row)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-[15px] h-[15px]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/distributors/")({
|
||||||
|
component: DistributorsIndexPage,
|
||||||
|
staticData: {
|
||||||
|
title: "Distributors",
|
||||||
|
subtitle: "Manage product distributors & agencies",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function DistributorsIndexPage() {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const { data: distributors, isLoading, error } = useListDistributors();
|
||||||
|
const removeMutation = useRemoveDistributor();
|
||||||
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(row: DistributorRow) => {
|
||||||
|
if (!confirm(`Delete ${row.agency}?`)) return;
|
||||||
|
removeMutation.mutate(
|
||||||
|
{ id: row.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => utils.distributor.list.invalidate(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[removeMutation, utils],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(() => makeColumns(handleDelete), [handleDelete]);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = searchQuery.toLowerCase().trim();
|
||||||
|
if (!q) return distributors ?? [];
|
||||||
|
return (distributors ?? []).filter((d) => {
|
||||||
|
return (
|
||||||
|
d.agency.toLowerCase().includes(q) ||
|
||||||
|
d.contact.toLowerCase().includes(q) ||
|
||||||
|
d.mobile.includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [searchQuery, distributors]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-slate-600 py-8">
|
||||||
|
Loading distributors...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-red-600 py-8">
|
||||||
|
Failed to load distributors.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3 mb-5 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2 flex-1 min-w-[200px] max-w-[400px] px-3.5 py-2 bg-white rounded-md border border-slate-200 transition-all duration-200 focus-within:border-blue-600 focus-within:shadow-[0_0_0_3px_rgba(37,99,235,0.1)]">
|
||||||
|
<Search className="w-4 h-4 text-slate-600 shrink-0" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search distributors..."
|
||||||
|
className="bg-transparent border-none outline-none text-sm text-slate-900 w-full placeholder:text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/distributors/add"
|
||||||
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-[15px] h-[15px]" />
|
||||||
|
Add Distributor
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GridTable
|
||||||
|
columns={columns}
|
||||||
|
data={filtered}
|
||||||
|
emptyState={
|
||||||
|
<div className="flex flex-col items-center py-16 px-6 text-slate-600">
|
||||||
|
<Truck className="w-12 h-12 mb-4 opacity-40" />
|
||||||
|
<h3 className="text-base font-semibold text-slate-900 mb-1.5">
|
||||||
|
No distributors found
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm">
|
||||||
|
{searchQuery
|
||||||
|
? "No distributors match your search. Try a different term."
|
||||||
|
: "Add your first distributor to get started."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE `distributors` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`agency` text NOT NULL,
|
||||||
|
`contact` text NOT NULL,
|
||||||
|
`mobile` text NOT NULL,
|
||||||
|
`address` text
|
||||||
|
);
|
||||||
110
packages/data-manager-sqlite/drizzle/meta/0001_snapshot.json
Normal file
110
packages/data-manager-sqlite/drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "73855818-f116-4251-b91d-129f185c5d16",
|
||||||
|
"prevId": "0a690373-295e-498e-a867-29ce0949f57d",
|
||||||
|
"tables": {
|
||||||
|
"storage_spaces": {
|
||||||
|
"name": "storage_spaces",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"name": "aliases",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
},
|
||||||
|
"image_urls": {
|
||||||
|
"name": "image_urls",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'[]'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"distributors": {
|
||||||
|
"name": "distributors",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"agency": {
|
||||||
|
"name": "agency",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"name": "contact",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"name": "mobile",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"name": "address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
"when": 1779522854247,
|
"when": 1779522854247,
|
||||||
"tag": "0000_normal_mephisto",
|
"tag": "0000_normal_mephisto",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1779527160219,
|
||||||
|
"tag": "0001_overjoyed_dakota_north",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
105
packages/data-manager-sqlite/src/distributors.ts
Normal file
105
packages/data-manager-sqlite/src/distributors.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import { db } from './db-instance'
|
||||||
|
import { distributors } from './schema/distributors'
|
||||||
|
|
||||||
|
export type Distributor = {
|
||||||
|
id: number
|
||||||
|
agency: string
|
||||||
|
contact: string
|
||||||
|
mobile: string
|
||||||
|
address: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateDistributorInput = {
|
||||||
|
agency: string
|
||||||
|
contact: string
|
||||||
|
mobile: string
|
||||||
|
address?: Distributor['address']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateDistributorPatch = Partial<CreateDistributorInput>
|
||||||
|
|
||||||
|
export type DistributorsRepo = {
|
||||||
|
getDistributors: () => Promise<Distributor[]>
|
||||||
|
getDistributorById: (id: number) => Promise<Distributor | null>
|
||||||
|
createDistributor: (input: CreateDistributorInput) => Promise<Distributor>
|
||||||
|
updateDistributor: (id: number, patch: UpdateDistributorPatch) => Promise<Distributor | null>
|
||||||
|
deleteDistributor: (id: number) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDistributor(row: {
|
||||||
|
id: number
|
||||||
|
agency: string
|
||||||
|
contact: string
|
||||||
|
mobile: string
|
||||||
|
address: string | null
|
||||||
|
}): Distributor {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
agency: row.agency,
|
||||||
|
contact: row.contact,
|
||||||
|
mobile: row.mobile,
|
||||||
|
address: row.address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDistributorsRepo(): {
|
||||||
|
repo: DistributorsRepo
|
||||||
|
} {
|
||||||
|
const repo: DistributorsRepo = {
|
||||||
|
getDistributors() {
|
||||||
|
const rows = db.select().from(distributors).all()
|
||||||
|
return Promise.resolve(rows.map(toDistributor))
|
||||||
|
},
|
||||||
|
|
||||||
|
getDistributorById(id) {
|
||||||
|
const row = db
|
||||||
|
.select()
|
||||||
|
.from(distributors)
|
||||||
|
.where(eq(distributors.id, id))
|
||||||
|
.get()
|
||||||
|
return Promise.resolve(row ? toDistributor(row) : null)
|
||||||
|
},
|
||||||
|
|
||||||
|
createDistributor(input) {
|
||||||
|
const created = db
|
||||||
|
.insert(distributors)
|
||||||
|
.values({
|
||||||
|
agency: input.agency,
|
||||||
|
contact: input.contact,
|
||||||
|
mobile: input.mobile,
|
||||||
|
address: input.address ?? null,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.get()
|
||||||
|
return Promise.resolve(toDistributor(created))
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDistributor(id, patch) {
|
||||||
|
const updated = db
|
||||||
|
.update(distributors)
|
||||||
|
.set({
|
||||||
|
...(patch.agency !== undefined ? { agency: patch.agency } : {}),
|
||||||
|
...(patch.contact !== undefined ? { contact: patch.contact } : {}),
|
||||||
|
...(patch.mobile !== undefined ? { mobile: patch.mobile } : {}),
|
||||||
|
...(patch.address !== undefined ? { address: patch.address } : {}),
|
||||||
|
})
|
||||||
|
.where(eq(distributors.id, id))
|
||||||
|
.returning()
|
||||||
|
.get()
|
||||||
|
return Promise.resolve(updated ? toDistributor(updated) : null)
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDistributor(id) {
|
||||||
|
const deleted = db
|
||||||
|
.delete(distributors)
|
||||||
|
.where(eq(distributors.id, id))
|
||||||
|
.returning({ id: distributors.id })
|
||||||
|
.get()
|
||||||
|
return Promise.resolve(Boolean(deleted))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return { repo }
|
||||||
|
}
|
||||||
|
|
@ -6,4 +6,10 @@ export {
|
||||||
type StorageSpace,
|
type StorageSpace,
|
||||||
type StorageSpacesRepo,
|
type StorageSpacesRepo,
|
||||||
} from './storageSpaces'
|
} from './storageSpaces'
|
||||||
|
export {
|
||||||
|
createDistributorsRepo,
|
||||||
|
type Distributor,
|
||||||
|
type DistributorsRepo,
|
||||||
|
} from './distributors'
|
||||||
export { storageSpaces } from './schema/storageSpacesSchema'
|
export { storageSpaces } from './schema/storageSpacesSchema'
|
||||||
|
export { distributors } from './schema/distributors'
|
||||||
|
|
|
||||||
9
packages/data-manager-sqlite/src/schema/distributors.ts
Normal file
9
packages/data-manager-sqlite/src/schema/distributors.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||||
|
|
||||||
|
export const distributors = sqliteTable('distributors', {
|
||||||
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
|
agency: text('agency').notNull(),
|
||||||
|
contact: text('contact').notNull(),
|
||||||
|
mobile: text('mobile').notNull(),
|
||||||
|
address: text('address'),
|
||||||
|
})
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './storageSpacesSchema'
|
export * from './storageSpacesSchema'
|
||||||
|
export * from './distributors'
|
||||||
|
|
|
||||||
21
packages/shared-react/src/hooks/distributors.ts
Normal file
21
packages/shared-react/src/hooks/distributors.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { trpc } from "../trpc";
|
||||||
|
|
||||||
|
export function useListDistributors() {
|
||||||
|
return trpc.distributor.list.useQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetDistributorById(id: number) {
|
||||||
|
return trpc.distributor.byId.useQuery({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateDistributor() {
|
||||||
|
return trpc.distributor.create.useMutation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDistributor() {
|
||||||
|
return trpc.distributor.update.useMutation();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRemoveDistributor() {
|
||||||
|
return trpc.distributor.remove.useMutation();
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,5 @@ export * from './query'
|
||||||
export * from './store'
|
export * from './store'
|
||||||
export * from './provider'
|
export * from './provider'
|
||||||
export * from './hooks/storageSpaces'
|
export * from './hooks/storageSpaces'
|
||||||
|
export * from './hooks/distributors'
|
||||||
export { trpc } from './trpc'
|
export { trpc } from './trpc'
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue