` in `
` + - Update global CSS to new layout (see above) +4. **Verify** with `agent-browser screenshot` that the layout renders correctly + +## Quality checks (run before reporting done) + +- [ ] Sidebar renders at correct width, right-border visible +- [ ] All 7 nav items present with correct icons +- [ ] Active item highlighted on current route +- [ ] Collapse toggle works, persists on refresh (localStorage) +- [ ] Collapsed mode: icons only, no label overflow +- [ ] FilterBar still visible above content +- [ ] No horizontal scrollbar on content area +- [ ] Existing cards, tables, badges unchanged in content views + +## Collapsible iteration (after initial implementation) + +If the task includes "collapsible with icons-only mode": +- The toggle button should be at the bottom of the nav items area, not in a header +- Use a `` with `width`/`opacity` for label animation +- CSS transition: `transition: width 0.2s ease` on the sidebar itself +- `title` attribute on collapsed nav items for native tooltip (no JS needed) diff --git a/client/src/App.vue b/client/src/App.vue index c2da05a5c..2ae9fcaff 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,42 +1,20 @@ @@ -58,25 +48,33 @@ import { ref, onMounted, computed } from 'vue' import { api } from './api' import { useAuth } from './composables/useAuth' -import { useI18n } from './composables/useI18n' +import { useBrainrot } from './composables/useBrainrot' +import { useWorksCouncil } from './composables/useWorksCouncil' import FilterBar from './components/FilterBar.vue' -import ProfileMenu from './components/ProfileMenu.vue' import ProfileDetailsModal from './components/ProfileDetailsModal.vue' import TasksModal from './components/TasksModal.vue' -import LanguageSwitcher from './components/LanguageSwitcher.vue' +import AppSidebar from './components/AppSidebar.vue' +import BrainrotPanel from './components/BrainrotPanel.vue' +import GdprCookieBanner from './components/GdprCookieBanner.vue' +import WorksCouncilModal from './components/WorksCouncilModal.vue' +import RightToDisconnectBanner from './components/RightToDisconnectBanner.vue' export default { name: 'App', components: { FilterBar, - ProfileMenu, ProfileDetailsModal, TasksModal, - LanguageSwitcher + AppSidebar, + BrainrotPanel, + GdprCookieBanner, + WorksCouncilModal, + RightToDisconnectBanner }, setup() { const { currentUser } = useAuth() - const { t } = useI18n() + const { brainrotEnabled, toggle } = useBrainrot() + const { isOpen: wcIsOpen, actionDescription: wcActionDescription, handleOverride: wcHandleOverride, handleClose: wcHandleClose } = useWorksCouncil() const showProfileDetails = ref(false) const showTasks = ref(false) const apiTasks = ref([]) @@ -149,13 +147,18 @@ export default { onMounted(loadTasks) return { - t, showProfileDetails, showTasks, tasks, addTask, deleteTask, - toggleTask + toggleTask, + brainrotEnabled, + toggle, + wcIsOpen, + wcActionDescription, + wcHandleOverride, + wcHandleClose } } } @@ -178,92 +181,47 @@ body { .app { display: flex; - flex-direction: column; + flex-direction: row; min-height: 100vh; } -.top-nav { - background: #ffffff; - border-bottom: 1px solid #e2e8f0; - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05); - position: sticky; - top: 0; - z-index: 100; -} - -.nav-container { - max-width: 1600px; - margin: 0 auto; - display: flex; - align-items: center; - padding: 0 2rem; - height: 70px; -} - -.nav-container > .nav-tabs { - margin-left: auto; - margin-right: 1rem; -} - -.nav-container > .language-switcher { - margin-right: 1rem; -} - -.logo { +.content-area { + flex: 1; display: flex; - align-items: baseline; - gap: 0.75rem; -} - -.logo h1 { - font-size: 1.375rem; - font-weight: 700; - color: #0f172a; - letter-spacing: -0.025em; -} - -.subtitle { - font-size: 0.813rem; - color: #64748b; - font-weight: 400; - padding-left: 0.75rem; - border-left: 1px solid #e2e8f0; + flex-direction: column; + min-width: 0; + background: #f8fafc; } -.nav-tabs { +/* ─── Brainrot split-screen layout ──────────────────────── */ +.content-top { + flex: 1; display: flex; - gap: 0.25rem; + flex-direction: column; + min-height: 0; } -.nav-tabs a { - padding: 0.625rem 1.25rem; - color: #64748b; - text-decoration: none; - font-weight: 500; - font-size: 0.938rem; - border-radius: 6px; - transition: all 0.2s ease; - position: relative; +.content-area.brainrot-active { + height: 100vh; + overflow: hidden; } -.nav-tabs a:hover { - color: #0f172a; - background: #f1f5f9; +.content-area.brainrot-active .content-top { + height: 60vh; + flex: 0 0 60vh; + overflow-y: auto; } -.nav-tabs a.active { - color: #2563eb; - background: #eff6ff; +.content-area.brainrot-active .main-content { + min-height: 0; } -.nav-tabs a.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - right: 0; - height: 2px; - background: #2563eb; +.brainrot-pane { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; } .main-content { @@ -287,7 +245,7 @@ body { } .page-header p { - color: #64748b; + color: #4b5563; font-size: 0.938rem; } @@ -312,7 +270,7 @@ body { } .stat-label { - color: #64748b; + color: #4b5563; font-size: 0.875rem; font-weight: 600; text-transform: uppercase; @@ -470,7 +428,7 @@ tbody tr:hover { .loading { text-align: center; padding: 3rem; - color: #64748b; + color: #4b5563; font-size: 0.938rem; } diff --git a/client/src/api.js b/client/src/api.js index 11cb9db70..9f39adea2 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -3,12 +3,12 @@ import axios from 'axios' const API_BASE_URL = 'http://localhost:8001/api' export const api = { - async getInventory(filters = {}) { + async getInventory(filters = {}, { signal } = {}) { const params = new URLSearchParams() if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) if (filters.category && filters.category !== 'all') params.append('category', filters.category) - const response = await axios.get(`${API_BASE_URL}/inventory?${params.toString()}`) + const response = await axios.get(`${API_BASE_URL}/inventory?${params.toString()}`, { signal }) return response.data }, @@ -17,14 +17,14 @@ export const api = { return response.data }, - async getOrders(filters = {}) { + async getOrders(filters = {}, { signal } = {}) { const params = new URLSearchParams() if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) if (filters.category && filters.category !== 'all') params.append('category', filters.category) if (filters.status && filters.status !== 'all') params.append('status', filters.status) if (filters.month && filters.month !== 'all') params.append('month', filters.month) - const response = await axios.get(`${API_BASE_URL}/orders?${params.toString()}`) + const response = await axios.get(`${API_BASE_URL}/orders?${params.toString()}`, { signal }) return response.data }, @@ -33,44 +33,44 @@ export const api = { return response.data }, - async getDemandForecasts() { - const response = await axios.get(`${API_BASE_URL}/demand`) + async getDemandForecasts({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/demand`, { signal }) return response.data }, - async getBacklog() { - const response = await axios.get(`${API_BASE_URL}/backlog`) + async getBacklog({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/backlog`, { signal }) return response.data }, - async getDashboardSummary(filters = {}) { + async getDashboardSummary(filters = {}, { signal } = {}) { const params = new URLSearchParams() if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) if (filters.category && filters.category !== 'all') params.append('category', filters.category) if (filters.status && filters.status !== 'all') params.append('status', filters.status) if (filters.month && filters.month !== 'all') params.append('month', filters.month) - const response = await axios.get(`${API_BASE_URL}/dashboard/summary?${params.toString()}`) + const response = await axios.get(`${API_BASE_URL}/dashboard/summary?${params.toString()}`, { signal }) return response.data }, - async getSpendingSummary() { - const response = await axios.get(`${API_BASE_URL}/spending/summary`) + async getSpendingSummary({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/spending/summary`, { signal }) return response.data }, - async getMonthlySpending() { - const response = await axios.get(`${API_BASE_URL}/spending/monthly`) + async getMonthlySpending({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/spending/monthly`, { signal }) return response.data }, - async getCategorySpending() { - const response = await axios.get(`${API_BASE_URL}/spending/categories`) + async getCategorySpending({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/spending/categories`, { signal }) return response.data }, - async getTransactions() { - const response = await axios.get(`${API_BASE_URL}/spending/transactions`) + async getTransactions({ signal } = {}) { + const response = await axios.get(`${API_BASE_URL}/spending/transactions`, { signal }) return response.data }, @@ -102,5 +102,21 @@ export const api = { async getPurchaseOrderByBacklogItem(backlogItemId) { const response = await axios.get(`${API_BASE_URL}/purchase-orders/${backlogItemId}`) return response.data + }, + + async getQuarterlyReports(filters = {}) { + const params = new URLSearchParams() + if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) + if (filters.category && filters.category !== 'all') params.append('category', filters.category) + const response = await axios.get(`${API_BASE_URL}/reports/quarterly?${params.toString()}`) + return response.data + }, + + async getMonthlyTrends(filters = {}) { + const params = new URLSearchParams() + if (filters.warehouse && filters.warehouse !== 'all') params.append('warehouse', filters.warehouse) + if (filters.category && filters.category !== 'all') params.append('category', filters.category) + const response = await axios.get(`${API_BASE_URL}/reports/monthly-trends?${params.toString()}`) + return response.data } } diff --git a/client/src/components/AlertReader.vue b/client/src/components/AlertReader.vue new file mode 100644 index 000000000..95cfd5fe8 --- /dev/null +++ b/client/src/components/AlertReader.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/client/src/components/AppSidebar.vue b/client/src/components/AppSidebar.vue new file mode 100644 index 000000000..ba15df347 --- /dev/null +++ b/client/src/components/AppSidebar.vue @@ -0,0 +1,689 @@ + + + + + diff --git a/client/src/components/BacklogDetailModal.vue b/client/src/components/BacklogDetailModal.vue index 280958acf..d66b28347 100644 --- a/client/src/components/BacklogDetailModal.vue +++ b/client/src/components/BacklogDetailModal.vue @@ -168,7 +168,7 @@ const formatDate = (dateString) => { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -224,7 +224,7 @@ const formatDate = (dateString) => { .item-sku { font-size: 0.875rem; - color: #64748b; + color: #4b5563; font-family: 'Monaco', 'Courier New', monospace; } @@ -281,7 +281,7 @@ const formatDate = (dateString) => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; margin-bottom: 0.5rem; } @@ -316,7 +316,7 @@ const formatDate = (dateString) => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; } .info-value { diff --git a/client/src/components/BrainrotPanel.vue b/client/src/components/BrainrotPanel.vue new file mode 100644 index 000000000..0405d45dd --- /dev/null +++ b/client/src/components/BrainrotPanel.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/client/src/components/BrainrotTicker.vue b/client/src/components/BrainrotTicker.vue new file mode 100644 index 000000000..ac5f7ad45 --- /dev/null +++ b/client/src/components/BrainrotTicker.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/client/src/components/CostDetailModal.vue b/client/src/components/CostDetailModal.vue index aa31bd14b..2259b0574 100644 --- a/client/src/components/CostDetailModal.vue +++ b/client/src/components/CostDetailModal.vue @@ -194,7 +194,7 @@ const close = () => { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -332,7 +332,7 @@ const close = () => { .cost-percentage { font-size: 0.875rem; - color: #64748b; + color: #4b5563; font-weight: 500; } diff --git a/client/src/components/EuAiActBanner.vue b/client/src/components/EuAiActBanner.vue new file mode 100644 index 000000000..f06bdabe3 --- /dev/null +++ b/client/src/components/EuAiActBanner.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/client/src/components/FilterBar.vue b/client/src/components/FilterBar.vue index 9e12b6945..cab19324a 100644 --- a/client/src/components/FilterBar.vue +++ b/client/src/components/FilterBar.vue @@ -3,8 +3,8 @@
- - @@ -22,8 +22,8 @@
- - @@ -32,8 +32,8 @@
- - @@ -44,8 +44,8 @@
- - @@ -135,7 +135,7 @@ export default { .filter-group label { font-size: 0.75rem; font-weight: 600; - color: #64748b; + color: #4b5563; white-space: nowrap; } @@ -170,7 +170,7 @@ export default { background: white; border: 1px solid #e2e8f0; border-radius: 6px; - color: #64748b; + color: #4b5563; cursor: pointer; transition: all 0.2s; flex-shrink: 0; diff --git a/client/src/components/GdprCookieBanner.vue b/client/src/components/GdprCookieBanner.vue new file mode 100644 index 000000000..3a36386f3 --- /dev/null +++ b/client/src/components/GdprCookieBanner.vue @@ -0,0 +1,447 @@ + + + + + diff --git a/client/src/components/InventoryDetailModal.vue b/client/src/components/InventoryDetailModal.vue index 9d648af33..fb15b42b8 100644 --- a/client/src/components/InventoryDetailModal.vue +++ b/client/src/components/InventoryDetailModal.vue @@ -219,7 +219,7 @@ const getSummaryCardClass = () => { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -286,7 +286,7 @@ const getSummaryCardClass = () => { .item-sku { font-size: 0.875rem; - color: #64748b; + color: #4b5563; font-family: 'Monaco', 'Courier New', monospace; } @@ -353,7 +353,7 @@ const getSummaryCardClass = () => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; margin-bottom: 0.5rem; } @@ -365,7 +365,7 @@ const getSummaryCardClass = () => { .summary-subtitle { font-size: 0.75rem; - color: #64748b; + color: #4b5563; margin-top: 0.25rem; } @@ -386,7 +386,7 @@ const getSummaryCardClass = () => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; } .info-value { diff --git a/client/src/components/LanguageSwitcher.vue b/client/src/components/LanguageSwitcher.vue index dde97df19..c7ddd216f 100644 --- a/client/src/components/LanguageSwitcher.vue +++ b/client/src/components/LanguageSwitcher.vue @@ -64,7 +64,9 @@ const isDropdownOpen = ref(false) const languageNames = { en: 'English', - ja: '日本語' + ja: '日本語', + sw: '🚀 Galactic Basic', + mc: '⛏️ ᒲ╎リᒷᓵ∷ᔑ⎓ℸ' } const getLanguageName = (locale) => { @@ -114,7 +116,7 @@ const selectLanguage = (locale) => { } .globe-icon { - color: #64748b; + color: #4b5563; flex-shrink: 0; } @@ -123,7 +125,7 @@ const selectLanguage = (locale) => { } .chevron { - color: #64748b; + color: #4b5563; transition: transform 0.2s ease; flex-shrink: 0; } diff --git a/client/src/components/ProductDetailModal.vue b/client/src/components/ProductDetailModal.vue index 76218306d..efadca437 100644 --- a/client/src/components/ProductDetailModal.vue +++ b/client/src/components/ProductDetailModal.vue @@ -175,7 +175,7 @@ const getStockBadgeClass = (stockLevel) => { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -231,7 +231,7 @@ const getStockBadgeClass = (stockLevel) => { .product-sku { font-size: 0.875rem; - color: #64748b; + color: #4b5563; font-family: 'Monaco', 'Courier New', monospace; } @@ -277,7 +277,7 @@ const getStockBadgeClass = (stockLevel) => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; } .info-value { diff --git a/client/src/components/ProfileDetailsModal.vue b/client/src/components/ProfileDetailsModal.vue index 4896a602f..5d2c1a11e 100644 --- a/client/src/components/ProfileDetailsModal.vue +++ b/client/src/components/ProfileDetailsModal.vue @@ -141,7 +141,7 @@ const formatDate = (dateString) => { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -201,7 +201,7 @@ const formatDate = (dateString) => { .profile-job-title { font-size: 1rem; - color: #64748b; + color: #4b5563; margin: 0; } @@ -222,7 +222,7 @@ const formatDate = (dateString) => { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; - color: #64748b; + color: #4b5563; } .info-value { diff --git a/client/src/components/ProfileMenu.vue b/client/src/components/ProfileMenu.vue index 5706d24cf..4e16a6f5c 100644 --- a/client/src/components/ProfileMenu.vue +++ b/client/src/components/ProfileMenu.vue @@ -159,7 +159,7 @@ const handleLogout = () => { } .chevron { - color: #64748b; + color: #4b5563; transition: transform 0.2s ease; } @@ -217,7 +217,7 @@ const handleLogout = () => { .user-email { font-size: 0.813rem; - color: #64748b; + color: #4b5563; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -251,7 +251,7 @@ const handleLogout = () => { } .dropdown-item svg { - color: #64748b; + color: #4b5563; flex-shrink: 0; } diff --git a/client/src/components/RightToDisconnectBanner.vue b/client/src/components/RightToDisconnectBanner.vue new file mode 100644 index 000000000..43ff7e6ff --- /dev/null +++ b/client/src/components/RightToDisconnectBanner.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/client/src/components/SupplyChainBadge.vue b/client/src/components/SupplyChainBadge.vue new file mode 100644 index 000000000..675c1e7fe --- /dev/null +++ b/client/src/components/SupplyChainBadge.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/client/src/components/TasksModal.vue b/client/src/components/TasksModal.vue index 67d44638f..4be713dec 100644 --- a/client/src/components/TasksModal.vue +++ b/client/src/components/TasksModal.vue @@ -291,7 +291,7 @@ export default { .close-button { background: none; border: none; - color: #64748b; + color: #4b5563; cursor: pointer; padding: 0.5rem; display: flex; @@ -427,7 +427,7 @@ label { .no-tasks { text-align: center; padding: 3rem; - color: #64748b; + color: #4b5563; font-size: 1.1rem; font-style: italic; } @@ -563,7 +563,7 @@ label { align-items: center; gap: 0.5rem; font-size: 0.813rem; - color: #64748b; + color: #4b5563; } .task-due-date svg { diff --git a/client/src/components/TikTokFeed.vue b/client/src/components/TikTokFeed.vue new file mode 100644 index 000000000..39b5f1d59 --- /dev/null +++ b/client/src/components/TikTokFeed.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/client/src/components/WorksCouncilModal.vue b/client/src/components/WorksCouncilModal.vue new file mode 100644 index 000000000..07ca1daad --- /dev/null +++ b/client/src/components/WorksCouncilModal.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/client/src/composables/useBrainrot.js b/client/src/composables/useBrainrot.js new file mode 100644 index 000000000..d780fdf71 --- /dev/null +++ b/client/src/composables/useBrainrot.js @@ -0,0 +1,11 @@ +import { ref } from 'vue' + +const brainrotEnabled = ref(false) + +export function useBrainrot() { + const toggle = () => { + brainrotEnabled.value = !brainrotEnabled.value + } + + return { brainrotEnabled, toggle } +} diff --git a/client/src/composables/useI18n.js b/client/src/composables/useI18n.js index bba55f0d3..2372755d6 100644 --- a/client/src/composables/useI18n.js +++ b/client/src/composables/useI18n.js @@ -1,10 +1,14 @@ import { ref, computed } from 'vue' import en from '../locales/en' import ja from '../locales/ja' +import sw from '../locales/sw' +import mc from '../locales/mc' const translations = { en, - ja + ja, + sw, + mc } // Load saved locale from localStorage, default to 'en' @@ -69,7 +73,9 @@ export function useI18n() { const localeName = computed(() => { const names = { en: 'English', - ja: '日本語' + ja: '日本語', + sw: 'Galactic Basic', + mc: 'ᒲ╎リᒷᓵ∷ᔑ⎓ℸ' } return names[currentLocale.value] || currentLocale.value }) diff --git a/client/src/composables/useRestockingOrders.js b/client/src/composables/useRestockingOrders.js new file mode 100644 index 000000000..57ff4b43c --- /dev/null +++ b/client/src/composables/useRestockingOrders.js @@ -0,0 +1,48 @@ +import { ref } from 'vue' + +// Module singleton — persists across navigation +const submittedOrders = ref([]) +let orderCounter = 1 + +const LEAD_TIME_DAYS = { increasing: 3, stable: 7, decreasing: 14 } + +function addDays(date, days) { + const result = new Date(date) + result.setDate(result.getDate() + days) + return result.toISOString().split('T')[0] +} + +export function useRestockingOrders() { + const submitOrder = (recommendedItems, budgetUsed) => { + const today = new Date().toISOString().split('T')[0] + const maxLeadTime = recommendedItems.reduce((max, item) => { + return Math.max(max, LEAD_TIME_DAYS[item.trend] ?? 7) + }, 0) + + const order = { + id: `rst-${Date.now()}`, + order_number: `RST-2025-${String(orderCounter++).padStart(4, '0')}`, + submitted_at: today, + expected_delivery: addDays(today, maxLeadTime), + lead_time_days: maxLeadTime, + items: recommendedItems.map(item => ({ + sku: item.item_sku, + name: item.item_name, + quantity: item.restock_qty, + unit_cost: item.unit_cost, + trend: item.trend, + lead_time_days: LEAD_TIME_DAYS[item.trend] ?? 7 + })), + total_cost: recommendedItems.reduce( + (sum, item) => sum + item.restock_qty * item.unit_cost, 0 + ), + budget_used: budgetUsed, + status: 'Submitted' + } + + submittedOrders.value.unshift(order) + return order + } + + return { submittedOrders, submitOrder } +} diff --git a/client/src/composables/useWorksCouncil.js b/client/src/composables/useWorksCouncil.js new file mode 100644 index 000000000..90b5d803c --- /dev/null +++ b/client/src/composables/useWorksCouncil.js @@ -0,0 +1,26 @@ +import { ref } from 'vue' + +const isOpen = ref(false) +const actionDescription = ref('') +const pendingAction = ref(null) + +export function useWorksCouncil() { + const requireApproval = (description, onOverride) => { + actionDescription.value = description + pendingAction.value = onOverride + isOpen.value = true + } + + const handleOverride = () => { + if (pendingAction.value) pendingAction.value() + isOpen.value = false + pendingAction.value = null + } + + const handleClose = () => { + isOpen.value = false + pendingAction.value = null + } + + return { isOpen, actionDescription, requireApproval, handleOverride, handleClose } +} diff --git a/client/src/locales/en.js b/client/src/locales/en.js index 03a58fe6e..f2ece9df7 100644 --- a/client/src/locales/en.js +++ b/client/src/locales/en.js @@ -6,6 +6,8 @@ export default { orders: 'Orders', finance: 'Finance', demandForecast: 'Demand Forecast', + reports: 'Reports', + restocking: 'Restocking', companyName: 'Catalyst Components', subtitle: 'Inventory Management System' }, @@ -324,5 +326,54 @@ export default { filter: 'Filter', export: 'Export', items: 'items' + }, + + // Reports + reports: { + title: 'Performance Reports', + description: 'Quarterly and monthly performance analytics', + quarterlyTitle: 'Quarterly Performance', + monthlyTitle: 'Monthly Revenue Trend', + momTitle: 'Month-over-Month Analysis', + quarter: 'Quarter', + totalOrders: 'Total Orders', + totalRevenue: 'Total Revenue', + delivered: 'Delivered', + avgOrderValue: 'Avg Order Value', + fulfillmentRate: 'Fulfillment Rate', + month: 'Month', + orderCount: 'Order Count', + revenue: 'Revenue', + deliveredCount: 'Delivered Count', + change: 'Change', + growthRate: 'Growth Rate', + ytdRevenue: 'Total Revenue (YTD)', + ytdOrders: 'Total Orders (YTD)', + avgFulfillment: 'Avg Fulfillment Rate', + avgMonthlyRevenue: 'Avg Monthly Revenue', + ytdDelivered: 'Delivered Orders (YTD)', + bestQuarter: 'Best Performing Quarter' + }, + + // Backlog + backlog: { + title: 'Backlog Management', + description: 'Track and resolve inventory shortages', + highPriority: 'High Priority', + mediumPriority: 'Medium Priority', + lowPriority: 'Low Priority', + totalItems: 'Total Backlog Items', + items: 'Backlog Items', + noItems: 'No backlog items - all orders can be fulfilled!', + orderId: 'Order ID', + sku: 'SKU', + itemName: 'Item Name', + quantityNeeded: 'Quantity Needed', + quantityAvailable: 'Quantity Available', + shortage: 'Shortage', + daysDelayed: 'Days Delayed', + priority: 'Priority', + unitsShort: 'units short', + days: 'days' } } diff --git a/client/src/locales/ja.js b/client/src/locales/ja.js index db33223ac..d216b86b8 100644 --- a/client/src/locales/ja.js +++ b/client/src/locales/ja.js @@ -6,6 +6,8 @@ export default { orders: '注文', finance: '財務', demandForecast: '需要予測', + reports: 'レポート', + restocking: '補充', companyName: '触媒コンポーネンツ', subtitle: '在庫管理システム' }, @@ -326,6 +328,55 @@ export default { items: '件' }, + // Reports + reports: { + title: 'パフォーマンスレポート', + description: '四半期および月次パフォーマンス分析', + quarterlyTitle: '四半期パフォーマンス', + monthlyTitle: '月次収益トレンド', + momTitle: '前月比分析', + quarter: '四半期', + totalOrders: '総注文数', + totalRevenue: '総収益', + delivered: '配達済み', + avgOrderValue: '平均注文額', + fulfillmentRate: '履行率', + month: '月', + orderCount: '注文数', + revenue: '収益', + deliveredCount: '配達済み数', + change: '変化', + growthRate: '成長率', + ytdRevenue: '年間総収益', + ytdOrders: '年間総注文数', + avgFulfillment: '平均履行率', + avgMonthlyRevenue: '月次平均収益', + ytdDelivered: '年間配達済み注文数', + bestQuarter: '最高パフォーマンス四半期' + }, + + // Backlog + backlog: { + title: 'バックログ管理', + description: '在庫不足の追跡と解消', + highPriority: '高優先度', + mediumPriority: '中優先度', + lowPriority: '低優先度', + totalItems: '総バックログ数', + items: 'バックログ品目', + noItems: 'バックログなし - すべての注文を履行できます', + orderId: '注文ID', + sku: 'SKU', + itemName: '品目名', + quantityNeeded: '必要数量', + quantityAvailable: '在庫数量', + shortage: '不足', + daysDelayed: '遅延日数', + priority: '優先度', + unitsShort: '単位不足', + days: '日' + }, + // Product Names productNames: { 'Single Layer PCB Assembly': '単層PCB組立', diff --git a/client/src/locales/mc.js b/client/src/locales/mc.js new file mode 100644 index 000000000..23bd4a2c9 --- /dev/null +++ b/client/src/locales/mc.js @@ -0,0 +1,340 @@ +export default { + nav: { + overview: '𝙹⍊ᒷ∷⍊╎ᒷ∴', + inventory: '╎リ⍊ᒷリℸ𝙹∷ꘐ', + orders: '𝙹∷↸ᒷ∷ᓭ', + finance: '⎓╎リᔑリᓵᒷ', + demandForecast: '↸ᒷᒲᔑリ↸ ⎓𝙹∷ᒷᓵᔑᓭℸ', + reports: '∷ᒷ!¡𝙹∷ℸᓭ', + restocking: '∷ᒷᓭℸ𝙹ᓵꖌ╎リ⊣', + companyName: 'ᓵᔑℸᔑꖎꘐᓭℸ ᓵ𝙹ᒲ!¡𝙹リᒷリℸᓭ', + subtitle: '╎リ⍊ᒷリℸ𝙹∷ꘐ ᒲᔑリᔑ⊣ᒷᒲᒷリℸ ᓭꘐᓭℸᒷᒲ', + }, + dashboard: { + title: '𝙹⍊ᒷ∷⍊╎ᒷ∴', + kpi: { + title: 'ꖌᒷꘐ !¡ᒷ∷⎓𝙹∷ᒲᔑリᓵᒷ ╎リ↸╎ᓵᔑℸ𝙹∷ᓭ', + inventoryTurnover: '╎リ⍊ᒷリℸ𝙹∷ꘐ ℸ⚍∷リ𝙹⍊ᒷ∷ ∷ᔑℸᒷ', + ordersFulfilled: '𝙹∷↸ᒷ∷ᓭ ⎓⚍ꖎ⎓╎ꖎꖎᒷ↸', + orderFillRate: '𝙹∷↸ᒷ∷ ⎓╎ꖎꖎ ∷ᔑℸᒷ', + revenue: '∷ᒷ⍊ᒷリ⚍ᒷ (𝙹∷↸ᒷ∷ᓭ)', + revenueYTD: '∷ᒷ⍊ᒷリ⚍ᒷ (𝙹∷↸ᒷ∷ᓭ) ꘐℸ↸', + revenueMTD: '∷ᒷ⍊ᒷリ⚍ᒷ (𝙹∷↸ᒷ∷ᓭ) ᒲℸ↸', + avgProcessingTime: 'ᔑ⍊⊣ !¡∷𝙹ᓵᒷᓭᓭ╎リ⊣ ℸ╎ᒲᒷ (↸ᔑꘐᓭ)', + goal: '⊣𝙹ᔑꖎ', + }, + summary: { + title: 'ᓭ⚍ᒲᒲᔑ∷ꘐ', + }, + orderHealth: { + title: '𝙹∷↸ᒷ∷ ⍑ᒷᔑꖎℸ⍑', + totalOrders: 'ℸ𝙹ℸᔑꖎ 𝙹∷↸ᒷ∷ᓭ', + revenue: '∷ᒷ⍊ᒷリ⚍ᒷ', + avgOrderValue: 'ᔑ⍊⊣ 𝙹∷↸ᒷ∷ ⍊ᔑꖎ⚍ᒷ', + onTimeRate: '𝙹リ-ℸ╎ᒲᒷ ∷ᔑℸᒷ', + avgFulfillmentDays: 'ᔑ⍊⊣ ⎓⚍ꖎ⎓╎ꖎꖎᒲᒷリℸ (↸ᔑꘐᓭ)', + total: 'ℸ𝙹ℸᔑꖎ', + }, + ordersByMonth: { + title: '𝙹∷↸ᒷ∷ᓭ ʖꘐ ᒲ𝙹リℸ⍑', + }, + inventoryValue: { + title: '╎リ⍊ᒷリℸ𝙹∷ꘐ ⍊ᔑꖎ⚍ᒷ ʖꘐ ᓵᔑℸᒷ⊣𝙹∷ꘐ', + }, + inventoryShortages: { + title: '╎リ⍊ᒷリℸ𝙹∷ꘐ ᓭ⍑𝙹∷ℸᔑ⊣ᒷᓭ', + noShortages: 'リ𝙹 ╎リ⍊ᒷリℸ𝙹∷ꘐ ᓭ⍑𝙹∷ℸᔑ⊣ᒷᓭ - ᔑꖎꖎ 𝙹∷↸ᒷ∷ᓭ ᓵᔑリ ʖᒷ ⎓⚍ꖎ⎓╎ꖎꖎᒷ↸!', + noData: 'リ𝙹 ╎リ⍊ᒷリℸ𝙹∷ꘐ ↸ᔑℸᔑ ⎓𝙹∷ ᓭᒷꖎᒷᓵℸᒷ↸ ⎓╎ꖎℸᒷ∷ᓭ', + orderId: '𝙹∷↸ᒷ∷ ╎↸', + sku: 'ᓭꖌ⚍', + itemName: '╎ℸᒷᒲ リᔑᒲᒷ', + quantityNeeded: 'ᑑ⚍ᔑリℸ╎ℸꘐ リᒷᒷ↸ᒷ↸', + quantityAvailable: 'ᑑ⚍ᔑリℸ╎ℸꘐ ᔑ⍊ᔑ╎ꖎᔑʖꖎᒷ', + shortage: 'ᓭ⍑𝙹∷ℸᔑ⊣ᒷ', + daysDelayed: '↸ᔑꘐᓭ ↸ᒷꖎᔑꘐᒷ↸', + priority: '!¡∷╎𝙹∷╎ℸꘐ', + unitsShort: '⚍リ╎ℸᓭ ᓭ⍑𝙹∷ℸ', + days: '↸ᔑꘐᓭ', + }, + topProducts: { + title: 'ℸ𝙹!¡ !¡∷𝙹↸⚍ᓵℸᓭ ʖꘐ ∷ᒷ⍊ᒷリ⚍ᒷ', + sku: 'ᓭꖌ⚍', + product: '!¡∷𝙹↸⚍ᓵℸ', + category: 'ᓵᔑℸᒷ⊣𝙹∷ꘐ', + warehouse: '∴ᔑ∷ᒷ⍑𝙹⚍ᓭᒷ', + stockStatus: 'ᓭℸ𝙹ᓵꖌ ᓭℸᔑℸ⚍ᓭ', + revenue: '∷ᒷ⍊ᒷリ⚍ᒷ', + unitsOrdered: '⚍リ╎ℸᓭ 𝙹∷↸ᒷ∷ᒷ↸', + firstOrder: '⎓╎∷ᓭℸ 𝙹∷↸ᒷ∷', + inStock: '╎リ ᓭℸ𝙹ᓵꖌ', + lowStock: 'ꖎ𝙹∴ ᓭℸ𝙹ᓵꖌ', + }, + }, + inventory: { + title: '╎リ⍊ᒷリℸ𝙹∷ꘐ', + description: 'ℸ∷ᔑᓵꖌ ᔑリ↸ ᒲᔑリᔑ⊣ᒷ ᔑꖎꖎ ╎リ⍊ᒷリℸ𝙹∷ꘐ ╎ℸᒷᒲᓭ', + stockLevels: 'ᓭℸ𝙹ᓵꖌ ꖎᒷ⍊ᒷꖎᓭ', + skus: 'ᓭꖌ⚍ᓭ', + searchPlaceholder: 'ᓭᒷᔑ∷ᓵ⍑ ʖꘐ ╎ℸᒷᒲ リᔑᒲᒷ...', + clearSearch: 'ᓵꖎᒷᔑ∷ ᓭᒷᔑ∷ᓵ⍑', + totalItems: 'ℸ𝙹ℸᔑꖎ ╎ℸᒷᒲᓭ', + totalValue: 'ℸ𝙹ℸᔑꖎ ⍊ᔑꖎ⚍ᒷ', + lowStockItems: 'ꖎ𝙹∴ ᓭℸ𝙹ᓵꖌ ╎ℸᒷᒲᓭ', + warehouses: '∴ᔑ∷ᒷ⍑𝙹⚍ᓭᒷᓭ', + table: { + sku: 'ᓭꖌ⚍', + itemName: '╎ℸᒷᒲ リᔑᒲᒷ', + name: 'リᔑᒲᒷ', + category: 'ᓵᔑℸᒷ⊣𝙹∷ꘐ', + warehouse: '∴ᔑ∷ᒷ⍑𝙹⚍ᓭᒷ', + quantity: 'ᑑ⚍ᔑリℸ╎ℸꘐ', + quantityOnHand: 'ᑑ⚍ᔑリℸ╎ℸꘐ 𝙹リ ⍑ᔑリ↸', + reorderPoint: '∷ᒷ𝙹∷↸ᒷ∷ !¡𝙹╎リℸ', + unitCost: '⚍リ╎ℸ ᓵ𝙹ᓭℸ', + unitPrice: '⚍リ╎ℸ !¡∷╎ᓵᒷ', + totalValue: 'ℸ𝙹ℸᔑꖎ ⍊ᔑꖎ⚍ᒷ', + location: 'ꖎ𝙹ᓵᔑℸ╎𝙹リ', + status: 'ᓭℸᔑℸ⚍ᓭ', + }, + }, + orders: { + title: '𝙹∷↸ᒷ∷ᓭ', + description: '⍊╎ᒷ∴ ᔑリ↸ ᒲᔑリᔑ⊣ᒷ ᓵ⚍ᓭℸ𝙹ᒲᒷ∷ 𝙹∷↸ᒷ∷ᓭ', + allOrders: 'ᔑꖎꖎ 𝙹∷↸ᒷ∷ᓭ', + totalOrders: 'ℸ𝙹ℸᔑꖎ 𝙹∷↸ᒷ∷ᓭ', + totalRevenue: 'ℸ𝙹ℸᔑꖎ ∷ᒷ⍊ᒷリ⚍ᒷ', + avgOrderValue: 'ᔑ⍊⊣ 𝙹∷↸ᒷ∷ ⍊ᔑꖎ⚍ᒷ', + onTimeDelivery: '𝙹リ-ℸ╎ᒲᒷ ↸ᒷꖎ╎⍊ᒷ∷ꘐ', + itemsCount: '{count} ╎ℸᒷᒲᓭ', + quantity: 'ᑑℸꘐ', + table: { + orderNumber: '𝙹∷↸ᒷ∷ リ⚍ᒲʖᒷ∷', + orderId: '𝙹∷↸ᒷ∷ ╎↸', + orderDate: '𝙹∷↸ᒷ∷ ↸ᔑℸᒷ', + date: '↸ᔑℸᒷ', + customer: 'ᓵ⚍ᓭℸ𝙹ᒲᒷ∷', + category: 'ᓵᔑℸᒷ⊣𝙹∷ꘐ', + warehouse: '∴ᔑ∷ᒷ⍑𝙹⚍ᓭᒷ', + items: '╎ℸᒷᒲᓭ', + value: '⍊ᔑꖎ⚍ᒷ', + totalValue: 'ℸ𝙹ℸᔑꖎ ⍊ᔑꖎ⚍ᒷ', + status: 'ᓭℸᔑℸ⚍ᓭ', + expectedDelivery: 'ᒷ!¿!¡ᒷᓵℸᒷ↸ ↸ᒷꖎ╎⍊ᒷ∷ꘐ', + actualDelivery: 'ᔑᓵℸ⚍ᔑꖎ ↸ᒷꖎ╎⍊ᒷ∷ꘐ', + }, + }, + finance: { + title: '⎓╎リᔑリᓵᒷ ↸ᔑᓭ⍑ʖ𝙹ᔑ∷↸', + description: 'ℸ∷ᔑᓵꖌ ∷ᒷ⍊ᒷリ⚍ᒷ, ᓵ𝙹ᓭℸᓭ, ᔑリ↸ ⎓╎リᔑリᓵ╎ᔑꖎ !¡ᒷ∷⎓𝙹∷ᒲᔑリᓵᒷ', + totalRevenue: 'ℸ𝙹ℸᔑꖎ ∷ᒷ⍊ᒷリ⚍ᒷ', + totalCosts: 'ℸ𝙹ℸᔑꖎ ᓵ𝙹ᓭℸᓭ', + netProfit: 'リᒷℸ !¡∷𝙹⎓╎ℸ', + avgOrderValue: 'ᔑ⍊⊣ 𝙹∷↸ᒷ∷ ⍊ᔑꖎ⚍ᒷ', + fromOrders: '⎓∷𝙹ᒲ {count} 𝙹∷↸ᒷ∷ᓭ', + costBreakdown: '!¡∷𝙹ᓵ⚍∷ᒷᒲᒷリℸ + 𝙹!¡ᒷ∷ᔑℸ╎𝙹リᔑꖎ + ꖎᔑʖ𝙹∷ + 𝙹⍊ᒷ∷⍑ᒷᔑ↸', + margin: 'ᒲᔑ∷⊣╎リ', + perOrderRevenue: '!¡ᒷ∷ 𝙹∷↸ᒷ∷ ∷ᒷ⍊ᒷリ⚍ᒷ', + revenueVsCosts: { + title: 'ᒲ𝙹リℸ⍑ꖎꘐ ∷ᒷ⍊ᒷリ⚍ᒷ ⍊ᓭ ᓵ𝙹ᓭℸᓭ', + revenue: '∷ᒷ⍊ᒷリ⚍ᒷ', + costs: 'ℸ𝙹ℸᔑꖎ ᓵ𝙹ᓭℸᓭ', + }, + monthlyCostFlow: { + title: 'ᒲ𝙹リℸ⍑ꖎꘐ ᓵ𝙹ᓭℸ ⎓ꖎ𝙹∴', + procurement: '!¡∷𝙹ᓵ⚍∷ᒷᒲᒷリℸ', + operational: '𝙹!¡ᒷ∷ᔑℸ╎𝙹リᔑꖎ', + labor: 'ꖎᔑʖ𝙹∷', + overhead: '𝙹⍊ᒷ∷⍑ᒷᔑ↸', + }, + categorySpending: { + title: 'ᓭ!¡ᒷリ↸╎リ⊣ ʖꘐ ᓵᔑℸᒷ⊣𝙹∷ꘐ', + ofTotal: '𝙹⎓ ℸ𝙹ℸᔑꖎ', + }, + transactions: { + title: '∷ᒷᓵᒷリℸ ℸ∷ᔑリᓭᔑᓵℸ╎𝙹リᓭ', + id: '╎↸', + description: '↸ᒷᓭᓵ∷╎!¡ℸ╎𝙹リ', + vendor: '⍊ᒷリ↸𝙹∷', + date: '↸ᔑℸᒷ', + amount: 'ᔑᒲ𝙹⚍リℸ', + }, + }, + demand: { + title: '↸ᒷᒲᔑリ↸ ⎓𝙹∷ᒷᓵᔑᓭℸ', + description: 'ᔑリᔑꖎꘐᘑᒷ ↸ᒷᒲᔑリ↸ ℸ∷ᒷリ↸ᓭ ᔑリ↸ ⎓𝙹∷ᒷᓵᔑᓭℸᓭ', + increasingDemand: '╎リᓵ∷ᒷᔑᓭ╎リ⊣ ↸ᒷᒲᔑリ↸', + stableDemand: 'ᓭℸᔑʖꖎᒷ ↸ᒷᒲᔑリ↸', + decreasingDemand: '↸ᒷᓵ∷ᒷᔑᓭ╎リ⊣ ↸ᒷᒲᔑリ↸', + itemsCount: '{count} ╎ℸᒷᒲᓭ', + more: 'ᒲ𝙹∷ᒷ...', + demandForecasts: '↸ᒷᒲᔑリ↸ ⎓𝙹∷ᒷᓵᔑᓭℸᓭ', + table: { + sku: 'ᓭꖌ⚍', + itemName: '╎ℸᒷᒲ リᔑᒲᒷ', + currentDemand: 'ᓵ⚍∷∷ᒷリℸ ↸ᒷᒲᔑリ↸', + forecastedDemand: '⎓𝙹∷ᒷᓵᔑᓭℸᒷ↸ ↸ᒷᒲᔑリ↸', + change: 'ᓵ⍑ᔑリ⊣ᒷ', + trend: 'ℸ∷ᒷリ↸', + period: '!¡ᒷ∷╎𝙹↸', + }, + }, + filters: { + timePeriod: 'ℸ╎ᒲᒷ !¡ᒷ∷╎𝙹↸', + location: 'ꖎ𝙹ᓵᔑℸ╎𝙹リ', + category: 'ᓵᔑℸᒷ⊣𝙹∷ꘐ', + orderStatus: '𝙹∷↸ᒷ∷ ᓭℸᔑℸ⚍ᓭ', + all: 'ᔑꖎꖎ', + allMonths: 'ᔑꖎꖎ ᒲ𝙹リℸ⍑ᓭ', + }, + status: { + delivered: '↸ᒷꖎ╎⍊ᒷ∷ᒷ↸', + shipped: 'ᓭ⍑╎!¡!¡ᒷ↸', + processing: '!¡∷𝙹ᓵᒷᓭᓭ╎リ⊣', + backordered: 'ʖᔑᓵꖌ𝙹∷↸ᒷ∷ᒷ↸', + inStock: '╎リ ᓭℸ𝙹ᓵꖌ', + lowStock: 'ꖎ𝙹∴ ᓭℸ𝙹ᓵꖌ', + adequate: 'ᔑ↸ᒷᑑ⚍ᔑℸᒷ', + }, + trends: { + increasing: '╎リᓵ∷ᒷᔑᓭ╎リ⊣', + stable: 'ᓭℸᔑʖꖎᒷ', + decreasing: '↸ᒷᓵ∷ᒷᔑᓭ╎リ⊣', + }, + priority: { + high: '⍑╎⊣⍑', + medium: 'ᒲᒷ↸╎⚍ᒲ', + low: 'ꖎ𝙹∴', + }, + categories: { + circuitBoards: 'ᓵ╎∷ᓵ⚍╎ℸ ʖ𝙹ᔑ∷↸ᓭ', + sensors: 'ᓭᒷリᓭ𝙹∷ᓭ', + actuators: 'ᔑᓵℸ⚍ᔑℸ𝙹∷ᓭ', + controllers: 'ᓵ𝙹リℸ∷𝙹ꖎꖎᒷ∷ᓭ', + powerSupplies: '!¡𝙹∴ᒷ∷ ᓭ⚍!¡!¡ꖎ╎ᒷᓭ', + }, + spendingCategories: { + rawMaterials: '∷ᔑ∴ ᒲᔑℸᒷ∷╎ᔑꖎᓭ', + components: 'ᓵ𝙹ᒲ!¡𝙹リᒷリℸᓭ', + equipment: 'ᒷᑑ⚍╎!¡ᒲᒷリℸ', + consumables: 'ᓵ𝙹リᓭ⚍ᒲᔑʖꖎᒷᓭ', + }, + warehouses: { + sanFrancisco: 'ᓭᔑリ ⎓∷ᔑリᓵ╎ᓭᓵ𝙹', + london: 'ꖎ𝙹リ↸𝙹リ', + tokyo: 'ℸ𝙹ꖌꘐ𝙹', + }, + months: { + jan: '⋮ᔑリ', + feb: '⎓ᒷʖ', + mar: 'ᒲᔑ∷', + apr: 'ᔑ!¡∷', + may: 'ᒲᔑꘐ', + jun: '⋮⚍リ', + jul: '⋮⚍ꖎ', + aug: 'ᔑ⚍⊣', + sep: 'ᓭᒷ!¡', + oct: '𝙹ᓵℸ', + nov: 'リ𝙹⍊', + dec: '↸ᒷᓵ', + january: '⋮ᔑリ⚍ᔑ∷ꘐ', + february: '⎓ᒷʖ∷⚍ᔑ∷ꘐ', + march: 'ᒲᔑ∷ᓵ⍑', + april: 'ᔑ!¡∷╎ꖎ', + june: '⋮⚍リᒷ', + july: '⋮⚍ꖎꘐ', + august: 'ᔑ⚍⊣⚍ᓭℸ', + september: 'ᓭᒷ!¡ℸᒷᒲʖᒷ∷', + october: '𝙹ᓵℸ𝙹ʖᒷ∷', + november: 'リ𝙹⍊ᒷᒲʖᒷ∷', + december: '↸ᒷᓵᒷᒲʖᒷ∷', + }, + profile: { + profileDetails: '!¡∷𝙹⎓╎ꖎᒷ ↸ᒷℸᔑ╎ꖎᓭ', + myTasks: 'ᒲꘐ ℸᔑᓭꖌᓭ', + logout: 'ꖎ𝙹⊣𝙹⚍ℸ', + }, + profileDetails: { + title: '!¡∷𝙹⎓╎ꖎᒷ ↸ᒷℸᔑ╎ꖎᓭ', + email: 'ᒷᒲᔑ╎ꖎ', + department: '↸ᒷ!¡ᔑ∷ℸᒲᒷリℸ', + location: 'ꖎ𝙹ᓵᔑℸ╎𝙹リ', + phone: '!¡⍑𝙹リᒷ', + joinDate: '⋮𝙹╎リ ↸ᔑℸᒷ', + employeeId: 'ᒷᒲ!¡ꖎ𝙹ꘐᒷᒷ ╎↸', + close: 'ᓵꖎ𝙹ᓭᒷ', + }, + tasks: { + title: 'ᒲꘐ ℸᔑᓭꖌᓭ', + taskTitle: 'ℸᔑᓭꖌ ℸ╎ℸꖎᒷ', + taskTitlePlaceholder: 'ᒷリℸᒷ∷ ℸᔑᓭꖌ ℸ╎ℸꖎᒷ...', + priority: '!¡∷╎𝙹∷╎ℸꘐ', + dueDate: '↸⚍ᒷ ↸ᔑℸᒷ', + addTask: 'ᔑ↸↸ ℸᔑᓭꖌ', + noTasks: 'リ𝙹 ℸᔑᓭꖌᓭ ꘐᒷℸ. ᔑ↸↸ ꘐ𝙹⚍∷ ⎓╎∷ᓭℸ ℸᔑᓭꖌ ᔑʖ𝙹⍊ᒷ!', + }, + language: { + english: 'ᒷリ⊣ꖎ╎ᓭ⍑', + japanese: '⋮ᔑ!¡ᔑリᒷᓭᒷ', + selectLanguage: 'ᓭᒷꖎᒷᓵℸ ꖎᔑリ⊣⚍ᔑ⊣ᒷ', + starwars: 'ᓭℸᔑ∷ ∴ᔑ∷ᓭ', + minecraft: 'ᒲ╎リᒷᓵ∷ᔑ⎓ℸ', + }, + common: { + loading: 'ꖎ𝙹ᔑ↸╎リ⊣...', + error: 'ᒷ∷∷𝙹∷', + noData: 'リ𝙹 ↸ᔑℸᔑ ᔑ⍊ᔑ╎ꖎᔑʖꖎᒷ', + viewDetails: '⍊╎ᒷ∴ ↸ᒷℸᔑ╎ꖎᓭ', + close: 'ᓵꖎ𝙹ᓭᒷ', + save: 'ᓭᔑ⍊ᒷ', + cancel: 'ᓵᔑリᓵᒷꖎ', + search: 'ᓭᒷᔑ∷ᓵ⍑', + filter: '⎓╎ꖎℸᒷ∷', + export: 'ᒷ!¿!¡𝙹∷ℸ', + items: '╎ℸᒷᒲᓭ', + }, + reports: { + title: '!¡ᒷ∷⎓𝙹∷ᒲᔑリᓵᒷ ∷ᒷ!¡𝙹∷ℸᓭ', + description: 'ᑑ⚍ᔑ∷ℸᒷ∷ꖎꘐ ᔑリ↸ ᒲ𝙹リℸ⍑ꖎꘐ !¡ᒷ∷⎓𝙹∷ᒲᔑリᓵᒷ ᔑリᔑꖎꘐℸ╎ᓵᓭ', + quarterlyTitle: 'ᑑ⚍ᔑ∷ℸᒷ∷ꖎꘐ !¡ᒷ∷⎓𝙹∷ᒲᔑリᓵᒷ', + monthlyTitle: 'ᒲ𝙹リℸ⍑ꖎꘐ ∷ᒷ⍊ᒷリ⚍ᒷ ℸ∷ᒷリ↸', + momTitle: 'ᒲ𝙹リℸ⍑-𝙹⍊ᒷ∷-ᒲ𝙹リℸ⍑ ᔑリᔑꖎꘐᓭ╎ᓭ', + quarter: 'ᑑ⚍ᔑ∷ℸᒷ∷', + totalOrders: 'ℸ𝙹ℸᔑꖎ 𝙹∷↸ᒷ∷ᓭ', + totalRevenue: 'ℸ𝙹ℸᔑꖎ ∷ᒷ⍊ᒷリ⚍ᒷ', + delivered: '↸ᒷꖎ╎⍊ᒷ∷ᒷ↸', + avgOrderValue: 'ᔑ⍊⊣ 𝙹∷↸ᒷ∷ ⍊ᔑꖎ⚍ᒷ', + fulfillmentRate: '⎓⚍ꖎ⎓╎ꖎꖎᒲᒷリℸ ∷ᔑℸᒷ', + month: 'ᒲ𝙹リℸ⍑', + orderCount: '𝙹∷↸ᒷ∷ ᓵ𝙹⚍リℸ', + revenue: '∷ᒷ⍊ᒷリ⚍ᒷ', + deliveredCount: '↸ᒷꖎ╎⍊ᒷ∷ᒷ↸ ᓵ𝙹⚍リℸ', + change: 'ᓵ⍑ᔑリ⊣ᒷ', + growthRate: '⊣∷𝙹∴ℸ⍑ ∷ᔑℸᒷ', + ytdRevenue: 'ℸ𝙹ℸᔑꖎ ∷ᒷ⍊ᒷリ⚍ᒷ (ꘐℸ↸)', + ytdOrders: 'ℸ𝙹ℸᔑꖎ 𝙹∷↸ᒷ∷ᓭ (ꘐℸ↸)', + avgFulfillment: 'ᔑ⍊⊣ ⎓⚍ꖎ⎓╎ꖎꖎᒲᒷリℸ ∷ᔑℸᒷ', + avgMonthlyRevenue: 'ᔑ⍊⊣ ᒲ𝙹リℸ⍑ꖎꘐ ∷ᒷ⍊ᒷリ⚍ᒷ', + ytdDelivered: '↸ᒷꖎ╎⍊ᒷ∷ᒷ↸ 𝙹∷↸ᒷ∷ᓭ (ꘐℸ↸)', + bestQuarter: 'ʖᒷᓭℸ !¡ᒷ∷⎓𝙹∷ᒲ╎リ⊣ ᑑ⚍ᔑ∷ℸᒷ∷', + }, + backlog: { + title: 'ʖᔑᓵꖌꖎ𝙹⊣ ᒲᔑリᔑ⊣ᒷᒲᒷリℸ', + description: 'ℸ∷ᔑᓵꖌ ᔑリ↸ ∷ᒷᓭ𝙹ꖎ⍊ᒷ ╎リ⍊ᒷリℸ𝙹∷ꘐ ᓭ⍑𝙹∷ℸᔑ⊣ᒷᓭ', + highPriority: '⍑╎⊣⍑ !¡∷╎𝙹∷╎ℸꘐ', + mediumPriority: 'ᒲᒷ↸╎⚍ᒲ !¡∷╎𝙹∷╎ℸꘐ', + lowPriority: 'ꖎ𝙹∴ !¡∷╎𝙹∷╎ℸꘐ', + totalItems: 'ℸ𝙹ℸᔑꖎ ʖᔑᓵꖌꖎ𝙹⊣ ╎ℸᒷᒲᓭ', + items: 'ʖᔑᓵꖌꖎ𝙹⊣ ╎ℸᒷᒲᓭ', + noItems: 'リ𝙹 ʖᔑᓵꖌꖎ𝙹⊣ ╎ℸᒷᒲᓭ - ᔑꖎꖎ 𝙹∷↸ᒷ∷ᓭ ᓵᔑリ ʖᒷ ⎓⚍ꖎ⎓╎ꖎꖎᒷ↸!', + orderId: '𝙹∷↸ᒷ∷ ╎↸', + sku: 'ᓭꖌ⚍', + itemName: '╎ℸᒷᒲ リᔑᒲᒷ', + quantityNeeded: 'ᑑ⚍ᔑリℸ╎ℸꘐ リᒷᒷ↸ᒷ↸', + quantityAvailable: 'ᑑ⚍ᔑリℸ╎ℸꘐ ᔑ⍊ᔑ╎ꖎᔑʖꖎᒷ', + shortage: 'ᓭ⍑𝙹∷ℸᔑ⊣ᒷ', + daysDelayed: '↸ᔑꘐᓭ ↸ᒷꖎᔑꘐᒷ↸', + priority: '!¡∷╎𝙹∷╎ℸꘐ', + unitsShort: '⚍リ╎ℸᓭ ᓭ⍑𝙹∷ℸ', + days: '↸ᔑꘐᓭ', + }, +} diff --git a/client/src/locales/sw.js b/client/src/locales/sw.js new file mode 100644 index 000000000..13737a115 --- /dev/null +++ b/client/src/locales/sw.js @@ -0,0 +1,381 @@ +export default { + // Navigation + nav: { + overview: 'Command Center', + inventory: 'Cargo Hold', + orders: 'Trade Routes', + finance: 'Credits Ledger', + demandForecast: 'Intel Report', + reports: 'Mission Logs', + restocking: 'Supply Run', + companyName: 'Catalyst Components', + subtitle: 'Galactic Inventory System' + }, + + // Dashboard + dashboard: { + title: 'Command Center', + kpi: { + title: 'Galactic Intelligence Metrics', + inventoryTurnover: 'Cargo Rotation Rate', + ordersFulfilled: 'Missions Completed', + orderFillRate: 'Mission Success Rate', + revenue: 'Credits Earned (Trades)', + revenueYTD: 'Credits Earned (Trades) YTD', + revenueMTD: 'Credits Earned (Trades) MTD', + avgProcessingTime: 'Avg Hyperjump Time (Cycles)', + goal: 'Imperial Quota' + }, + summary: { + title: 'Holographic Briefing' + }, + orderHealth: { + title: 'Trade Route Health', + totalOrders: 'Total Trade Runs', + revenue: 'Credits', + avgOrderValue: 'Avg Trade Value', + onTimeRate: 'On-Time Rate', + avgFulfillmentDays: 'Avg Delivery Cycles', + total: 'Total' + }, + ordersByMonth: { + title: 'Trade Routes by Cycle' + }, + inventoryValue: { + title: 'Cargo Value by Class' + }, + inventoryShortages: { + title: 'Cargo Shortages', + noShortages: 'No cargo shortages - all trade routes can be fulfilled!', + noData: 'No cargo data for selected sectors', + orderId: 'Mission ID', + sku: 'Cargo Code', + itemName: 'Cargo Name', + quantityNeeded: 'Quantity Required', + quantityAvailable: 'Quantity in Hold', + shortage: 'Shortage', + daysDelayed: 'Cycles Delayed', + priority: 'Alert Level', + unitsShort: 'units short', + days: 'cycles' + }, + topProducts: { + title: 'Top Cargo by Credits', + sku: 'Cargo Code', + product: 'Cargo', + category: 'Class', + warehouse: 'Station', + stockStatus: 'Hold Status', + revenue: 'Credits', + unitsOrdered: 'Units Traded', + firstOrder: 'First Trade', + inStock: 'Available', + lowStock: 'Running Low' + } + }, + + // Inventory + inventory: { + title: 'Cargo Hold', + description: 'Track and manage all cargo in your hold', + stockLevels: 'Hold Levels', + skus: 'Cargo Codes', + searchPlaceholder: 'Scan cargo hold...', + clearSearch: 'Clear scan', + totalItems: 'Total Cargo', + totalValue: 'Total Credits', + lowStockItems: 'Low Hold Items', + warehouses: 'Stations', + table: { + sku: 'Cargo Code', + itemName: 'Cargo Name', + name: 'Name', + category: 'Class', + warehouse: 'Station', + quantity: 'Quantity', + quantityOnHand: 'Quantity on Hand', + reorderPoint: 'Resupply Point', + unitCost: 'Unit Cost', + unitPrice: 'Unit Price', + totalValue: 'Total Credits', + location: 'Sector', + status: 'Status' + } + }, + + // Orders + orders: { + title: 'Trade Routes', + description: 'View and manage all trade missions', + allOrders: 'All Trade Routes', + totalOrders: 'Total Trade Routes', + totalRevenue: 'Total Credits', + avgOrderValue: 'Avg Trade Value', + onTimeDelivery: 'On-Time Delivery', + itemsCount: '{count} items', + quantity: 'Qty', + table: { + orderNumber: 'Mission Number', + orderId: 'Mission ID', + orderDate: 'Mission Date', + date: 'Date', + customer: 'Client', + category: 'Class', + warehouse: 'Station', + items: 'Cargo', + value: 'Credits', + totalValue: 'Total Credits', + status: 'Status', + expectedDelivery: 'Estimated Arrival', + actualDelivery: 'Actual Arrival' + } + }, + + // Finance/Spending + finance: { + title: 'Credits Ledger', + description: 'Track revenue, costs, and galactic financial performance', + totalRevenue: 'Total Credits', + totalCosts: 'Total Expenditures', + netProfit: 'Net Credits', + avgOrderValue: 'Avg Trade Value', + fromOrders: 'From {count} trade runs', + costBreakdown: 'Procurement + Operational + Labor + Overhead', + margin: 'margin', + perOrderRevenue: 'Per trade revenue', + revenueVsCosts: { + title: 'Monthly Credits vs Expenditures', + revenue: 'Credits', + costs: 'Total Expenditures' + }, + monthlyCostFlow: { + title: 'Monthly Expenditure Flow', + procurement: 'Procurement', + operational: 'Operational', + labor: 'Labor', + overhead: 'Overhead' + }, + categorySpending: { + title: 'Spending by Class', + ofTotal: 'of total' + }, + transactions: { + title: 'Recent Transactions', + id: 'ID', + description: 'Description', + vendor: 'Vendor', + date: 'Date', + amount: 'Amount' + } + }, + + // Demand Forecast + demand: { + title: 'Intel Report', + description: 'Analyze demand trends across the galaxy', + increasingDemand: 'Rising Demand', + stableDemand: 'Balanced Demand', + decreasingDemand: 'Fading Demand', + itemsCount: '{count} items', + more: 'more...', + demandForecasts: 'Demand Intelligence', + table: { + sku: 'Cargo Code', + itemName: 'Cargo Name', + currentDemand: 'Current Demand', + forecastedDemand: 'Forecasted Demand', + change: 'Change', + trend: 'Trend', + period: 'Period' + } + }, + + // Filters + filters: { + timePeriod: 'Time Cycle', + location: 'Sector', + category: 'Class', + orderStatus: 'Mission Status', + all: 'All', + allMonths: 'All Cycles' + }, + + // Statuses + status: { + delivered: 'Kessel Run Complete', + shipped: 'Jumped to Hyperspace', + processing: 'Aboard the Falcon', + backordered: 'Stuck on Tatooine', + inStock: 'In Hold', + lowStock: 'Running Low', + adequate: 'Well Stocked' + }, + + // Trends + trends: { + increasing: 'rising', + stable: 'in the Force balance', + decreasing: 'fading' + }, + + // Priority + priority: { + high: 'Code Red Alert', + medium: 'Standard Alert', + low: 'No Disturbance' + }, + + // Categories + categories: { + circuitBoards: 'Droid Logic Boards', + sensors: 'Sensor Arrays', + actuators: 'Servo Units', + controllers: 'Navigation Controllers', + powerSupplies: 'Power Cells' + }, + + // Spending Categories + spendingCategories: { + rawMaterials: 'Raw Ore', + components: 'Components', + equipment: 'Equipment', + consumables: 'Rations' + }, + + // Warehouses + warehouses: { + sanFrancisco: 'Coruscant Hub', + london: 'Bespin Station', + tokyo: 'Tatooine Depot' + }, + + // Months + months: { + jan: 'First Cycle', + feb: 'Second Cycle', + mar: 'Third Cycle', + apr: 'Fourth Cycle', + may: 'Fifth Cycle', + jun: 'Sixth Cycle', + jul: 'Seventh Cycle', + aug: 'Eighth Cycle', + sep: 'Ninth Cycle', + oct: 'Tenth Cycle', + nov: 'Eleventh Cycle', + dec: 'Twelfth Cycle', + january: 'Month of the First Cycle', + february: 'Month of the Second Cycle', + march: 'Month of the Third Cycle', + april: 'Month of the Fourth Cycle', + june: 'Month of the Sixth Cycle', + july: 'Month of the Seventh Cycle', + august: 'Month of the Eighth Cycle', + september: 'Month of the Ninth Cycle', + october: 'Month of the Tenth Cycle', + november: 'Month of the Eleventh Cycle', + december: 'Month of the Twelfth Cycle' + }, + + // Profile Menu + profile: { + profileDetails: 'Mission Dossier', + myTasks: 'My Missions', + logout: 'Leave the Cantina' + }, + + // Profile Details Modal + profileDetails: { + title: 'Mission Dossier', + email: 'Holo Address', + department: 'Division', + location: 'Station', + phone: 'Comm Channel', + joinDate: 'Enlistment Date', + employeeId: 'Trooper ID', + close: 'These aren\'t the droids you\'re looking for' + }, + + // Tasks Modal + tasks: { + title: 'My Missions', + taskTitle: 'Mission Title', + taskTitlePlaceholder: 'Enter mission title...', + priority: 'Alert Level', + dueDate: 'Mission Deadline', + addTask: 'Add Mission', + noTasks: 'No missions yet. Plan your first mission above!' + }, + + // Language + language: { + english: 'Galactic Basic', + japanese: 'Japanese', + selectLanguage: 'Select Language', + starwars: 'Galactic Basic', + minecraft: 'Enchantment Language' + }, + + // Common + common: { + loading: 'Calculating jump to hyperspace...', + error: 'The Force has been disturbed', + noData: 'Nothing in the holonet, there is', + viewDetails: 'Open Holofile', + close: 'These aren\'t the droids you\'re looking for', + save: 'Transmit to Fleet', + cancel: 'Abort Mission', + search: 'Scan holonet', + filter: 'Narrow Sector', + export: 'Transmit Data', + items: 'items' + }, + + // Reports + reports: { + title: 'Mission Logs', + description: 'Quarterly and cycle performance analytics', + quarterlyTitle: 'Quarterly Mission Performance', + monthlyTitle: 'Monthly Credits Trend', + momTitle: 'Cycle-over-Cycle Analysis', + quarter: 'Quarter', + totalOrders: 'Total Trade Routes', + totalRevenue: 'Total Credits', + delivered: 'Delivered', + avgOrderValue: 'Avg Trade Value', + fulfillmentRate: 'Mission Success Rate', + month: 'Cycle', + orderCount: 'Trade Count', + revenue: 'Credits', + deliveredCount: 'Delivered Count', + change: 'Change', + growthRate: 'Growth Rate', + ytdRevenue: 'Total Credits (YTD)', + ytdOrders: 'Total Trades (YTD)', + avgFulfillment: 'Avg Mission Success Rate', + avgMonthlyRevenue: 'Avg Monthly Credits', + ytdDelivered: 'Delivered Trades (YTD)', + bestQuarter: 'Best Performing Quarter' + }, + + // Backlog + backlog: { + title: 'Supply Run', + description: 'Track and resolve cargo shortages', + highPriority: 'Code Red Alert', + mediumPriority: 'Standard Alert', + lowPriority: 'No Disturbance', + totalItems: 'Total Backlog Items', + items: 'Backlog Items', + noItems: 'No backlog - all trade routes can be fulfilled!', + orderId: 'Mission ID', + sku: 'Cargo Code', + itemName: 'Cargo Name', + quantityNeeded: 'Quantity Required', + quantityAvailable: 'Quantity in Hold', + shortage: 'Shortage', + daysDelayed: 'Cycles Delayed', + priority: 'Alert Level', + unitsShort: 'units short', + days: 'cycles' + } +} diff --git a/client/src/main.js b/client/src/main.js index 477c2d966..8884eea63 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -7,6 +7,7 @@ import Orders from './views/Orders.vue' import Demand from './views/Demand.vue' import Spending from './views/Spending.vue' import Reports from './views/Reports.vue' +import Restocking from './views/Restocking.vue' const router = createRouter({ history: createWebHistory(), @@ -16,7 +17,8 @@ const router = createRouter({ { path: '/orders', component: Orders }, { path: '/demand', component: Demand }, { path: '/spending', component: Spending }, - { path: '/reports', component: Reports } + { path: '/reports', component: Reports }, + { path: '/restocking', component: Restocking } ] }) diff --git a/client/src/views/Backlog.vue b/client/src/views/Backlog.vue index bb3bac21d..e9fcf91b8 100644 --- a/client/src/views/Backlog.vue +++ b/client/src/views/Backlog.vue @@ -1,53 +1,53 @@