@madebylars.com/mbl-order 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -0
- package/dist/module.d.mts +24 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +46 -0
- package/dist/runtime/composables/useDriverOrders.d.ts +1 -0
- package/dist/runtime/composables/useDriverOrders.js +10 -0
- package/dist/runtime/composables/useFleet.d.ts +1 -0
- package/dist/runtime/composables/useFleet.js +16 -0
- package/dist/runtime/composables/useInvoice.d.ts +1 -0
- package/dist/runtime/composables/useInvoice.js +10 -0
- package/dist/runtime/composables/useInvoiceActions.d.ts +5 -0
- package/dist/runtime/composables/useInvoiceActions.js +39 -0
- package/dist/runtime/composables/useOrder.d.ts +1 -0
- package/dist/runtime/composables/useOrder.js +18 -0
- package/dist/runtime/composables/useOrderActions.d.ts +7 -0
- package/dist/runtime/composables/useOrderActions.js +55 -0
- package/dist/runtime/composables/useOrderConfig.d.ts +2 -0
- package/dist/runtime/composables/useOrderConfig.js +4 -0
- package/dist/runtime/composables/useOrderKpis.d.ts +2 -0
- package/dist/runtime/composables/useOrderKpis.js +30 -0
- package/dist/runtime/composables/useOrderLocale.d.ts +7 -0
- package/dist/runtime/composables/useOrderLocale.js +32 -0
- package/dist/runtime/composables/useOrders.d.ts +2 -0
- package/dist/runtime/composables/useOrders.js +11 -0
- package/dist/runtime/composables/useQuote.d.ts +1 -0
- package/dist/runtime/composables/useQuote.js +10 -0
- package/dist/runtime/composables/useQuoteActions.d.ts +6 -0
- package/dist/runtime/composables/useQuoteActions.js +33 -0
- package/dist/runtime/composables/useStorageActions.d.ts +5 -0
- package/dist/runtime/composables/useStorageActions.js +31 -0
- package/dist/runtime/composables/useStoragePeriod.d.ts +1 -0
- package/dist/runtime/composables/useStoragePeriod.js +10 -0
- package/dist/runtime/locales/en.d.ts +84 -0
- package/dist/runtime/locales/en.js +80 -0
- package/dist/runtime/locales/sv.d.ts +3 -0
- package/dist/runtime/locales/sv.js +80 -0
- package/dist/runtime/server/migrations/001_transport_schema.sql +241 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/types.d.mts +9 -0
- package/package.json +82 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Quote, CreateQuotePayload, ActionResult } from '../../types.js';
|
|
2
|
+
export declare function useQuoteActions(): {
|
|
3
|
+
createQuote: (payload: CreateQuotePayload) => Promise<ActionResult<Quote>>;
|
|
4
|
+
acceptQuote: (quoteId: string) => Promise<ActionResult<Quote>>;
|
|
5
|
+
expireQuote: (quoteId: string) => Promise<ActionResult<Quote>>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function useQuoteActions() {
|
|
2
|
+
const client = useSupabaseClient();
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const schema = config.public.mblOrder.supabaseSchema;
|
|
5
|
+
async function createQuote(payload) {
|
|
6
|
+
try {
|
|
7
|
+
const { data, error } = await client.schema(schema).from("quotes").insert({ order_id: payload.orderId, expires_at: payload.expiresAt, notes: payload.notes ?? null }).select().single();
|
|
8
|
+
if (error) throw error;
|
|
9
|
+
return { data, error: null };
|
|
10
|
+
} catch (err) {
|
|
11
|
+
return { data: null, error: err };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function acceptQuote(quoteId) {
|
|
15
|
+
try {
|
|
16
|
+
const { data, error } = await client.schema(schema).from("quotes").update({ accepted_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", quoteId).select().single();
|
|
17
|
+
if (error) throw error;
|
|
18
|
+
return { data, error: null };
|
|
19
|
+
} catch (err) {
|
|
20
|
+
return { data: null, error: err };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function expireQuote(quoteId) {
|
|
24
|
+
try {
|
|
25
|
+
const { data, error } = await client.schema(schema).from("quotes").update({ rejected_at: (/* @__PURE__ */ new Date()).toISOString(), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", quoteId).select().single();
|
|
26
|
+
if (error) throw error;
|
|
27
|
+
return { data, error: null };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
return { data: null, error: err };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { createQuote, acceptQuote, expireQuote };
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { StoragePeriod, ExtendStoragePeriodPayload, ActionResult } from '../../types.js';
|
|
2
|
+
export declare function useStorageActions(): {
|
|
3
|
+
extendPeriod: (payload: ExtendStoragePeriodPayload) => Promise<ActionResult<StoragePeriod>>;
|
|
4
|
+
endPeriod: (orderId: string, endDate?: string) => Promise<ActionResult<StoragePeriod>>;
|
|
5
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function useStorageActions() {
|
|
2
|
+
const client = useSupabaseClient();
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const schema = config.public.mblOrder.supabaseSchema;
|
|
5
|
+
async function extendPeriod(payload) {
|
|
6
|
+
try {
|
|
7
|
+
const { data: existing, error: fetchError } = await client.schema(schema).from("storage_periods").select("id").eq("order_id", payload.orderId).order("start_date", { ascending: false }).limit(1).single();
|
|
8
|
+
if (fetchError) throw fetchError;
|
|
9
|
+
const { data, error } = await client.schema(schema).from("storage_periods").update({ end_date: payload.newEndDate, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select().single();
|
|
10
|
+
if (error) throw error;
|
|
11
|
+
return { data, error: null };
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return { data: null, error: err };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function endPeriod(orderId, endDate) {
|
|
17
|
+
try {
|
|
18
|
+
const { data: existing, error: fetchError } = await client.schema(schema).from("storage_periods").select("id").eq("order_id", orderId).order("start_date", { ascending: false }).limit(1).single();
|
|
19
|
+
if (fetchError) throw fetchError;
|
|
20
|
+
const { data, error } = await client.schema(schema).from("storage_periods").update({
|
|
21
|
+
end_date: endDate ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
22
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23
|
+
}).eq("id", existing.id).select().single();
|
|
24
|
+
if (error) throw error;
|
|
25
|
+
return { data, error: null };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return { data: null, error: err };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { extendPeriod, endPeriod };
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useStoragePeriod(orderId: string): any;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function useStoragePeriod(orderId) {
|
|
2
|
+
return useAsyncData(`mbl-order:storage-period:${orderId}`, async () => {
|
|
3
|
+
const client = useSupabaseClient();
|
|
4
|
+
const config = useRuntimeConfig();
|
|
5
|
+
const schema = config.public.mblOrder.supabaseSchema;
|
|
6
|
+
const { data, error } = await client.schema(schema).from("storage_periods").select("*").eq("order_id", orderId).order("start_date", { ascending: false }).limit(1).maybeSingle();
|
|
7
|
+
if (error) throw error;
|
|
8
|
+
return data;
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
declare const en: {
|
|
2
|
+
readonly mblOrder: {
|
|
3
|
+
readonly status: {
|
|
4
|
+
readonly draft: "Draft";
|
|
5
|
+
readonly quoted: "Quote sent";
|
|
6
|
+
readonly confirmed: "Confirmed";
|
|
7
|
+
readonly assigned: "Driver assigned";
|
|
8
|
+
readonly collected: "Collected";
|
|
9
|
+
readonly in_transit: "In transit";
|
|
10
|
+
readonly delivered: "Delivered";
|
|
11
|
+
readonly invoiced: "Invoiced";
|
|
12
|
+
readonly active: "Active";
|
|
13
|
+
readonly extended: "Extended";
|
|
14
|
+
readonly ended: "Ended";
|
|
15
|
+
};
|
|
16
|
+
readonly orderType: {
|
|
17
|
+
readonly transport: "Transport order";
|
|
18
|
+
readonly storage: "Storage order";
|
|
19
|
+
};
|
|
20
|
+
readonly billingMethod: {
|
|
21
|
+
readonly per_km: "Per km";
|
|
22
|
+
readonly per_hour: "Per hour";
|
|
23
|
+
readonly per_weight: "Per weight";
|
|
24
|
+
readonly fixed: "Fixed rate";
|
|
25
|
+
readonly per_sqm: "Per m²";
|
|
26
|
+
readonly per_pallet: "Per pallet";
|
|
27
|
+
readonly per_period: "Per period";
|
|
28
|
+
readonly fixed_subscription: "Fixed subscription";
|
|
29
|
+
};
|
|
30
|
+
readonly errors: {
|
|
31
|
+
readonly invalidTransition: "This status change is not allowed";
|
|
32
|
+
readonly orderNotFound: "Order not found";
|
|
33
|
+
readonly quoteExpired: "This quote has expired";
|
|
34
|
+
readonly driverUnavailable: "Driver is not available";
|
|
35
|
+
readonly invoiceAlreadyExists: "An invoice already exists for this order";
|
|
36
|
+
readonly periodNotFound: "No active storage period found";
|
|
37
|
+
};
|
|
38
|
+
readonly invoice: {
|
|
39
|
+
readonly title: "Invoice";
|
|
40
|
+
readonly generated: "Invoice generated";
|
|
41
|
+
readonly lineItem: "Description";
|
|
42
|
+
readonly quantity: "Qty";
|
|
43
|
+
readonly unitPrice: "Unit price";
|
|
44
|
+
readonly vat: "VAT";
|
|
45
|
+
readonly total: "Total";
|
|
46
|
+
readonly subtotal: "Subtotal";
|
|
47
|
+
readonly dueDate: "Due date";
|
|
48
|
+
readonly issueDate: "Issue date";
|
|
49
|
+
readonly status: {
|
|
50
|
+
readonly draft: "Draft";
|
|
51
|
+
readonly sent: "Sent";
|
|
52
|
+
readonly paid: "Paid";
|
|
53
|
+
readonly overdue: "Overdue";
|
|
54
|
+
readonly void: "Void";
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
readonly quote: {
|
|
58
|
+
readonly expires: "Expires";
|
|
59
|
+
readonly accepted: "Quote accepted";
|
|
60
|
+
readonly expired: "Quote expired";
|
|
61
|
+
readonly noQuote: "No quote issued yet";
|
|
62
|
+
};
|
|
63
|
+
readonly fleet: {
|
|
64
|
+
readonly active: "On job";
|
|
65
|
+
readonly idle: "Available";
|
|
66
|
+
readonly offline: "Offline";
|
|
67
|
+
};
|
|
68
|
+
readonly actions: {
|
|
69
|
+
readonly createOrder: "Create order";
|
|
70
|
+
readonly assignDriver: "Assign driver";
|
|
71
|
+
readonly confirmOrder: "Confirm order";
|
|
72
|
+
readonly cancelOrder: "Cancel order";
|
|
73
|
+
readonly generateInvoice: "Generate invoice";
|
|
74
|
+
readonly markPaid: "Mark as paid";
|
|
75
|
+
readonly extendPeriod: "Extend period";
|
|
76
|
+
readonly endPeriod: "End storage";
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export default en;
|
|
81
|
+
type Stringify<T> = {
|
|
82
|
+
[K in keyof T]: T[K] extends Record<string, unknown> ? Stringify<T[K]> : string;
|
|
83
|
+
};
|
|
84
|
+
export type MblOrderMessages = Stringify<typeof en>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const en = {
|
|
2
|
+
mblOrder: {
|
|
3
|
+
status: {
|
|
4
|
+
draft: "Draft",
|
|
5
|
+
quoted: "Quote sent",
|
|
6
|
+
confirmed: "Confirmed",
|
|
7
|
+
assigned: "Driver assigned",
|
|
8
|
+
collected: "Collected",
|
|
9
|
+
in_transit: "In transit",
|
|
10
|
+
delivered: "Delivered",
|
|
11
|
+
invoiced: "Invoiced",
|
|
12
|
+
active: "Active",
|
|
13
|
+
extended: "Extended",
|
|
14
|
+
ended: "Ended"
|
|
15
|
+
},
|
|
16
|
+
orderType: {
|
|
17
|
+
transport: "Transport order",
|
|
18
|
+
storage: "Storage order"
|
|
19
|
+
},
|
|
20
|
+
billingMethod: {
|
|
21
|
+
per_km: "Per km",
|
|
22
|
+
per_hour: "Per hour",
|
|
23
|
+
per_weight: "Per weight",
|
|
24
|
+
fixed: "Fixed rate",
|
|
25
|
+
per_sqm: "Per m\xB2",
|
|
26
|
+
per_pallet: "Per pallet",
|
|
27
|
+
per_period: "Per period",
|
|
28
|
+
fixed_subscription: "Fixed subscription"
|
|
29
|
+
},
|
|
30
|
+
errors: {
|
|
31
|
+
invalidTransition: "This status change is not allowed",
|
|
32
|
+
orderNotFound: "Order not found",
|
|
33
|
+
quoteExpired: "This quote has expired",
|
|
34
|
+
driverUnavailable: "Driver is not available",
|
|
35
|
+
invoiceAlreadyExists: "An invoice already exists for this order",
|
|
36
|
+
periodNotFound: "No active storage period found"
|
|
37
|
+
},
|
|
38
|
+
invoice: {
|
|
39
|
+
title: "Invoice",
|
|
40
|
+
generated: "Invoice generated",
|
|
41
|
+
lineItem: "Description",
|
|
42
|
+
quantity: "Qty",
|
|
43
|
+
unitPrice: "Unit price",
|
|
44
|
+
vat: "VAT",
|
|
45
|
+
total: "Total",
|
|
46
|
+
subtotal: "Subtotal",
|
|
47
|
+
dueDate: "Due date",
|
|
48
|
+
issueDate: "Issue date",
|
|
49
|
+
status: {
|
|
50
|
+
draft: "Draft",
|
|
51
|
+
sent: "Sent",
|
|
52
|
+
paid: "Paid",
|
|
53
|
+
overdue: "Overdue",
|
|
54
|
+
void: "Void"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
quote: {
|
|
58
|
+
expires: "Expires",
|
|
59
|
+
accepted: "Quote accepted",
|
|
60
|
+
expired: "Quote expired",
|
|
61
|
+
noQuote: "No quote issued yet"
|
|
62
|
+
},
|
|
63
|
+
fleet: {
|
|
64
|
+
active: "On job",
|
|
65
|
+
idle: "Available",
|
|
66
|
+
offline: "Offline"
|
|
67
|
+
},
|
|
68
|
+
actions: {
|
|
69
|
+
createOrder: "Create order",
|
|
70
|
+
assignDriver: "Assign driver",
|
|
71
|
+
confirmOrder: "Confirm order",
|
|
72
|
+
cancelOrder: "Cancel order",
|
|
73
|
+
generateInvoice: "Generate invoice",
|
|
74
|
+
markPaid: "Mark as paid",
|
|
75
|
+
extendPeriod: "Extend period",
|
|
76
|
+
endPeriod: "End storage"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export default en;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const sv = {
|
|
2
|
+
mblOrder: {
|
|
3
|
+
status: {
|
|
4
|
+
draft: "Utkast",
|
|
5
|
+
quoted: "Offert skickad",
|
|
6
|
+
confirmed: "Bekr\xE4ftad",
|
|
7
|
+
assigned: "F\xF6rare tilldelad",
|
|
8
|
+
collected: "Upph\xE4mtad",
|
|
9
|
+
in_transit: "Under transport",
|
|
10
|
+
delivered: "Levererad",
|
|
11
|
+
invoiced: "Fakturerad",
|
|
12
|
+
active: "Aktiv",
|
|
13
|
+
extended: "F\xF6rl\xE4ngd",
|
|
14
|
+
ended: "Avslutad"
|
|
15
|
+
},
|
|
16
|
+
orderType: {
|
|
17
|
+
transport: "Transportorder",
|
|
18
|
+
storage: "Magasinsorder"
|
|
19
|
+
},
|
|
20
|
+
billingMethod: {
|
|
21
|
+
per_km: "Per km",
|
|
22
|
+
per_hour: "Per timme",
|
|
23
|
+
per_weight: "Per vikt",
|
|
24
|
+
fixed: "Fast pris",
|
|
25
|
+
per_sqm: "Per m\xB2",
|
|
26
|
+
per_pallet: "Per pall",
|
|
27
|
+
per_period: "Per period",
|
|
28
|
+
fixed_subscription: "Fast abonnemang"
|
|
29
|
+
},
|
|
30
|
+
errors: {
|
|
31
|
+
invalidTransition: "Denna status\xE4ndring \xE4r inte till\xE5ten",
|
|
32
|
+
orderNotFound: "Ordern hittades inte",
|
|
33
|
+
quoteExpired: "Offerten har g\xE5tt ut",
|
|
34
|
+
driverUnavailable: "F\xF6raren \xE4r inte tillg\xE4nglig",
|
|
35
|
+
invoiceAlreadyExists: "En faktura finns redan f\xF6r denna order",
|
|
36
|
+
periodNotFound: "Ingen aktiv lagringsperiod hittades"
|
|
37
|
+
},
|
|
38
|
+
invoice: {
|
|
39
|
+
title: "Faktura",
|
|
40
|
+
generated: "Faktura skapad",
|
|
41
|
+
lineItem: "Beskrivning",
|
|
42
|
+
quantity: "Antal",
|
|
43
|
+
unitPrice: "\xC0-pris",
|
|
44
|
+
vat: "Moms",
|
|
45
|
+
total: "Totalt",
|
|
46
|
+
subtotal: "Delsumma",
|
|
47
|
+
dueDate: "F\xF6rfallodatum",
|
|
48
|
+
issueDate: "Fakturadatum",
|
|
49
|
+
status: {
|
|
50
|
+
draft: "Utkast",
|
|
51
|
+
sent: "Skickad",
|
|
52
|
+
paid: "Betald",
|
|
53
|
+
overdue: "F\xF6rfallen",
|
|
54
|
+
void: "Makulerad"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
quote: {
|
|
58
|
+
expires: "G\xE5r ut",
|
|
59
|
+
accepted: "Offert accepterad",
|
|
60
|
+
expired: "Offerten har g\xE5tt ut",
|
|
61
|
+
noQuote: "Ingen offert har skickats \xE4nnu"
|
|
62
|
+
},
|
|
63
|
+
fleet: {
|
|
64
|
+
active: "P\xE5 uppdrag",
|
|
65
|
+
idle: "Tillg\xE4nglig",
|
|
66
|
+
offline: "Offline"
|
|
67
|
+
},
|
|
68
|
+
actions: {
|
|
69
|
+
createOrder: "Skapa order",
|
|
70
|
+
assignDriver: "Tilldela f\xF6rare",
|
|
71
|
+
confirmOrder: "Bekr\xE4fta order",
|
|
72
|
+
cancelOrder: "Avbryt order",
|
|
73
|
+
generateInvoice: "Skapa faktura",
|
|
74
|
+
markPaid: "Markera som betald",
|
|
75
|
+
extendPeriod: "F\xF6rl\xE4ng period",
|
|
76
|
+
endPeriod: "Avsluta lagring"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
export default sv;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
-- mbl-order: transport schema
|
|
2
|
+
-- Run via Supabase CLI: supabase db push
|
|
3
|
+
-- Or paste into the Supabase SQL editor.
|
|
4
|
+
--
|
|
5
|
+
-- Accounting integration is out of scope for the POC.
|
|
6
|
+
-- accounting_provider / external_id columns are reserved for future use.
|
|
7
|
+
|
|
8
|
+
CREATE SCHEMA IF NOT EXISTS transport;
|
|
9
|
+
|
|
10
|
+
-- ─── Drivers ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
CREATE TABLE transport.drivers (
|
|
13
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
14
|
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
phone TEXT,
|
|
17
|
+
license_number TEXT,
|
|
18
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
19
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE UNIQUE INDEX drivers_user_id_idx ON transport.drivers (user_id);
|
|
23
|
+
|
|
24
|
+
-- ─── Vehicles ─────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
CREATE TABLE transport.vehicles (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
registration TEXT NOT NULL UNIQUE,
|
|
29
|
+
make TEXT,
|
|
30
|
+
model TEXT,
|
|
31
|
+
year INT,
|
|
32
|
+
payload_kg NUMERIC,
|
|
33
|
+
volume_m3 NUMERIC,
|
|
34
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
35
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- ─── Orders ───────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
CREATE TABLE transport.orders (
|
|
41
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
42
|
+
order_number TEXT NOT NULL UNIQUE,
|
|
43
|
+
type TEXT NOT NULL CHECK (type IN ('transport', 'storage')),
|
|
44
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
45
|
+
customer_id UUID NOT NULL REFERENCES auth.users(id),
|
|
46
|
+
assigned_driver_id UUID REFERENCES transport.drivers(id) ON DELETE SET NULL,
|
|
47
|
+
assigned_vehicle_id UUID REFERENCES transport.vehicles(id) ON DELETE SET NULL,
|
|
48
|
+
billing_method TEXT NOT NULL,
|
|
49
|
+
currency TEXT NOT NULL DEFAULT 'GBP',
|
|
50
|
+
notes TEXT,
|
|
51
|
+
pickup_address JSONB,
|
|
52
|
+
delivery_address JSONB,
|
|
53
|
+
scheduled_pickup_at TIMESTAMPTZ,
|
|
54
|
+
scheduled_delivery_at TIMESTAMPTZ,
|
|
55
|
+
actual_pickup_at TIMESTAMPTZ,
|
|
56
|
+
actual_delivery_at TIMESTAMPTZ,
|
|
57
|
+
distance_km NUMERIC,
|
|
58
|
+
weight_kg NUMERIC,
|
|
59
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
60
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX orders_customer_id_idx ON transport.orders (customer_id);
|
|
64
|
+
CREATE INDEX orders_driver_id_idx ON transport.orders (assigned_driver_id);
|
|
65
|
+
CREATE INDEX orders_status_idx ON transport.orders (status);
|
|
66
|
+
CREATE INDEX orders_type_status_idx ON transport.orders (type, status);
|
|
67
|
+
|
|
68
|
+
-- ─── Order lines ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
CREATE TABLE transport.order_lines (
|
|
71
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
|
+
order_id UUID NOT NULL REFERENCES transport.orders(id) ON DELETE CASCADE,
|
|
73
|
+
description TEXT NOT NULL,
|
|
74
|
+
quantity NUMERIC NOT NULL DEFAULT 1 CHECK (quantity > 0),
|
|
75
|
+
unit TEXT NOT NULL,
|
|
76
|
+
unit_price NUMERIC NOT NULL,
|
|
77
|
+
vat_rate NUMERIC NOT NULL DEFAULT 0 CHECK (vat_rate >= 0 AND vat_rate <= 1),
|
|
78
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE INDEX order_lines_order_id_idx ON transport.order_lines (order_id);
|
|
82
|
+
|
|
83
|
+
-- ─── Quotes ───────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
CREATE TABLE transport.quotes (
|
|
86
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
87
|
+
order_id UUID NOT NULL REFERENCES transport.orders(id) ON DELETE CASCADE,
|
|
88
|
+
total_excl_vat NUMERIC NOT NULL,
|
|
89
|
+
total_vat NUMERIC NOT NULL,
|
|
90
|
+
total_incl_vat NUMERIC NOT NULL,
|
|
91
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
92
|
+
accepted_at TIMESTAMPTZ,
|
|
93
|
+
rejected_at TIMESTAMPTZ,
|
|
94
|
+
notes TEXT,
|
|
95
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
96
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX quotes_order_id_idx ON transport.quotes (order_id);
|
|
100
|
+
|
|
101
|
+
-- ─── Storage periods ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
CREATE TABLE transport.storage_periods (
|
|
104
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
105
|
+
order_id UUID NOT NULL REFERENCES transport.orders(id) ON DELETE CASCADE,
|
|
106
|
+
start_date DATE NOT NULL,
|
|
107
|
+
end_date DATE,
|
|
108
|
+
billing_interval TEXT NOT NULL DEFAULT 'monthly'
|
|
109
|
+
CHECK (billing_interval IN ('monthly', 'quarterly', 'annual', 'custom')),
|
|
110
|
+
billing_day INT NOT NULL DEFAULT 1 CHECK (billing_day BETWEEN 1 AND 28),
|
|
111
|
+
area_m2 NUMERIC,
|
|
112
|
+
pallets INT,
|
|
113
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
114
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX storage_periods_order_id_idx ON transport.storage_periods (order_id);
|
|
118
|
+
|
|
119
|
+
-- ─── Invoices ─────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
CREATE TABLE transport.invoices (
|
|
122
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
123
|
+
order_id UUID NOT NULL REFERENCES transport.orders(id),
|
|
124
|
+
invoice_number TEXT,
|
|
125
|
+
accounting_provider TEXT, -- reserved for future accounting integration
|
|
126
|
+
external_id TEXT,
|
|
127
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
128
|
+
CHECK (status IN ('draft', 'sent', 'paid', 'overdue', 'void')),
|
|
129
|
+
total_excl_vat NUMERIC NOT NULL,
|
|
130
|
+
total_vat NUMERIC NOT NULL,
|
|
131
|
+
total_incl_vat NUMERIC NOT NULL,
|
|
132
|
+
currency TEXT NOT NULL DEFAULT 'GBP',
|
|
133
|
+
issued_at TIMESTAMPTZ,
|
|
134
|
+
due_at TIMESTAMPTZ,
|
|
135
|
+
paid_at TIMESTAMPTZ,
|
|
136
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
137
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
CREATE INDEX invoices_order_id_idx ON transport.invoices (order_id);
|
|
141
|
+
CREATE INDEX invoices_status_idx ON transport.invoices (status);
|
|
142
|
+
CREATE UNIQUE INDEX invoices_external_idx ON transport.invoices (accounting_provider, external_id)
|
|
143
|
+
WHERE external_id IS NOT NULL;
|
|
144
|
+
|
|
145
|
+
-- ─── Row Level Security ───────────────────────────────────────────────────────
|
|
146
|
+
-- Assumes mbl-auth stores the user role in auth.users app_metadata as:
|
|
147
|
+
-- { "role": "customer" | "driver" | "dispatcher" | "admin" }
|
|
148
|
+
|
|
149
|
+
ALTER TABLE transport.orders ENABLE ROW LEVEL SECURITY;
|
|
150
|
+
ALTER TABLE transport.order_lines ENABLE ROW LEVEL SECURITY;
|
|
151
|
+
ALTER TABLE transport.quotes ENABLE ROW LEVEL SECURITY;
|
|
152
|
+
ALTER TABLE transport.storage_periods ENABLE ROW LEVEL SECURITY;
|
|
153
|
+
ALTER TABLE transport.invoices ENABLE ROW LEVEL SECURITY;
|
|
154
|
+
ALTER TABLE transport.drivers ENABLE ROW LEVEL SECURITY;
|
|
155
|
+
ALTER TABLE transport.vehicles ENABLE ROW LEVEL SECURITY;
|
|
156
|
+
|
|
157
|
+
-- Helper: extract role from JWT app_metadata
|
|
158
|
+
CREATE OR REPLACE FUNCTION transport.current_user_role()
|
|
159
|
+
RETURNS TEXT LANGUAGE sql STABLE SECURITY DEFINER AS $$
|
|
160
|
+
SELECT coalesce(
|
|
161
|
+
(auth.jwt() -> 'app_metadata' ->> 'role'),
|
|
162
|
+
'customer'
|
|
163
|
+
)
|
|
164
|
+
$$;
|
|
165
|
+
|
|
166
|
+
-- Orders: customers see own; drivers see assigned; dispatcher/admin see all
|
|
167
|
+
CREATE POLICY orders_select ON transport.orders FOR SELECT USING (
|
|
168
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
169
|
+
OR customer_id = auth.uid()
|
|
170
|
+
OR assigned_driver_id IN (SELECT id FROM transport.drivers WHERE user_id = auth.uid())
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE POLICY orders_insert ON transport.orders FOR INSERT WITH CHECK (
|
|
174
|
+
transport.current_user_role() IN ('customer', 'dispatcher', 'admin')
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
CREATE POLICY orders_update ON transport.orders FOR UPDATE USING (
|
|
178
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
179
|
+
OR (transport.current_user_role() = 'driver'
|
|
180
|
+
AND assigned_driver_id IN (SELECT id FROM transport.drivers WHERE user_id = auth.uid()))
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
-- Order lines: same visibility as parent order
|
|
184
|
+
CREATE POLICY order_lines_select ON transport.order_lines FOR SELECT USING (
|
|
185
|
+
order_id IN (SELECT id FROM transport.orders)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
CREATE POLICY order_lines_insert ON transport.order_lines FOR INSERT WITH CHECK (
|
|
189
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
-- Quotes: customers see own order quotes; dispatcher/admin see all
|
|
193
|
+
CREATE POLICY quotes_select ON transport.quotes FOR SELECT USING (
|
|
194
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
195
|
+
OR order_id IN (SELECT id FROM transport.orders WHERE customer_id = auth.uid())
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE POLICY quotes_insert ON transport.quotes FOR INSERT WITH CHECK (
|
|
199
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE POLICY quotes_update ON transport.quotes FOR UPDATE USING (
|
|
203
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
204
|
+
-- Customers may accept their own quotes
|
|
205
|
+
OR order_id IN (SELECT id FROM transport.orders WHERE customer_id = auth.uid())
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
-- Storage periods: same access pattern as orders
|
|
209
|
+
CREATE POLICY storage_periods_select ON transport.storage_periods FOR SELECT USING (
|
|
210
|
+
order_id IN (SELECT id FROM transport.orders)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
CREATE POLICY storage_periods_write ON transport.storage_periods FOR ALL USING (
|
|
214
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
-- Invoices: customers see own; admin/dispatcher see all
|
|
218
|
+
CREATE POLICY invoices_select ON transport.invoices FOR SELECT USING (
|
|
219
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
220
|
+
OR order_id IN (SELECT id FROM transport.orders WHERE customer_id = auth.uid())
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE POLICY invoices_write ON transport.invoices FOR ALL USING (
|
|
224
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
-- Drivers: drivers see own record; dispatcher/admin see all
|
|
228
|
+
CREATE POLICY drivers_select ON transport.drivers FOR SELECT USING (
|
|
229
|
+
transport.current_user_role() IN ('dispatcher', 'admin')
|
|
230
|
+
OR user_id = auth.uid()
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
CREATE POLICY drivers_write ON transport.drivers FOR ALL USING (
|
|
234
|
+
transport.current_user_role() = 'admin'
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
-- Vehicles: all authenticated users can read; only admin can write
|
|
238
|
+
CREATE POLICY vehicles_select ON transport.vehicles FOR SELECT TO authenticated USING (true);
|
|
239
|
+
CREATE POLICY vehicles_write ON transport.vehicles FOR ALL USING (
|
|
240
|
+
transport.current_user_role() = 'admin'
|
|
241
|
+
);
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
+
|
|
3
|
+
import type { default as Module } from './module.mjs'
|
|
4
|
+
|
|
5
|
+
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
+
|
|
7
|
+
export { default } from './module.mjs'
|
|
8
|
+
|
|
9
|
+
export { type MblOrderOptions } from './module.mjs'
|