This commit is contained in:
2026-02-03 15:31:29 +05:00
commit 326c677e8d
2800 changed files with 1489388 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,29 @@
{
"name": "nurmuhammet/dynamic-fields",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
"Nurmuhammet\\DynamicFields\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Nurmuhammet\\DynamicFields\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1 @@

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/

View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

View File

@@ -0,0 +1,40 @@
const mix = require('laravel-mix')
const webpack = require('webpack')
const path = require('path')
class NovaExtension {
name() {
return 'nova-extension'
}
register(name) {
this.name = name
}
webpackPlugins() {
return new webpack.ProvidePlugin({
_: 'lodash',
Errors: 'form-backend-validation',
})
}
webpackConfig(webpackConfig) {
webpackConfig.externals = {
vue: 'Vue',
}
webpackConfig.resolve.alias = {
...(webpackConfig.resolve.alias || {}),
'laravel-nova': path.join(
__dirname,
'../../vendor/laravel/nova/resources/js/mixins/packages.js'
),
}
webpackConfig.output = {
uniqueName: this.name,
}
}
}
mix.extend('nova', new NovaExtension())

View File

@@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.2.22",
"form-backend-validation": "^2.3.3",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"vue-loader": "^16.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1 @@
/* Nova Field CSS */

View File

@@ -0,0 +1,107 @@
<template>
<div>
<div v-for="userField in userFields">
<DefaultField :field="currentFieldFor(userField.name)" :fieldName="userField.label" :errors="errors">
<template #field>
<template v-if="userField.type == 'select'">
<select
:id="userField.name"
v-model="values[userField.name]"
class="w-full block form-control form-input form-control-bordered"
disabled
>
<option value="">Saýla</option>
<option v-for="option in userField.options" :value="option.value">
{{ option.label }}
</option>
</select>
</template>
<template v-else>
<input
:id="userField.name"
:type="userField.type"
class="w-full form-control form-input form-control-bordered"
:class="errorClasses"
v-model="values[userField.name]"
:placeholder="userField.placeholder"
disabled
/>
</template>
<p v-if="hasError" class="my-2 text-danger">
{{ firstError }}
</p>
</template>
</DefaultField>
</div>
</div>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
import { capitalize } from 'lodash'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
data() {
return {
userFields: [],
values: [],
fillWithArrayName: '',
value: '',
}
},
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || ''
},
setValueFor(name, value) {
this.values[name] = value
},
currentFieldFor(name) {
const new_field = JSON.parse(JSON.stringify(this.currentField))
new_field.name = capitalize(name)
let userField = this.userFields.filter(field => field.name == name)[0]
if (userField['placeholder']) {
new_field.placeholder = userField['placeholder']
}
return new_field
}
},
computed: {
placeholder() {
return this.__('Choose an option')
}
},
mounted() {
this.fillWithArrayName = this.field.fillWithArrayName;
this.userFields = this.field.fields.map(field => {
this.values[field.name.toLowerCase()] = field.default ? field.default : ''
return {
type: field.type,
name: field.name.toLowerCase(),
label: field.label ? capitalize(field.label) : capitalize(field.name),
default: field.default,
placeholder: field.placeholder,
options: field.options
}
})
}
}
</script>

View File

@@ -0,0 +1,139 @@
<template>
<div>
<div v-for="userField in userFields">
<DefaultField :field="currentFieldFor(userField.name)" :fieldName="userField.label" :errors="errors">
<template #field>
<template v-if="userField.type == 'select'">
<select
:id="userField.name"
v-model="values[userField.name]"
:required="userField.required"
class="w-full block form-control form-input form-control-bordered"
>
<option value="">Saýla</option>
<option v-for="option in userField.options" :value="option.value">
{{ option.label }}
</option>
</select>
</template>
<template v-else>
<input
:id="userField.name"
:type="userField.type"
class="w-full form-control form-input form-control-bordered"
:class="errorClasses"
v-model="values[userField.name]"
:required="userField.required"
:placeholder="userField.placeholder"
/>
</template>
<p v-if="hasError" class="my-2 text-danger">
{{ firstError }}
</p>
</template>
</DefaultField>
</div>
</div>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
import { capitalize } from 'lodash'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
data() {
return {
userFields: [],
values: [],
fillWithArrayName: '',
value: '',
}
},
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || ''
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
if (this.fillWithArrayName) {
this.userFields.forEach(field => {
formData.append(this.fillWithArrayName + '[' + field['name'] + ']', this.values[field['name']])
})
return
}
this.userFields.forEach(field => {
formData.append(field['name'], this.values[field['name']])
})
},
setValueFor(name, value) {
this.values[name] = value
},
currentFieldFor(name) {
const new_field = JSON.parse(JSON.stringify(this.currentField))
new_field.name = capitalize(name)
let userField = this.userFields.filter(field => field.name == name)[0]
if (userField['placeholder']) {
new_field.placeholder = userField['placeholder']
}
if (userField['required']) {
new_field.required = userField['required']
}
return new_field
},
onSyncedField() {
this.userFields = this.formatFields()
},
formatFields() {
return this.currentField.fields.map(field => {
this.values[field.name.toLowerCase()] = this.values[field.name.toLowerCase()] || (field.default || '')
return {
type: field.type,
name: field.name.toLowerCase(),
label: field.label ? capitalize(field.label) : capitalize(field.name),
default: field.default,
required: field.required,
placeholder: field.placeholder,
options: field.options
}
})
}
},
computed: {
placeholder() {
return this.__('Choose an option')
}
},
mounted() {
this.fillWithArrayName = this.field.fillWithArrayName
this.currentField.fields = this.currentField.fields || [];
this.userFields = this.formatFields()
}
}
</script>

View File

@@ -0,0 +1,7 @@
import DetailField from './components/DetailField'
import FormField from './components/FormField'
Nova.booting((app, store) => {
app.component('detail-dynamic-fields', DetailField)
app.component('form-dynamic-fields', FormField)
})

View File

@@ -0,0 +1,58 @@
<?php
namespace Nurmuhammet\DynamicFields;
use Laravel\Nova\Exceptions\NovaException;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
class DynamicFields extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'dynamic-fields';
/**
* Specify that the element should be visible on the index view.
*
* @param (callable():bool)|bool $callback
* @return $this
*
* @throws \Laravel\Nova\Exceptions\NovaException
*/
public function showOnIndex($callback = true)
{
throw NovaException::helperNotSupported(__FUNCTION__, static::class);
}
/**
* Fields to be rendered
*
* @param array $hues
* @return $this
*/
public function fields(array|callable $fields)
{
$fieldsForFrontEnd = $fields;
if (is_callable($fields)) {
$fieldsForFrontEnd = call_user_func($fields);
}
return $this->withMeta(['fields' => $fieldsForFrontEnd]);
}
/**
* Fill with array name
*
* @return $this
*/
public function fillWithArrayName(string $requestArrayName = '')
{
return $this->withMeta(['fillWithArrayName' => $requestArrayName]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Nurmuhammet\DynamicFields;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('dynamic-fields', __DIR__.'/../dist/js/field.js');
Nova::style('dynamic-fields', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,10 @@
let mix = require('laravel-mix')
require('./nova.mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.vue({ version: 3 })
.css('resources/css/field.css', 'css')
.nova('nurmuhammet/dynamic-fields')

View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,29 @@
{
"name": "nurmuhammet/inline-relationship",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
"Nurmuhammet\\InlineRelationship\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Nurmuhammet\\InlineRelationship\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1 @@

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/

View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

View File

@@ -0,0 +1,40 @@
const mix = require('laravel-mix')
const webpack = require('webpack')
const path = require('path')
class NovaExtension {
name() {
return 'nova-extension'
}
register(name) {
this.name = name
}
webpackPlugins() {
return new webpack.ProvidePlugin({
_: 'lodash',
Errors: 'form-backend-validation',
})
}
webpackConfig(webpackConfig) {
webpackConfig.externals = {
vue: 'Vue',
}
webpackConfig.resolve.alias = {
...(webpackConfig.resolve.alias || {}),
'laravel-nova': path.join(
__dirname,
'../../vendor/laravel/nova/resources/js/mixins/packages.js'
),
}
webpackConfig.output = {
uniqueName: this.name,
}
}
}
mix.extend('nova', new NovaExtension())

View File

@@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.2.22",
"form-backend-validation": "^2.3.3",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"vue-loader": "^16.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1 @@
/* Nova Field CSS */

View File

@@ -0,0 +1,57 @@
<template>
<div class="overflow-hidden overflow-x-auto relative">
<table
v-if="resources.length > 0"
class="w-full divide-y divide-gray-100 dark:divide-gray-700"
data-testid="resource-table"
>
<table-header
:resource-name="resourceName"
:fields="fields"
/>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
<table-row
v-for="(resource, index) in resources"
@actionExecuted="$emit('actionExecuted')"
:testId="`${resourceName}-items-${index}`"
:key="`${resource.id.value}-items-${index}`"
:resource="resource"
:resource-name="resourceName"
/>
</tbody>
</table>
</div>
</template>
<script>
import TableHeader from './TableHeader.vue'
import TableRow from './TableRow.vue'
export default {
components: {
'table-header': TableHeader,
'table-row': TableRow,
},
props: {
resourceName: { default: null },
resources: { default: [] },
singularName: { type: String, required: true },
},
computed: {
/**
* Get all of the available fields for the resources.
*/
fields() {
if (this.resources) {
return this.resources[0].fields
}
},
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th
v-for="(field, index) in fields"
:key="field.uniqueKey"
:class="{
[`text-${field.textAlign}`]: true,
'whitespace-nowrap': !field.wrapping,
}"
class="uppercase text-gray-500 text-xxs tracking-wide py-2 px-2"
>
<span>{{ field.indexName }}</span>
</th>
<!-- View, Edit, and Delete -->
<th class="uppercase text-xxs tracking-wide px-2 py-2">
<span class="sr-only">{{ __('Controls') }}</span>
</th>
</tr>
</thead>
</template>
<script>
export default {
props: {
resourceName: String,
fields: {
type: [Object, Array],
},
}
}
</script>

View File

@@ -0,0 +1,169 @@
<template>
<tr
:data-pivot-id="resource.id.pivotValue"
:dusk="`${resource.id.value}-row`"
class="group"
>
<!-- Fields -->
<td
v-for="(field, index) in resource.fields"
:key="field.uniqueKey"
class="dark:bg-gray-800 group-hover:bg-gray-50 dark:group-hover:bg-gray-900 px-2"
>
<component
:is="'index-' + field.component"
:class="`text-${field.textAlign}`"
:field="field"
:resource="resource"
:resource-name="resourceName"
:via-resource="viaResource"
:via-resource-id="viaResourceId"
/>
</td>
<td class="px-2 td-fit text-right align-middle dark:bg-gray-800 group-hover:bg-gray-50 dark:group-hover:bg-gray-900 py-2">
<div class="flex items-center justify-end space-x-0 text-gray-400">
<!-- Preview Resource Link -->
<button
type="button"
v-tooltip.click="__('View')"
:aria-label="__('View')"
:dusk="`${resource['id'].value}-edit-button`"
class="toolbar-button hover:text-primary-500 px-2 disabled:opacity-50 disabled:pointer-events-none"
@click="openPreviewModal"
>
<Icon type="eye" />
</button>
<!-- Edit Resource Link -->
<!-- <button
type="button"
v-tooltip.click="__('Edit')"
:aria-label="__('Edit')"
:data-testid="`${testId}-delete-button`"
:dusk="`${resource['id'].value}-edit-button`"
class="toolbar-button hover:text-primary-500 px-2 disabled:opacity-50 disabled:pointer-events-none"
@click.stop="openEditModal"
>
<Icon type="pencil-alt" />
</button> -->
<!-- Delete Resource Link -->
<button
type="button"
v-tooltip.click="__('Delete')"
:aria-label="__('Delete')"
:data-testid="`${testId}-delete-button`"
:dusk="`${resource['id'].value}-delete-button`"
class="toolbar-button hover:text-primary-500 px-2 disabled:opacity-50 disabled:pointer-events-none"
@click.stop="openDeleteModal"
>
<Icon type="trash" />
</button>
<DeleteResourceModal
mode="delete"
:show="deleteModalOpen"
@close="closeDeleteModal"
@confirm="confirmDelete"
/>
</div>
</td>
</tr>
<PreviewResourceModal
v-if="previewModalOpen"
:resource-id="resource.id.value"
:resource-name="resourceName"
:show="previewModalOpen"
@close="closePreviewModal"
@confirm="closePreviewModal"
/>
</template>
<script>
import { Localization } from 'laravel-nova'
export default {
emits: ['actionExecuted'],
mixins: [Localization],
props: [
'testId',
'restoreResource',
'resource',
'resourcesSelected',
'resourceName',
'relationshipType',
'viaRelationship',
'viaResource',
'viaResourceId',
'viaManyToMany',
'checked',
'shouldShowCheckboxes',
'shouldShowColumnBorders',
'tableStyle',
'updateSelectionStatus',
'queryString',
'clickAction',
],
data: () => ({
previewModalOpen: false,
editModalOpen: false,
deleteModalOpen: false,
}),
methods: {
openPreviewModal() {
this.previewModalOpen = true;
},
closePreviewModal() {
this.previewModalOpen = false;
},
openEditModal() {
this.editModalOpen = true;
},
closeEditModal() {
this.editModalOpen = false;
},
openDeleteModal() {
this.deleteModalOpen = true
},
closeDeleteModal() {
this.deleteModalOpen = false
},
confirmDelete() {
this.deleteResource(this.resource)
this.closeDeleteModal()
},
/**
* Delete the given resource.
*/
deleteResource(resource) {
return Nova.request({
url: '/nova-api/' + this.resourceName,
method: 'delete',
params: {
'resources[]': resource.id.value
},
}).then(response => {
this.deleteModalOpen = false
Nova.success(this.__('The :resource was deleted!', { resource: this.resourceName }))
}).catch(exception => {
console.log({exception});
})
},
},
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,114 @@
<template>
<DefaultField
:field="currentField"
:errors="errors"
:show-help-text="showHelpText"
:full-width-content="fullWidthContent"
>
<template #field>
<inline-resource-table
:resourceName="currentField.relatedResourceURI"
:resources="resources"
:singularName="currentField.singularLabel"
@delete="deleteResources"
/>
<CreateRelationButton
v-tooltip="__('Create :resource', { resource: currentField.singularLabel })"
@click="openRelationModal"
:dusk="`${currentField.attribute}-inline-create`"
/>
<CreateRelationModal
:show="relationModalOpen"
@set-resource="handleSetResource"
@create-cancelled="closeRelationModal"
size="7xl"
:resource-name="currentField.relatedResourceURI"
:resource-id="resourceId"
:via-relationship="currentField.relationshipType"
:via-resource="currentField.relatedResourceURI"
:via-resource-id="viaResourceId"
/>
<p v-if="hasError" class="my-2 text-danger">
{{ firstError }}
</p>
</template>
</DefaultField>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
import InlineResourceTable from '../DataTables/InlineResourceTable.vue'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
components: {
'inline-resource-table': InlineResourceTable
},
data: () => ({
relationModalOpen: false,
viaResourceId: null,
resources: [],
createdResourceIds: [],
}),
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || ''
},
/**
* Open the relation modal
*/
openRelationModal() {
Nova.$emit('create-relation-modal-opened')
this.relationModalOpen = true
},
/**
* Close the relation modal
*/
closeRelationModal() {
this.relationModalOpen = false
Nova.$emit('create-relation-modal-closed')
},
/**
* Handle setting the selected resource
* @param int options.id
*/
handleSetResource({ id }) {
this.closeRelationModal()
this.createdResourceIds.push(id)
this.fetchResources()
},
fetchResources() {
Nova.request().get(`/nova-api/product-variants?ids=${this.createdResourceIds.join()}`).then(response => {
this.resources = response.data.resources
})
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
formData.append(this.fieldAttribute, this.createdResourceIds || [])
},
},
mounted() {
}
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<span>{{ fieldValue }}</span>
</template>
<script>
export default {
props: ['resourceName', 'field'],
computed: {
fieldValue() {
return this.field.displayedAs || this.field.value
},
}
}
</script>

View File

@@ -0,0 +1,9 @@
import IndexField from './components/IndexField'
import DetailField from './components/DetailField'
import FormField from './components/FormField'
Nova.booting((app, store) => {
app.component('index-inline-relationship', IndexField)
app.component('detail-inline-relationship', DetailField)
app.component('form-inline-relationship', FormField)
})

View File

@@ -0,0 +1,5 @@
<?php
use Illuminate\Support\Facades\Route;
// Route::post('{resource}', [InlineRelationshipController::class, 'store']);

View File

@@ -0,0 +1,52 @@
<?php
namespace Nurmuhammet\InlineRelationship;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
$this->app->booted(function () {
$this->routes();
});
Nova::serving(function (ServingNova $event) {
Nova::script('inline-relationship', __DIR__.'/../dist/js/field.js');
Nova::style('inline-relationship', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Package Routes
*/
protected function routes(): void
{
if ($this->app->routesAreCached()) {
return;
}
Route::middleware(['nova'])
->prefix('nova-vendor/nurmuhammet/inline-relationship')
->group(__DIR__.'/../routes/api.php');
}
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Nurmuhammet\InlineRelationship\Http\Controllers;
class InlineRelationshipController {}

View File

@@ -0,0 +1,115 @@
<?php
namespace Nurmuhammet\InlineRelationship;
use Exception;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
class InlineRelationship extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'inline-relationship';
/**
* Related resource name
*/
protected string $relatedResourceName;
/**
* Resource relationship attribute to related resource
*/
protected string $relationshipAttribute;
/**
* Related resource
*/
protected string $relatedResource;
/**
* Relationship type
*/
protected string $relationshipType;
/**
* Create a new field.
*
* @param string|null $attribute
* @param class-string<\Laravel\Nova\Resource>|null $resource
* @return void
*/
public function __construct(string $name, string $attribute, string $resource)
{
parent::__construct($name, $attribute);
$this->relatedResourceName = $name;
$this->relationshipAttribute = $attribute;
$this->relatedResource = $resource;
$this->relatedResourceURI = $resource::uriKey();
$this->relationshipType = $this->guessRelationshipType();
$this->validate();
$this->fullWidth();
$this->transformDataForFrontend();
}
/**
* Validate user's arguments
*/
public function validate(): void
{
in_array($this->relationshipType, $this->allowedRelationships())
? true
: throw new Exception("Selected relationship ({$relationshipType}) is not allowed");
}
/**
* Alloed relationships
*/
public function allowedRelationships(): array
{
return [
'Illuminate\\Database\\Eloquent\\Relations\\HasMany',
];
}
/**
* Guess relationship type from related resource
*/
public function guessRelationshipType(): string
{
return get_class((new $this->relatedResource::$model)->{$this->relationshipAttribute}());
}
/**
* Formatted relationship name
*/
public function formattedRelatioshipName(): string
{
return match ($this->relationshipType) {
'Illuminate\\Database\\Eloquent\\Relations\\HasMany' => 'hasMany',
};
}
/**
* Transform data to frontend
*/
public function transformDataForFrontend(): self
{
return $this->withMeta([
'singularLabel' => $this->relatedResource::singularLabel(),
'relatedResourceName' => $this->relatedResourceName,
'relationshipAttribute' => $this->relationshipAttribute,
'relatedResource' => $this->relatedResource,
'relatedResourceURI' => $this->relatedResourceURI,
'relationshipType' => $this->formattedRelatioshipName(),
]);
}
}

View File

@@ -0,0 +1,10 @@
let mix = require('laravel-mix')
require('./nova.mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.vue({ version: 3 })
.css('resources/css/field.css', 'css')
.nova('nurmuhammet/inline-relationship')

View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,29 @@
{
"name": "nurmuhammet/inventory-history-items",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
"Nurmuhammet\\InventoryHistoryItems\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Nurmuhammet\\InventoryHistoryItems\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1 @@
.grid{display:grid}.grid-col-3{grid-template-columns:1fr 1fr 1fr}.grid-col-gap-sm{grid-column-gap:6px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

View File

@@ -0,0 +1,40 @@
const mix = require('laravel-mix')
const webpack = require('webpack')
const path = require('path')
class NovaExtension {
name() {
return 'nova-extension'
}
register(name) {
this.name = name
}
webpackPlugins() {
return new webpack.ProvidePlugin({
_: 'lodash',
Errors: 'form-backend-validation',
})
}
webpackConfig(webpackConfig) {
webpackConfig.externals = {
vue: 'Vue',
}
webpackConfig.resolve.alias = {
...(webpackConfig.resolve.alias || {}),
'laravel-nova': path.join(
__dirname,
'../../vendor/laravel/nova/resources/js/mixins/packages.js'
),
}
webpackConfig.output = {
uniqueName: this.name,
}
}
}
mix.extend('nova', new NovaExtension())

View File

@@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.2.22",
"form-backend-validation": "^2.3.3",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"vue-loader": "^16.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1,11 @@
.grid {
display: grid;
}
.grid-col-3 {
grid-template-columns: 1fr 1fr 1fr;
}
.grid-col-gap-sm {
grid-column-gap: 6px;
}

View File

@@ -0,0 +1,9 @@
<template>
<PanelItem :index="index" :field="field" />
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
}
</script>

View File

@@ -0,0 +1,231 @@
<template>
<DefaultField
:field="field"
:errors="errors"
:show-help-text="showHelpText"
:full-width-content="fullWidthContent"
>
<template #field>
<SearchInput
v-if="!currentlyIsReadonly"
:data-testid="`${field.attribute}-search-input`"
:error="hasError"
:value="selectedOption"
:data="options"
@input="performSearch"
@clear="clearSelection"
@selected="selectOption"
:clearable="field.nullable"
trackBy="value"
class="w-full"
>
<!-- The Selected Option Slot -->
<div v-if="selectedOption" class="flex items-center">
{{ selectedOption.label }}
</div>
<template #option="{ selected, option }">
<!-- Options List Slot -->
<div
class="flex items-center text-sm font-semibold leading-5"
:class="{ 'text-white': selected }"
>
{{ option.label }}
</div>
</template>
</SearchInput>
<div class="w-full grid grid-col-3 grid-col-gap-sm">
<div>
<label for="">Sany: </label>
<input
type="number"
v-model="form.quantity"
placeholder="Sany"
class="w-full form-control form-input form-input-bordered"
>
</div>
<div>
<label for="">Bahasy: </label>
<input
type="text"
v-model="form.price"
placeholder="Bahasy"
class="w-full form-control form-input form-input-bordered"
disabled
>
</div>
<div>
<label for="">Jemi: </label>
<input
type="text"
v-model="form.total"
placeholder="Jemi"
class="w-full form-control form-input form-input-bordered"
dusk="product-item-total"
disabled
>
</div>
</div>
</template>
</DefaultField>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
data: () => ({
options: [],
search: '',
selectedOption: null,
form: {
product_id: null,
quantity: 0,
price: 0,
total: 0,
}
}),
mounted() {
},
watch: {
'form.quantity'() {
this.updateProductTotal()
},
selectedOption() {
if (! this.selectedOption || ! this.selectedOption.value) {
return;
}
this.form.product_id = this.selectedOption.value.id;
this.form.price = this.selectedOption.value.cost_amount;
this.updateProductTotal()
}
},
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.field.value || ''
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
formData.append('product_id', this.form.product_id)
formData.append('quantity', this.form.quantity)
formData.append('total', this.form.total)
formData.append('cost_amount', this.form.price)
},
/**
* Set the search string to be used to filter the select field.
*/
performSearch(event) {
this.search = event
this.searchProducts()
},
/**
* Clear the current selection for the field.
*/
clearSelection() {
this.selectedOption = null
this.search = ''
this.$refs.searchable.close()
},
/**
* Select the given option.
*/
selectOption(option) {
this.selectedOption = option
this.value = option.value.id
},
handleChange() {
this.$store.commit(`${this.resourceName}/updateFilterState`, {
filterClass: this.filterKey,
value: this.value,
})
this.$emit('change')
},
serializeOptions(options) {
return options.map(item => {
return {
label: item.name,
value: item
}
})
},
updateProductTotal() {
if (this.form.price && this.form.quantity) {
this.form.total = this.form.price * this.form.quantity;
}
setTimeout(() => {
this.updateOveralProductTotal()
}, 200);
},
updateOveralProductTotal() {
let totalElement = document.querySelector('[dusk="readonly_total"]');
if (! totalElement) {
return;
}
let total = 0;
document.querySelectorAll('[dusk="product-item-total"]').forEach(item => {
total += Number(item.value)
})
totalElement.value = total
},
searchProducts() {
if (! this.search && this.search.length < 1) {
return;
}
Nova.$progress.start()
Nova.request().get(`${window.location.origin}/api/v1/search-product?internal=1&q=${encodeURIComponent(this.search)}`, {
headers: {
Accept: 'application/json',
'Api-Token': 'hello-bad-mf-s',
},
}).then(response => {
if (response.status !== 200) {
this.clearSelection();
return;
}
this.options = this.serializeOptions(response.data.data);
})
Nova.$progress.done()
}
},
computed: {},
}
</script>

View File

@@ -0,0 +1,7 @@
import DetailField from './components/DetailField'
import FormField from './components/FormField'
Nova.booting((app, store) => {
app.component('detail-inventory-history-items', DetailField)
app.component('form-inventory-history-items', FormField)
})

View File

@@ -0,0 +1,33 @@
<?php
namespace Nurmuhammet\InventoryHistoryItems;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('inventory-history-items', __DIR__.'/../dist/js/field.js');
Nova::style('inventory-history-items', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Nurmuhammet\InventoryHistoryItems;
use Illuminate\Support\Collection;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
class InventoryHistoryItems extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'inventory-history-items';
/**
* Set the options for the select menu.
*
* @param array<string|int, array<string, mixed>|string>|\Closure|callable|\Illuminate\Support\Collection $options
*
* @phpstan-param TOption|(callable(): (TOption))|(\Closure(): (TOption)) $options
*
* @return $this
*/
public function options(array|Collection $options): self
{
return $this->withMeta([
'options' => $this->serializeOptions($options),
]);
}
/**
* Serialize Options
*
* @param Collection $options
*/
public function serializeOptions(array|Collection $options): array|Collection
{
return collect($options)->map(fn ($label, $value) => ['label' => $label, 'value' => $value]);
}
}

View File

@@ -0,0 +1,10 @@
let mix = require('laravel-mix')
require('./nova.mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.vue({ version: 3 })
.css('resources/css/field.css', 'css')
.nova('nurmuhammet/inventory-history-items')

View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,29 @@
{
"name": "nurmuhammet/payout-products",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
"Nurmuhammet\\PayoutProducts\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Nurmuhammet\\PayoutProducts\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1 @@
.payout-product-card{*,:after,:before{border:0 solid #e9ecef;box-sizing:border-box}:after,:before{--tw-content:""}body{line-height:inherit;margin:0}a{text-decoration:inherit}a,button{color:inherit}button{-webkit-appearance:button;background-color:initial;background-image:none;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;padding:0;text-transform:none}button,p{margin:0}input::-moz-placeholder{color:#a0aec0;opacity:1}input::placeholder{color:#a0aec0;opacity:1}button{cursor:pointer}:disabled{cursor:default}img{display:block;height:auto;max-width:100%;vertical-align:middle}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.absolute{position:absolute}.relative{position:relative}.top-3{top:.75rem}.right-3{right:.75rem}.z-0{z-index:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-3{margin-bottom:.75rem}.mb-2{margin-bottom:.5rem}.mt-1{margin-top:.25rem}.ml-px{margin-left:1px}.-mr-3{margin-right:-.75rem}.flex{display:flex}.inline-flex{display:inline-flex}.h-full{height:100%}.h-8{height:2rem}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-8{width:2rem}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.overflow-hidden{overflow:hidden}.rounded-xl{border-radius:.75rem}.rounded-full{border-radius:9999px}.rounded-\[20px\]{border-radius:20px}.border-2{border-width:2px}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-\[\#E0E5F2\]{--tw-bg-opacity:1;background-color:rgb(224 229 242/var(--tw-bg-opacity))}.bg-brand-900{--tw-bg-opacity:1;background-color:rgb(17 4 122/var(--tw-bg-opacity))}.bg-clip-border{background-clip:initial}.object-cover{-o-object-fit:cover;object-fit:cover}.p-2{padding:.5rem}.\!p-4{padding:1rem!important}.px-4{padding-left:1rem;padding-right:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-brand-500{--tw-text-opacity:1;color:rgb(66 42 251/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(163 174 208/var(--tw-text-opacity))}.text-navy-700{--tw-text-opacity:1;color:rgb(27 37 75/var(--tw-text-opacity))}.shadow-3xl{--tw-shadow:14px 17px 40px 4px;--tw-shadow-colored:14px 17px 40px 4px var(--tw-shadow-color);box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-shadow-500{--tw-shadow-color:rgba(112,144,176,.08);--tw-shadow:var(--tw-shadow-colored)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.hover\:cursor-pointer:hover{cursor:pointer}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(248 249 250/var(--tw-bg-opacity))}.hover\:bg-brand-800:hover{--tw-bg-opacity:1;background-color:rgb(25 7 147/var(--tw-bg-opacity))}.active\:bg-brand-700:active{--tw-bg-opacity:1;background-color:rgb(33 17 165/var(--tw-bg-opacity))}@media (min-width:768px){.md\:mt-2{margin-top:.5rem}.md\:items-start{align-items:flex-start}.md\:items-center{align-items:center}}@media (min-width:992px){.lg\:mt-0{margin-top:0}.lg\:justify-between{justify-content:space-between}}@media (min-width:1600px){.\33xl\:h-full{height:100%}.\33xl\:w-full{width:100%}}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

View File

@@ -0,0 +1,40 @@
const mix = require('laravel-mix')
const webpack = require('webpack')
const path = require('path')
class NovaExtension {
name() {
return 'nova-extension'
}
register(name) {
this.name = name
}
webpackPlugins() {
return new webpack.ProvidePlugin({
_: 'lodash',
Errors: 'form-backend-validation',
})
}
webpackConfig(webpackConfig) {
webpackConfig.externals = {
vue: 'Vue',
}
webpackConfig.resolve.alias = {
...(webpackConfig.resolve.alias || {}),
'laravel-nova': path.join(
__dirname,
'../../vendor/laravel/nova/resources/js/mixins/packages.js'
),
}
webpackConfig.output = {
uniqueName: this.name,
}
}
}
mix.extend('nova', new NovaExtension())

View File

@@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.2.22",
"form-backend-validation": "^2.3.3",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"vue-loader": "^16.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1,91 @@
/* Nova Field CSS */
.payout-product-card {
*,:after,:before{border:0 solid #e9ecef;box-sizing:border-box;}
:after,:before{--tw-content:"";}
body{line-height:inherit;margin:0;}
a{color:inherit;text-decoration:inherit;}
button{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0;}
button{text-transform:none;}
button{-webkit-appearance:button;background-color:initial;background-image:none;}
p{margin:0;}
input::placeholder{color:#a0aec0;opacity:1;}
button{cursor:pointer;}
:disabled{cursor:default;}
img{display:block;vertical-align:middle;}
img{height:auto;max-width:100%;}
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;}
.absolute{position:absolute;}
.relative{position:relative;}
.top-3{top:.75rem;}
.right-3{right:.75rem;}
.z-0{z-index:0;}
.z-10{z-index:10;}
.mx-auto{margin-left:auto;margin-right:auto;}
.mb-3{margin-bottom:.75rem;}
.mb-2{margin-bottom:.5rem;}
.mt-1{margin-top:.25rem;}
.ml-px{margin-left:1px;}
.-mr-3{margin-right:-.75rem;}
.flex{display:flex;}
.inline-flex{display:inline-flex;}
.h-full{height:100%;}
.h-8{height:2rem;}
.w-full{width:100%;}
.w-max{width:-webkit-max-content;width:max-content;}
.w-8{width:2rem;}
.flex-row-reverse{flex-direction:row-reverse;}
.flex-col{flex-direction:column;}
.items-center{align-items:center;}
.justify-center{justify-content:center;}
.justify-between{justify-content:space-between;}
.overflow-hidden{overflow:hidden;}
.rounded-xl{border-radius:.75rem;}
.rounded-full{border-radius:9999px;}
.rounded-\[20px\]{border-radius:20px;}
.border-2{border-width:2px;}
.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity));}
.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));}
.bg-\[\#E0E5F2\]{--tw-bg-opacity:1;background-color:rgb(224 229 242/var(--tw-bg-opacity));}
.bg-brand-900{--tw-bg-opacity:1;background-color:rgb(17 4 122/var(--tw-bg-opacity));}
.bg-clip-border{background-clip:initial;}
.object-cover{object-fit:cover;}
.p-2{padding:.5rem;}
.\!p-4{padding:1rem!important;}
.px-4{padding-left:1rem;padding-right:1rem;}
.px-1{padding-left:.25rem;padding-right:.25rem;}
.py-2{padding-bottom:.5rem;padding-top:.5rem;}
.text-xl{font-size:1.25rem;line-height:1.75rem;}
.text-sm{font-size:.875rem;line-height:1.25rem;}
.text-lg{font-size:1.125rem;line-height:1.75rem;}
.text-xs{font-size:.75rem;line-height:1rem;}
.text-base{font-size:1rem;line-height:1.5rem;}
.font-bold{font-weight:700;}
.font-medium{font-weight:500;}
.font-normal{font-weight:400;}
.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity));}
.text-brand-500{--tw-text-opacity:1;color:rgb(66 42 251/var(--tw-text-opacity));}
.text-gray-600{--tw-text-opacity:1;color:rgb(163 174 208/var(--tw-text-opacity));}
.text-navy-700{--tw-text-opacity:1;color:rgb(27 37 75/var(--tw-text-opacity));}
.shadow-3xl{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);}
.shadow-3xl{--tw-shadow:14px 17px 40px 4px;--tw-shadow-colored:14px 17px 40px 4px var(--tw-shadow-color);}
.shadow-shadow-500{--tw-shadow-color:rgba(112,144,176,.08);--tw-shadow:var(--tw-shadow-colored);}
.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,-webkit-text-decoration-color,-webkit-transform,-webkit-filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-transform,-webkit-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);}
.duration-200{transition-duration:.2s;}
.hover\:cursor-pointer:hover{cursor:pointer;}
.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(248 249 250/var(--tw-bg-opacity));}
.hover\:bg-brand-800:hover{--tw-bg-opacity:1;background-color:rgb(25 7 147/var(--tw-bg-opacity));}
.active\:bg-brand-700:active{--tw-bg-opacity:1;background-color:rgb(33 17 165/var(--tw-bg-opacity));}
@media (min-width:768px){
.md\:mt-2{margin-top:.5rem;}
.md\:items-start{align-items:flex-start;}
.md\:items-center{align-items:center;}
}
@media (min-width:992px){
.lg\:mt-0{margin-top:0;}
.lg\:justify-between{justify-content:space-between;}
}
@media (min-width:1600px){
.\33xl\:h-full{height:100%;}
.\33xl\:w-full{width:100%;}
}
}

View File

@@ -0,0 +1,71 @@
<template>
<DefaultField
:field="currentField"
:errors="errors"
:show-help-text="showHelpText"
:full-width-content="fullWidthContent"
>
<template #field>
<div class="payout-product-card">
<div class="flex">
<div class="flex flex-wrap">
<product-cart
v-for="product in currentField.products"
:product="product"
:editMode="false"
></product-cart>
</div>
</div>
</div>
</template>
</DefaultField>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
import ProductCart from './product-cart'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
components: {
ProductCart,
},
data: () => ({
values: []
}),
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.currentField.value || []
},
onSyncedField() {
if (this.currentField.products) {
this.value = Array.from(this.currentField.products).map(orderItem => orderItem.id)
this.emitFieldValueChange(this.field.attribute, this.value)
}
},
},
mounted() {
Nova.$on('orderItemChanged', (item) => {
if (! item.checked) {
this.value = Array.from(this.value).filter(element => element !== item.id);
} else {
this.value = [...this.value, item.id];
}
this.emitFieldValueChange(this.field.attribute, this.value)
})
}
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<DefaultField
:field="currentField"
:errors="errors"
:show-help-text="showHelpText"
:full-width-content="fullWidthContent"
>
<template #field>
<div class="payout-product-card">
<div class="w-full">
<div class="w-1/3">
<span>{{ __('Hasap') }}: <strong>{{ total.toFixed(2) }}</strong></span>
</div>
<div class="w-1/3">
<span>{{ __('Telekeci') }}: <strong>{{ entrepreneurTotal.toFixed(2) }}</strong></span>
</div>
<div class="w-1/3">
<span>{{ __('POSTSHOP jemi') }}: <strong>{{ postshopTotal.toFixed(2) }}</strong></span>
</div>
</div>
<div class="flex flex-wrap">
<product-cart
v-for="product in currentField.products"
:key="product.id"
:product="product"
></product-cart>
</div>
</div>
</template>
</DefaultField>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova';
import ProductCart from './product-cart';
export default {
mixins: [DependentFormField, HandlesValidationErrors],
components: {
ProductCart,
},
props: ['resourceName', 'resourceId', 'field'],
data() {
return {
total: 0,
entrepreneurTotal: 0,
postshopTotal: 0,
values: [],
prices: [],
};
},
methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue() {
this.value = this.currentField.value || []
},
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
Array.from(this.values).forEach(value => {
formData.append(this.currentField.attribute + '[]', value)
})
Array.from(this.prices).forEach(value => {
formData.append('prices[]', JSON.stringify(value))
})
formData.append('total_sum', this.total.toFixed(2))
formData.append('entrepreneur_total', this.entrepreneurTotal.toFixed(2))
formData.append('postshop_total', this.postshopTotal.toFixed(2))
},
onSyncedField() {
if (this.currentField.products) {
this.values = Array.from(this.currentField.products).map(orderItem => orderItem.id)
}
},
resetMoney() {
this.total = 0;
this.postshopTotal = 0;
this.entrepreneurTotal = 0;
},
calculateMoney() {
this.resetMoney();
this.prices.forEach(item => {
if (! this.values.includes(item.order_item_id)) {
console.log('should return')
return;
}
const unitPriceAmount = Number(item.unit_price_amount);
const costAmount = Number(item.cost_amount);
const quantity = Number(item.quantity);
const percentageDifference = unitPriceAmount - costAmount;
this.total += unitPriceAmount * quantity;
this.postshopTotal += percentageDifference * quantity;
this.entrepreneurTotal += costAmount * quantity;
});
this.total = parseFloat(this.total.toFixed(2));
this.postshopTotal = parseFloat(this.postshopTotal.toFixed(2));
this.entrepreneurTotal = parseFloat(this.entrepreneurTotal.toFixed(2));
},
},
provide() {
return {
sharedState: {
total: this.total,
entrepreneurTotal: this.entrepreneurTotal,
postshopTotal: this.postshopTotal,
values: this.values,
prices: this.prices,
calculateMoney: this.calculateMoney,
resetMoney: this.resetMoney,
},
};
},
mounted() {
Nova.$on('add_to_values', (product) => {
this.values.push(product.order_item_id)
})
Nova.$on('remove_from_values', (product) => {
this.values = this.values.filter(item => item !== product.order_item_id)
})
}
};
</script>

View File

@@ -0,0 +1,170 @@
<template>
<div
class="!z-5 relative flex rounded-[20px] max-w-200px bg-clip-border simple-shadow w-full !p-4 3xl:p-![18px] mr-1 mb-2 px product-cart-item"
>
<div class="h-full w-full">
<div class="relative w-full">
<span class="text-white mb-5">
Sany: <span class="font-bold">{{ product.quantity }}</span>
</span>
<img
:src="product.product_thumbnail"
class="mb-3 mt-5 h-full w-full rounded max-w-200px max-h-200px"
/>
<button
type="button"
class="absolute top-0 right-0 flex items-center justify-center rounded-full bg-white p-2 text-brand-500 hover:cursor-pointer"
>
<div
class="flex h-full w-full items-center justify-center rounded-full text-xl hover:bg-gray-50"
>
<input
type="checkbox"
class="checkbox scale-2"
@change="handleChange($event)"
:checked="isChecked"
/>
</div>
</button>
</div>
<div class="mb-3 px-1">
<div class="mb-2">
<p class="text-lg font-bold text-white">
{{ product.product_name }}
</p>
<ul class="mt-1 text-sm font-medium text-white">
<li>
Sargyt ID:
<span class="font-bold">{{ product.order_id }}</span>,
</li>
<li>
Haryt ID:
<span class="font-bold">{{ product.product_id }}</span>
</li>
<li>
Satylan baha:
<div v-if="editMode">
<input
class="text-black"
type="text"
v-model="unit_price_amount"
@input="fireProductPriceChangeEvent()"
>
</div>
<div v-else>
<span class="font-bold">
{{ product.unit_price_amount}}
</span>
</div>
</li>
<li>
Haryt oz bahasy:
<div v-if="editMode">
<input
class="text-black"
type="text"
v-model="cost_amount"
@input="fireProductPriceChangeEvent()"
>
</div>
<div v-else>
<span class="font-bold">
{{ product.unit_cost_amount }}
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
product: {
type: Object,
required: true,
},
editMode: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
isChecked: true,
unit_price_amount: this.product.unit_price_amount,
cost_amount: this.product.product_cost_amount,
};
},
inject: ['sharedState'],
methods: {
getProductItem() {
return {
product_id: this.product.product_id,
order_item_id: this.product.id,
unit_price_amount: this.unit_price_amount,
cost_amount: this.cost_amount,
quantity: this.product.quantity,
};
},
handleChange(event) {
const index = this.sharedState.values;
if (event.target.checked) {
Nova.$emit('add_to_values', this.getProductItem())
} else {
Nova.$emit('remove_from_values', this.getProductItem())
}
this.sharedState.calculateMoney()
},
fireProductPriceChangeEvent() {
const productIndex = this.sharedState.prices.findIndex(
price => price.order_item_id === this.product.id
);
const product = this.getProductItem();
if (productIndex === -1) {
this.sharedState.prices.push(product);
} else {
this.sharedState.prices[productIndex] = product;
}
this.sharedState.resetMoney();
this.sharedState.calculateMoney();
},
},
mounted() {
this.fireProductPriceChangeEvent();
},
};
</script>
<style>
.product-cart-item {
padding: 15px 15px 0 15px;
}
.max-w-200px {
max-width: 200px;
}
.max-h-200px {
max-height: 200px;
}
.scale-2 {
transform: scale(2);
}
.simple-shadow {
box-shadow: 0px 0px 2px 0px white;
}
.mb-2 {
margin-bottom: 8px;
}
.mt-5 {
margin-top: 15px;
}
</style>

View File

@@ -0,0 +1,7 @@
import DetailField from './components/DetailField'
import FormField from './components/FormField'
Nova.booting((app, store) => {
app.component('detail-payout-products', DetailField)
app.component('form-payout-products', FormField)
})

View File

@@ -0,0 +1,33 @@
<?php
namespace Nurmuhammet\PayoutProducts;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('payout-products', __DIR__.'/../dist/js/field.js');
Nova::style('payout-products', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Nurmuhammet\PayoutProducts;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
class PayoutProducts extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'payout-products';
/**
* Products to be rendered
*
* @param array $products
*/
public function products(array|callable|Collection $products): self
{
if (is_callable($products)) {
$products = call_user_func($products);
}
if ($products instanceof Arrayable) {
$products = $products->toArray();
}
return $this->withMeta(['products' => $products]);
}
}

View File

@@ -0,0 +1,10 @@
let mix = require('laravel-mix')
require('./nova.mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.vue({ version: 3 })
.css('resources/css/field.css', 'css')
.nova('nurmuhammet/payout-products')

View File

@@ -0,0 +1,10 @@
/.idea
/vendor
/node_modules
package-lock.json
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db

View File

@@ -0,0 +1,29 @@
{
"name": "nurmuhammet/product-inventory",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.3|^8.0"
},
"autoload": {
"psr-4": {
"Nurmuhammet\\ProductInventory\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Nurmuhammet\\ProductInventory\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1 @@

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <http://feross.org>
* @license MIT
*/
/*!
* vuex v4.1.0
* (c) 2022 Evan You
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */

View File

@@ -0,0 +1,4 @@
{
"/js/field.js": "/js/field.js",
"/css/field.css": "/css/field.css"
}

View File

@@ -0,0 +1,40 @@
const mix = require('laravel-mix')
const webpack = require('webpack')
const path = require('path')
class NovaExtension {
name() {
return 'nova-extension'
}
register(name) {
this.name = name
}
webpackPlugins() {
return new webpack.ProvidePlugin({
_: 'lodash',
Errors: 'form-backend-validation',
})
}
webpackConfig(webpackConfig) {
webpackConfig.externals = {
vue: 'Vue',
}
webpackConfig.resolve.alias = {
...(webpackConfig.resolve.alias || {}),
'laravel-nova': path.join(
__dirname,
'../../vendor/laravel/nova/resources/js/mixins/packages.js'
),
}
webpackConfig.output = {
uniqueName: this.name,
}
}
}
mix.extend('nova', new NovaExtension())

View File

@@ -0,0 +1,22 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production",
"nova:install": "npm --prefix='../../vendor/laravel/nova' ci"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.2.22",
"form-backend-validation": "^2.3.3",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss": "^8.3.11",
"vue-loader": "^16.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1 @@
/* Nova Field CSS */

View File

@@ -0,0 +1,37 @@
<template>
<DefaultField :field="field" :errors="errors">
<template #field>
<div class="md:flex md:flex-row" v-for="(item, index) in field.value">
<div class="w-full px-1 md:w-1/2">
<div class="flex relative w-full">
<select class="w-full block form-control form-input form-control-bordered link-default">
<option>{{ item.name }}</option>
</select>
<svg class="flex-shrink-0 pointer-events-none form-select-arrow" xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6">
<path class="fill-current" d="M8.292893.292893c.390525-.390524 1.023689-.390524 1.414214 0 .390524.390525.390524 1.023689 0 1.414214l-4 4c-.390525.390524-1.023689.390524-1.414214 0l-4-4c-.390524-.390525-.390524-1.023689 0-1.414214.390525-.390524 1.023689-.390524 1.414214 0L5 3.585786 8.292893.292893z"></path>
</svg>
</div>
</div>
<div class="w-full px-1 md:w-1/2">
<input
type="number"
v-model="item.value"
class="w-full block form-control form-input form-control-bordered"
disabled="disabled"
/>
</div>
</div>
<br>
</template>
</DefaultField>
</template>
<script>
export default {
props: ['index', 'resource', 'resourceName', 'resourceId', 'field'],
data: () => ({
generator: 0,
}),
}
</script>

View File

@@ -0,0 +1,133 @@
<template>
<DefaultField :field="currentField" :errors="errors">
<template #field>
<div class="md:flex md:flex-row" v-for="(item, index) in items" :key="generator">
<div class="w-full px-1 md:w-1/2">
<div class="flex relative w-full">
<select
class="w-full block form-control form-input form-control-bordered"
v-model="item.key"
@change="optionSelected(item.key)"
:disabled="item.key !== ''"
required
>
<option value="" disabled>Select</option>
<option v-for="option in item.availableOptions" :key="option" :value="option.value">{{ option.label }}</option>
</select>
<svg class="flex-shrink-0 pointer-events-none form-select-arrow" xmlns="http://www.w3.org/2000/svg" width="10" height="6" viewBox="0 0 10 6">
<path class="fill-current" d="M8.292893.292893c.390525-.390524 1.023689-.390524 1.414214 0 .390524.390525.390524 1.023689 0 1.414214l-4 4c-.390525.390524-1.023689.390524-1.414214 0l-4-4c-.390524-.390525-.390524-1.023689 0-1.414214.390525-.390524 1.023689-.390524 1.414214 0L5 3.585786 8.292893.292893z"></path>
</svg>
</div>
</div>
<div class="w-full px-1 md:w-1/2">
<input
type="number"
v-model="item.value"
class="w-full form-control form-input form-input-bordered"
/>
</div>
<IconButton
@click="removeItem(index)"
class="ml-auto"
iconType="trash"
solid
small
/>
</div>
<InvertedButton type="button" @click="addItem()" v-if="addButtonVisibility">
<span>Add</span>
</InvertedButton>
<p v-if="hasError" class="my-2 text-danger">
{{ firstError }}
</p>
</template>
</DefaultField>
</template>
<script>
import { DependentFormField, HandlesValidationErrors } from 'laravel-nova'
export default {
mixins: [DependentFormField, HandlesValidationErrors],
props: ['resourceName', 'resourceId', 'field'],
data: () => ({
generator: 0,
items: [],
options: [],
selectedValues: [],
addButtonVisibility: true,
}),
methods: {
/**
* Fill the given FormData object with the field's internal value.
*/
fill(formData) {
this.items.forEach((item, index) => {
if (item.key && item.value) {
formData.append(`${this.fieldAttribute}[${item.key}]`, item.value)
}
})
},
/**
* Add item
*/
addItem(key = '', value = '') {
this.items.push({
id: this.generator++,
key: key,
value: value,
availableOptions: this.options.filter(option => ! this.selectedValues.includes(option.value))
})
this.addButtonVisibility = false
},
/**
* Remove item
*/
removeItem(index) {
this.selectedValues = this.selectedValues.filter(item => item !== this.items[index].key)
this.items.splice(index, 1)
this.checkAddButtonVisibility()
},
/**
* Option Selected
*/
optionSelected(value) {
this.selectedValues.push(value)
this.checkAddButtonVisibility()
},
/**
* Check Add Button Visibility
*/
checkAddButtonVisibility() {
if (this.selectedValues.length === this.options.length) {
return
}
this.addButtonVisibility = true
},
},
mounted() {
this.options = Object.values(this.currentField.options)
if (this.currentField.value && this.currentField.value.length > 0) {
Array.from(this.currentField.value).forEach(value => {
this.addItem(value.id, value.pivot.stock)
this.optionSelected(value.id)
})
} else {
this.addItem()
}
}
}
</script>

View File

@@ -0,0 +1,7 @@
import DetailField from './components/DetailField'
import FormField from './components/FormField'
Nova.booting((app, store) => {
app.component('detail-product-inventory', DetailField)
app.component('form-product-inventory', FormField)
})

View File

@@ -0,0 +1,33 @@
<?php
namespace Nurmuhammet\ProductInventory;
use Illuminate\Support\ServiceProvider;
use Laravel\Nova\Events\ServingNova;
use Laravel\Nova\Nova;
class FieldServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Nova::serving(function (ServingNova $event) {
Nova::script('product-inventory', __DIR__.'/../dist/js/field.js');
Nova::style('product-inventory', __DIR__.'/../dist/css/field.css');
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Nurmuhammet\ProductInventory;
use Illuminate\Support\Collection;
use Laravel\Nova\Fields\Field;
use Laravel\Nova\Fields\SupportsDependentFields;
class ProductInventory extends Field
{
use SupportsDependentFields;
/**
* The field's component.
*
* @var string
*/
public $component = 'product-inventory';
/**
* Set the options for the select menu.
*
* @param array<string|int, array<string, mixed>|string>|\Closure|callable|\Illuminate\Support\Collection $options
*
* @phpstan-param TOption|(callable(): (TOption))|(\Closure(): (TOption)) $options
*
* @return $this
*/
public function options(array|Collection $options): self
{
return $this->withMeta([
'options' => $this->serializeOptions($options),
]);
}
/**
* Serialize Options
*
* @param Collection $options
*/
public function serializeOptions(array|Collection $options): array|Collection
{
return collect($options)->map(fn ($label, $value) => ['label' => $label, 'value' => $value]);
}
}

View File

@@ -0,0 +1,10 @@
let mix = require('laravel-mix')
require('./nova.mix')
mix
.setPublicPath('dist')
.js('resources/js/field.js', 'js')
.vue({ version: 3 })
.css('resources/css/field.css', 'css')
.nova('nurmuhammet/product-inventory')